NOIP比例简化题解:分数逼近与误差最小化的算法实现 1. 这道题到底在考什么——从“比例简化”四个字看透NOIP2014普及组的命题逻辑P2118这个编号在洛谷上一搜就跳出来标题写着“[NOIP2014 普及组] 比例简化”乍一看像小学数学应用题把6:9约成2:3。但如果你真这么做了交上去就是WA——而且是连续WA五次后才恍然大悟这不是约分是在误差允许范围内找最简整数比。我第一次做这道题时用Python写了个暴力循环从1试到10000本地测样例全过一提交——TLE。后来翻了十几份AC代码发现几乎没人用暴力全在用枚举分母向上取整找分子的策略背后其实是浮点数精度控制分数逼近思想的落地实践。这道题真正考的不是你会不会gcd而是你能不能把“比例简化”这个生活化表述准确翻译成数学语言给定两个正整数A和B要求找出一对正整数a和b满足三个硬性条件第一a:b必须尽可能接近A:B第二a和b互质第三a和b都不能超过给定上限L。注意这里“尽可能接近”不是指差值最小而是指相对误差最小——即|a/b − A/B|最小。而题目里那句“在所有满足条件的a,b中a/b ≥ A/B的优先”其实是在处理浮点比较的歧义当两个比值误差相同时选偏大的那个。这已经超出了小学数学范畴进入了计算几何中近似分数构造的底层逻辑。适合谁来啃如果你是刚学完for循环和if判断的初中生这题就是一道分水岭——它逼你第一次思考“怎么让计算机理解‘接近’这个词”。如果你是带学生刷题的教练这题必须拆开讲透为什么不能直接用A/gcd(A,B):B/gcd(A,B)因为结果可能超限L为什么不能只枚举a再算bA×b/A因为整除会丢精度为什么最优解一定出现在某个分母b对应的a⌈A×b/B⌉或⌊A×b/B⌋这背后是单调性证明误差函数极值分析。我见过太多学生卡在这一步不是不会写代码而是根本没读懂题干里隐藏的数学契约。2. 题目拆解与核心思路为什么暴力枚举行不通而“固定分母找分子”才是正解2.1 题面重述与关键约束提炼先明确输入输出输入三行第一行是A和B原比例第二行是La和b的上限输出一行两个整数a和b用空格隔开。约束条件非常干净1 ≤ A ≤ B ≤ 10^61 ≤ L ≤ 100000。注意B可以等于A但A和B都是正整数L也是正整数。现在把题干要求翻译成数学不等式a ∈ [1, L]b ∈ [1, L]gcd(a, b) 1互质目标函数最小化 |a/b − A/B|若存在多个最小值选满足 a/b ≥ A/B 的那组这里有个致命陷阱很多人以为“最简比”就是约分后的结果比如A6,B9约分得2:3。但如果L22和3都≤2不32所以2:3非法。此时必须找别的组合比如1:1误差|1−0.666...|0.333...或1:2误差|0.5−0.666...|0.166...后者更优。这说明上限L的存在彻底否定了直接约分的可行性必须重新建模。2.2 暴力枚举的复杂度灾难与实测数据假设你不管三七二十一写双重循环best_a, best_b 1, 1 min_err float(inf) for a in range(1, L1): for b in range(1, L1): if math.gcd(a, b) ! 1: continue err abs(a/b - A/B) # 处理误差相等时的优先级 if err min_err or (abs(err - min_err) 1e-9 and a/b A/B): min_err err best_a, best_b a, b时间复杂度O(L² log L)L最大10⁵L²就是10¹⁰log L按20算也2×10¹¹次操作。现代CPU每秒能跑10⁸次简单运算这段代码要跑2000秒——超时是必然的。我实测过L1000时Python纯循环要12秒L5000直接卡死。更糟的是浮点数a/b在L10⁵时会产生严重精度丢失比如a99999,b100000真实值0.99999但float64只能保证15-17位有效数字计算误差可能达到1e-15而题目要求的误差比较精度远高于此。2.3 正解思路固定b用数学推导确定最优a核心洞察在于对每个固定的分母b分子a的最优解必然是使a/b最接近A/B的那个整数。由于a必须是整数a的理论最优值是A×b/B。但a必须是整数且在[1,L]内所以实际候选只有两个a₁ floor(A×b/B) → 向下取整a₂ ceil(A×b/B) → 向上取整为什么不是四舍五入因为题目明确要求“a/b ≥ A/B的优先”所以当A×b/B不是整数时a₂ ⌈A×b/B⌉天然满足a₂/b ≥ A/B而a₁/b ≤ A/B。若两者误差相同即A×b/B恰好在两整数正中间按题意选a₂。但要注意边界a₁可能1a₂可能L。所以对每个b我们只考虑合法的a候选如果floor(A×b/B) ≥ 1则a₁ floor(A×b/B)如果ceil(A×b/B) ≤ L则a₂ ceil(A×b/B)然后对每个合法a检查gcd(a,b)1再计算误差。这样单次b的处理是O(1)总复杂度O(L log L)L10⁵时约10⁵×171.7×10⁶次操作Python轻松跑进1秒。提示为什么不用二分找a因为a的候选只有两个二分反而多此一举。很多初学者看到“最优”就条件反射想二分但这里数学结构决定了候选集极小。2.4 数学证明为什么最优解一定出现在⌈A×b/B⌉或⌊A×b/B⌋设f(a) |a/b − A/B|这是关于a的V型函数在a A×b/B处取最小值。由于a必须是整数最小值必在距离A×b/B最近的两个整数处取得。严格证明如下令x A×b/Bx 0。对任意整数a有若a ≤ floor(x)则f(a) x − a/b ≥ x − floor(x)/b若a ≥ ceil(x)则f(a) a/b − x ≥ ceil(x)/b − x而floor(x)和ceil(x)正是距离x最近的两个整数当x非整数时当x为整数时两者相等。因此全局最小值必在{floor(x), ceil(x)}中产生。这个结论不依赖于b的取值所以枚举b时只需检查这两个a值。3. 实操实现细节从读入到输出的完整链路与避坑指南3.1 输入解析与数据类型选择NOIP普及组题目输入格式很规范第一行两个整数A、B第二行一个整数L。但要注意数据范围A、B最大10⁶L最大10⁵。如果用Cint足够2³¹−1≈2×10⁹Python用int没问题但计算A×b时b最大10⁵A最大10⁶乘积最大10¹¹在Python里是long但C里int会溢出必须用long long。我见过最典型的错误是int A, B, L; cin A B L; for(int b 1; b L; b) { int a1 (A * b) / B; // 错A*b可能溢出 }正确写法long long A, B, L; cin A B L; for(long long b 1; b L; b) { long long product A * b; // 先转long long再乘 long long a1 product / B; // 整除向下取整 long long a2 (product B - 1) / B; // 等价于ceil(product/B) }Python虽无溢出问题但A*b/B是浮点除法精度不够。必须用整数运算模拟ceila2 (A*b B - 1) // B。这个技巧叫“整数向上取整公式”原理是对正整数x,y⌈x/y⌉ (xy−1)//y。验证x7,y3(73−1)//39//33正确x6,y3(63−1)//38//32错等等8//32但6/32ceil2正确。通用公式成立。3.2 GCD实现与互质判断优化普及组默认你会写gcd但很多人写递归版本def gcd(a, b): return a if b 0 else gcd(b, a % b)递归深度在A,B10⁶时最多log₂(10⁶)≈20层安全。但更推荐迭代版避免栈溢出风险def gcd(a, b): while b: a, b b, a % b return a关键优化点提前剪枝。对每个候选(a,b)先检查a是否≤L且b≤L虽然b已枚举在[1,L]但a可能超限再检查gcd(a,b)1。但gcd计算本身有开销能否跳过观察若a和b有公因子d1则d必整除a和b所以只要a和b都是偶数gcd至少为2。因此可加一层快速判断if a % 2 0 and b % 2 0: continue # 偶数对一定不互质跳过gcd计算但这只是特例。更通用的剪枝是若a1或b1则gcd1直接接受。实践中由于L≤10⁵gcd调用次数最多2×10⁵次每次平均10步完全可接受不必过度优化。3.3 误差计算的精度陷阱与安全比较这是本题最隐蔽的坑。直接计算abs(a/b - A/B)会引入浮点误差。例如A1,B3,L2理论最优是1:2误差|0.5−0.333...|0.166...但float计算可能因二进制表示误差变成0.16666666666666666或0.16666666666666663导致比较失败。正确做法用交叉乘法消除除法。比较|a/b − A/B| |c/d − A/B|等价于比较|a×B − A×b| × d×B |c×B − A×d| × b×B两边同乘b×d×B正数不改变不等号方向得 |a×B − A×b| × d |c×B − A×d| × b但我们需要的是绝对误差最小且处理相等情况。最终比较逻辑应为计算err_val abs(aB - Ab) * 1.0 / (b*B) // 仍用浮点但分子是整数精度更高或者更稳妥存储分子diff abs(aB - Ab)和分母denom bB比较diff1denom2 diff2*denom1但题目只要求输出一组最优解不需要排序所有所以用浮点epsilon比较更简洁EPS 1e-12 current_err abs(a * B - A * b) / (b * B) # 分子是整数分母是整数精度损失小 if current_err best_err - EPS: best_err current_err best_a, best_b a, b elif abs(current_err - best_err) EPS: # 误差相等检查a/b A/B if a * B A * b: # 等价于a/b A/B避免除法 best_a, best_b a, b注意a * B A * b是整数比较绝对精确这才是题干“a/b ≥ A/B”的无误差实现。3.4 完整代码实现Python版与逐行注释import math # 读入数据 A, B map(int, input().split()) L int(input()) # 初始化最优解设为1:1总是合法 best_a, best_b 1, 1 # 用整数形式存储误差比较基准|a*B - A*b| / (b*B) # 为避免浮点我们用分子diff |a*B - A*b| 和分母denom b*B 来比较 # 但为简化先用浮点加EPS处理 best_diff abs(1 * B - A * 1) # 分子部分 best_denom 1 * B # 分母部分 # 枚举分母b从1到L for b in range(1, L 1): # 计算理论最优分子x A * b / B # 候选a1 floor(x), a2 ceil(x) product A * b # a1 floor(product / B) a1 product // B # a2 ceil(product / B) (product B - 1) // B a2 (product B - 1) // B # 检查a1是否合法1 且 L if a1 1 and a1 L: # 检查互质 if math.gcd(a1, b) 1: diff abs(a1 * B - A * b) # |a1*B - A*b| denom b * B # b*B # 比较误差diff/denom 与 best_diff/best_denom # 用交叉乘法diff * best_denom best_diff * denom ? if diff * best_denom best_diff * denom: best_diff diff best_denom denom best_a, best_b a1, b elif diff * best_denom best_diff * denom: # 误差相等检查a1/b A/B 即 a1*B A*b if a1 * B A * b: best_a, best_b a1, b # 检查a2是否合法 if a2 1 and a2 L and a2 ! a1: # 避免重复计算当product%B0时a1a2 if math.gcd(a2, b) 1: diff abs(a2 * B - A * b) denom b * B if diff * best_denom best_diff * denom: best_diff diff best_denom denom best_a, best_b a2, b elif diff * best_denom best_diff * denom: if a2 * B A * b: best_a, best_b a2, b print(best_a, best_b)这段代码通过整数运算规避了浮点精度问题用交叉乘法比较误差逻辑清晰。实测在L100000时Python 3.8运行时间约0.8秒完全满足NOIP时限。4. 常见问题与排查技巧实录那些年我们踩过的坑4.1 “样例过了但提交WA”的五大高频原因我整理了洛谷P2118讨论区前50页的WA记录归纳出以下五类问题附带调试方法问题类型具体表现根本原因调试技巧精度丢失样例1A6,B9,L10输出2 3但A1,B3,L2输出1 2失败用a/b - A/B直接浮点比较1e-15级误差导致判断错误在代码开头加print(abs(1/2 - 1/3))看是否输出理想值改用a*B - A*b整数比较边界遗漏L1时输出1 1但A1000000,B1000000,L1应输出1 1正确A2,B1,L1却输出1 1错误因为2/121/11误差1-21但a1,b1是唯一合法解互质误判A4,B6,L3理论最优是2:3gcd1但代码输出1:1gcd函数写错如return gcd(b, a%b)漏了a,b b, a%b写个测试函数print(gcd(4,6))应输出2print(gcd(2,3))应输出1优先级逻辑错误差相等时本该选a/b≥A/B却选了小的比较a/b A/B用了浮点除法或写成a*B A*b漏了等号打印所有候选解的a*B和A*b看是否满足a*B A*b用而非初始化错误L1时若A1,B1000000最优是1:1但代码初始化为1:1误差1-0.0000010.999...而实际没有更好解4.2 性能瓶颈定位与加速技巧当L10⁵时Python可能卡在0.9秒边缘。优化点如下GCD预计算对b从1到La从1到Lgcd(a,b)可预计算成二维数组但空间O(L²)10¹⁰不可行。改为对每个b预计算其质因子再检查a是否含相同因子——更复杂不推荐。减少math.gcd调用用内置math.gcd比自写快但仍有开销。可对小数值用查表法预先计算1~1000内所有数对的gcd存入dict但L10⁵时大部分b1000收益有限。最有效优化剪枝。观察当b很小时a10非法a2可能超L当b很大时a1和a2都趋近A×b/B但若A×b/B L则a2L只剩a1而a1可能1。所以可提前break当b L×B//A时A0a1 A×b//B La2 L后续b全非法。计算临界b₀ L×B//A 1枚举b从1到min(L, b₀)。实测L10⁵,A1,B10⁶时b₀10⁵×10⁶//110¹¹无剪枝但A10⁶,B1时b₀10⁵×1//10⁶0直接跳过。这个优化要看A,B比例平均节省10%-20%时间。4.3 测试用例设计覆盖所有边界场景光跑样例不够必须自己造数据。我常用的六组测试用例基础样例A6,B9,L10 → 输出2 3约分结果且未超限超限样例A6,B9,L2 → 输出1 2因为2:3中321:2误差更小大数样例A1000000,B1000000,L100000 → 输出1 1A/B1最优是1:1精度敏感样例A1,B3,L2 → 输出1 2|0.5-0.333...|0.166... |1-0.333...|0.666...误差相等样例A1,B2,L3 → 候选1:2误差0、2:4非法gcd2、3:6非法。但1:2和2:4不互质唯一解是1:2。要构造相等需A3,B6,L23:61:2候选1:2误差0、2:4非法。还是不行。真正相等A2,B4,L32:41:2候选1:2误差0、2:4gcd2非法、3:6b6L3非法。所以需要A3,B6,L3理论x3*b/6b/2b2时x1a1a21误差0b4L不考虑。还是不行。最终构造A1,B1,L2所有ab都满足a/b1误差0按题意选a/b≥1即所有a≥b且互质。a1,b1a2,b12/12≥1a2,b2gcd2非法。所以最优是2:1。验证|2/1−1/1|1|1/1−1/1|0所以1:1更优。哦误差0才是最小。所以相等场景极少但代码必须处理。极端边界A1,B1000000,L1 → 只能选1:1误差|1−0.000001|0.9999994.4 从普及组到提高组这道题的延伸思考这道题看似简单实则是连分数逼近的入门题。最优解a/b本质上是在分母≤L的有理数中对A/B的最佳逼近。数学上最佳逼近由A/B的连分数展开给出如A/B0.666...2/3连分数[0;1,2]收敛子1/1,2/3。但NOIP普及组不要求这个所以枚举b是合理解法。但如果你学过提高组会知道Stern-Brocot树能O(log L)找到最优解。Stern-Brocot树是所有正有理数的二叉搜索树根为1/1左子为a/(ab)右子为(ab)/b。从根开始若当前节点a/b A/B则向右走若a/b A/B则向左走直到分母L。路径上的节点就是逼近序列。不过这超纲了普及组掌握枚举法足矣。最后分享个小技巧考试时如果时间紧先写暴力L≤1000可用再逐步优化。我教学生时强调先让代码跑起来再让它跑得快。很多学生卡在“想一步到位写最优”结果调试半天没输出不如先交个暴力拿30分。5. 工具与环境配置如何搭建本地测试环境并高效调试5.1 本地测试框架搭建NOIP不提供IDE但本地调试必须高效。我用Pythonpytest搭建简易测试框架# test_p2118.py import pytest from io import StringIO from unittest.mock import patch def solve(): # 把你的主程序逻辑放这里返回a,b pass pytest.mark.parametrize(input_str,expected, [ (6 9\n10, 2 3), (6 9\n2, 1 2), (1 3\n2, 1 2), ]) def test_p2118(input_str, expected): with patch(builtins.input, side_effectinput_str.split(\n)): result solve() assert f{result[0]} {result[1]} expected运行pytest test_p2118.py -v即可批量测试。好处是修改代码后一键回归不怕改坏。5.2 在线评测平台的特殊注意事项洛谷P2118的输入是标准输入但有些平台如Codeforces可能有多组测试。本题是单组但习惯性加个while True try-except更保险import sys try: data sys.stdin.read().split() if not data: break A, B int(data[0]), int(data[1]) L int(data[2]) # 主逻辑 except EOFError: pass但NOIP官方数据一定是单组不必复杂化。5.3 时间复杂度实测与性能监控用time模块测单次运行import time start time.time() # 运行solve() end time.time() print(fTime: {end-start:.4f}s)对L100000我的代码输出Time: 0.7823s符合1秒时限。如果超时先检查是否用了math.gcdC用__gcd更快再检查是否有O(L²)循环残留。5.4 调试信息输出开关竞赛代码不能有print但开发时需要。我用DEBUG开关DEBUG False if DEBUG: print(fb{b}, a1{a1}, a2{a2}, diff{diff})提交前设为False或用sys.argv控制python p2118.py debug。注意NOIP禁止使用文件IO所有输入输出必须用stdin/stdout。这点务必牢记否则编译错误。我在实际教学中发现学生最大的问题不是不会算法而是调试能力弱。看到WA就慌不知道从哪查。所以我要求他们每次WA先写一行print(DEBUG:, A,B,L)确认输入读对再打印第一个b的a1,a2看计算是否正确最后打印所有被接受的候选解。三步下来90%的问题都能定位。这比盲目改代码高效十倍。这个题目的价值远不止于AC。它教会你把自然语言需求翻译成数学约束再把数学约束转化为可计算的算法步骤。这种能力在任何编程场景中都是核心竞争力。我带过的学员后来做数据分析时处理“在预算内找最优配置”做游戏开发时做“在帧率限制下找最高画质”思路都源于这道“比例简化”。

相关新闻

最新新闻

2026年福建做智慧排水监测系统的公司前10名有哪些?

2026年福建做智慧排水监测系统的公司前10名有哪些?

台风中心刚在福建沿海登陆,福州城区的雨势却没有立刻减弱,天文大潮正把闽江水顶进雨水排口,管网液位在短时间内快速抬升,调度大屏上的监测点陆续变红。这样的场景,福建沿海城市每年汛期都可能遇到——暴雨和潮水同时到…

2026/8/24 9:22:47
PyAMG快速上手教程:5分钟从安装到求解2D Poisson方程

PyAMG快速上手教程:5分钟从安装到求解2D Poisson方程

PyAMG快速上手教程:5分钟从安装到求解2D Poisson方程 【免费下载链接】pyamg Algebraic Multigrid Solvers in Python 项目地址: https://gitcode.com/gh_mirrors/py/pyamg PyAMG 是一个用 Python 编写的代数多重网格(Algebraic Multigrid, AMG&a…

2026/8/24 9:22:47
99.3% 通过率背后的失败模式:Multi-Agent-CAD 与单 Agent CAD 生成器的 3 类典型错误深度对比

99.3% 通过率背后的失败模式:Multi-Agent-CAD 与单 Agent CAD 生成器的 3 类典型错误深度对比

99.3% 通过率背后的失败模式:Multi-Agent-CAD 与单 Agent CAD 生成器的 3 类典型错误深度对比 【免费下载链接】Multi-Agent-CAD MAC (Multi-Agent CAD): A decoupled multi-agent framework for text-to-CAD generation via constrained test-time compute 项目地…

2026/8/24 9:22:47
TDAD框架:用测试驱动与图分析为AI编码助手构建安全沙盒

TDAD框架:用测试驱动与图分析为AI编码助手构建安全沙盒

1. 项目概述:当AI编码助手开始“闯祸”,我们如何为它装上“刹车”?最近半年,我身边用上AI编码助手(比如GitHub Copilot、Cursor、Claude Code)的开发者越来越多了。效率的提升是肉眼可见的,以前…

2026/8/24 9:22:47
多智能体协同规划框架:零维降阶模型在复杂系统优化中的应用

多智能体协同规划框架:零维降阶模型在复杂系统优化中的应用

1. 项目概述:当降阶模型遇上多智能体规划如果你在工程仿真、系统优化或者复杂流程控制领域工作过,大概率对“降阶模型”这个词不陌生。它本质上是一种“高保真”的简化模型,用极少的变量(比如几个关键状态参数)去捕捉一…

2026/8/24 9:22:47
浅谈C++/C关于#define的那些奇奇怪怪的用法

浅谈C++/C关于#define的那些奇奇怪怪的用法

前言 众所周知,#define(也就是宏定义)在C/C里用处很广泛。对于一个萌新小白来说,宏定义有以下几种用法: 1 缩减代码 第一种用法与typedef类似,而且比typedef应用得更广泛。举个例子,在以下C程序中,unsig…

2026/8/24 9:17:46