☰
浮点二分入门:AcWing 790数的三次方根与精度控制详解
2026/9/28 14:36:20 网站建设 项目流程

1. 浮点二分的基础课:为什么三次方根是个二分问题

先说说我当年在 AcWing 算法基础课上做这道题时的真实感受。乍看标题"AcWing 790. 数的三次方根",第一反应是"这有什么好讲的,直接调用库函数不就行了?"——在 C++ 里用pow(n, 1.0/3.0)或者 Python 里用n ** (1/3)都能得到结果。但如果你真这么交上去,虽然能过样例,却错过了这道题真正想让你练的东西:浮点数二分模板。

AcWing 的题目编号 790 是浮点数二分的经典入门题,它在算法基础课里的定位,和整数二分的"数的范围"(789)是紧挨着的。两题放一起,就是让你一次性把二分思想吃透,搞清楚整数二分和浮点数二分到底差在哪里,为什么浮点二分不需要考虑mid到底是取l还是l+1。这是理解整个二分体系的关键一环,跳过去的话,后面遇到"求方程近似解""三分求极值"这类变形题,你会很容易绕晕。

题目的描述其实很短:给定一个浮点数n,求它的三次方根,结果保留 6 位小数。n的范围是浮点数,负数的三次方根也是合法的(比如 -8 的三次方根是 -2)。所以这不是纯数学题,也不是纯编程题,而是"用算法求方程的近似解"的经典应用:解x^3 = n这个方程。

为什么必须是二分?因为 f(x) = x^3 - n 这个函数是严格单调递增的。单调函数求零点,二分是最朴素、最可靠、最好证明正确性的方法。你不需要导数的符号判断,不需要迭代收敛性分析(比如牛顿迭代),只需要一个基本事实:如果 f(mid) 和 f(l) 异号,零点就在 [l, mid] 里,否则就在 [mid, r] 里。这个逻辑比舍入误差更稳定,比盲猜更可控。

这道题也特别适合用来理解"精度"这个概念。你要保留 6 位小数,那么二分的终止条件是什么?不是l < r,而是r - l > eps,这个eps怎么取、取多少合适,直接决定了你代码是对的还是错在边界上。我第一次做的时候就在这个问题上翻了车,后面展开细说。

适合什么人看这道题?一是正在刷 AcWing 算法基础课、做到二分这一章的人;二是学过数据结构、但一直没搞懂"二分除了在有序数组里查找之外还能干嘛"的人;三是想系统整理浮点数二分边界细节、避免精度判题 WA 的人。这篇文章我会把从题意拆解到代码实现、再到踩坑复盘的全过程都写出来,保证你跟着看一遍就能彻底把这 6 分题稳稳拿到手。

2. 边界选择:为什么区间的左右端点不能拍脑袋定

很多新手拿到这道题,第一步就卡住了:三分之一的边界到底怎么定?有人说l = 0, r = n,有人说l = -10000, r = 10000,还有人直接l = -1e5, r = 1e5。哪个对?为什么?

先说最常见的坑:l = 0, r = n这个写法在 n 是正数时没问题,但 n 一旦小于 1,比如 n = 0.008,你就犯了方向性错误。因为 0.008 的三次方根是 0.2,它比 n 本身还要大。如果你把右边界定成r = n = 0.008,那整个可行区间 [0, 0.008] 里压根不含答案 0.2,二分无论如何都不可能收敛到正确值。这是我当年做这道题遇到的第一个大坑,换了三个写法才真正弄明白问题不是出在二分逻辑,而是出在边界上。

那稳妥的方案是什么?

第一种,也是最省脑子的方案:直接取一个覆盖题目所有可能输入的大区间,比如l = -10000, r = 10000。这个范围怎么来的?因为题目中说 n 是浮点数,虽然没有严格限制范围,但在 AcWing 平台这类题目通常要求 n 在绝对值 10000 以内。三次方根在 [-22, 22] 左右,边界留足余量即可。这种做法的好处是:不管 n 是正、是负、是小绝对值还是大绝对值,答案一定落在区间里。坏处是:如果你把eps定得特别小,比如 1e-8,那区间长度是 20000,你需要迭代大约log2(20000 / 1e-8)次,大概是 51 次左右。这完全是可以接受的,浮点二分不像整数二分有步数焦虑,多几次迭代毫无压力。

第二种,稍微优雅一点:l = -abs(n) - 1, r = abs(n) + 1。这个做法是"以 n 的绝对值包络答案"。理由是这样的:当 |n| > 1 时,n 的三次方根绝对值一定小于 |n|;当 |n| < 1 时,n 的三次方根绝对值反而大于 |n|。所以你直接取[-max(1, |n|), max(1, |n|)]其实是更严谨的写法。但大多数人图省事,直接取[-1e5, 1e5]也不会错,只是看起来不够讲究。

第三种,根据符号动态调整。我后来在实际工程里比较喜欢这样写:

double n; scanf("%lf", &n); double l = -1e5, r = 1e5;

然后在二分里判断mid * mid * mid >= n来决定往左还是往右。这个写法不需要讨论正负号,因为三次方根函数在整个实数域上是单调递增的,判断条件天然统一。

所以边界选择的结论就是:如果你不想动脑子,就把边界设成 ±1e5;如果你想让代码显得有理论依据,就用l = min(-1.0, n) - 1, r = max(1.0, n) + 1这类写法。但无论如何,不要写l = 0, r = n然后祈祷 n 是大于 1 的正数。算法竞赛里没有什么比"边界之外藏着答案"更隐蔽的错误了。

2.1 为什么负数不需要单独讨论

浮点数二分和整数二分一个很大的区别就是:你不需要对负数做特判。整数二分里,mid = (l + r) / 2在负数区间上会有取整方向的问题,所以你得区分l + r >> 1是下取整,然后对应l = mid + 1或者r = mid - 1。但浮点二分里,mid就是精确的中间值,不存在取整方向问题,判断条件也统一是正负号自带方向。

具体到这道题:你要找的是 x 使得x^3 = n。如果 n = -8,那 x = -2。你把区间设成 [-10000, 10000],mid 从 0 附近开始收敛,通过mid * mid * mid和n的比较,自然会把区间压到负数那一边。没有特殊分支,没有绝对值转换,代码量直接减半。

这一点恰恰是这道题想教的:浮点数的连续性让二分变得异常干净,你只需要关心"往左还是往右",完全不用关心"边界+1还是-1"。很多人在整数二分里反复背模板、背mid取值的口诀,到浮点二分这里反而懵了,其实是因为没意识到浮点二分才是二分的本质——连续空间上的单调逼近,整数二分只是因为离散取整才引入了一堆麻烦。

2.2 边界取 ±1e5 的合理性验证

如果题目数据范围没有明说,你怎么知道你设置的边界一定安全?有个简单的验证方法:在二分结束后,打印l和r,看看它们是否落在边界内部很远的地方。如果答案靠近边界,说明你的边界有问题,需要扩大。实际上 n 的三次方根增长非常缓慢:n = 1e9 时根才 1000,n = 1e12 时根才 10000。所以对绝大多数浮点数输入,±1e5 完全是碾压级别的安全。哪怕遇到 n = 1e15,根也就 21544 左右,依然安全。除非 n 是 1e18 量级,但那已经不属于这类基础题的考察范围了。

那为什么不直接把边界设成 ±1e18 一劳永逸?因为浮点数在极大边界下做二分,mid * mid * mid可能会发生溢出或精度下降。虽然double能表示 1e308 那么大,但三次方乘法的中间积一旦超过 1e154,精度就开始恶化。所以边界宁可行程大,也不要大到失去精度。±1e5 是个记忆负担小、理论无懈可击的选择,我个人的建议是直接照抄这个值,不要自己发挥改成 ±1e4 或者 ±1e9。

3. 精度控制的核心:eps 到底取多少才不会 WA

这是浮点数二分里最值得掰扯清楚的细节。题目要求保留 6 位小数,那你的二分终止条件r - l应该小于多少?很多人第一反应是"保留 6 位,那 eps 取 1e-6 呗"。这个想法在思路上没错,但在实际判题中容易踩到边界错误,需要解释清楚。

3.1 为什么 1e-6 可能不够用

假设你二分结束时区间长度是 1e-6,也就是说答案在 [l, r] 里,且 l 和 r 的差是 1e-6。这时候如果你输出 l,经过四舍五入保留 6 位小数,结果和真实答案的误差是多少?最坏情况下,答案在你区间的右端点 r,而你输出了 l,两者的差是 1e-6。保留 6 位小数时,它对最后一位的影响是"满格"的——可能把 0.123456 输出成 0.123457,也可能输出 0.123455。如果判题系统的误差容限是 1e-6,你就有可能在临界数据上 WA。

这有点像你拿一把最小刻度是 1 毫米的尺子去量一根 1.0004 厘米的线,读数没有太大偏差,但如果要你精确到 0.1 毫米,必须在读数后再估一位。二分这里也一样:你想要的输出精度是 6 位小数,那你的计算精度就应该是 7 位甚至 8 位,让"计算精度"严格高于"输出精度",才不会出现边界抖动。

所以常规做法是eps = 1e-8,有些保守派甚至会取1e-10。1e-8 的意思就是:把区间压缩到 0.00000001,这比 6 位小数(1e-6)高两个数量级。此时输出 l 或 r 的任意一个,保留 6 位都是一模一样的字符串,判题绝对不会因为输出端点选择而扣你分。

3.2 迭代次数和 eps 的关系

盲目缩小 eps 也不是完全没代价。区间 [l, r] 的长度假设是 2e5(就是 ±1e5),eps 取 1e-8,那理论上需要迭代的次数大约是多少?每迭代一次区间折半,区间长度从 2e5 降到 1e-8,需要将 2e5 除以 2 共多少次小于等于 1e-8?算一下:

  • 2e5 约等于 2^18
  • 1e-8 约等于 2^-27
  • 总共需要 18 + 27 = 45 次左右

如果 eps 取 1e-10,那就多迭代 7 次,52 次左右。在实际运行时,四五十次循环对计算机来说连"开销"都算不上,所以把 eps 设得更小一些,比如 1e-8 或 1e-10,性能上完全不用担忧。这也是浮点二分比整数二分让人舒心的地方:整数二分你担心死循环,浮点二分你只需要担心精度够不够,循环次数天然可控。

这里我要分享一个我自己用得很顺手的经验:代码里不要写死while (r - l > 1e-8),而是先定义一个const double eps = 1e-8;,后面要调精度时只改一个值。如果你用 C++ 写,还可以直接写while (r - l > eps)。原因很简单:浮点数的比较和 debug 时,命名常量比魔法数字可读性好得多,而且当你把这道题的模板迁移到别的浮点二分场景时,比如求平方根、求对数近似值,只需要调整 eps 一个量,不会四处找散落的魔法数字。

3.3 输出格式的细节:l 还是 r,以及 printf 的舍入模式

二分结束时区间的左右端点都在误差范围内,输出哪一个理论上都可以。但如果你真的在临界数据上测试,可能会发现输出 l 和输出 r 在第 6 位上有 1 的偏差。因为浮点数在计算机里的表示不是十进制的,二进制的舍入误差和十进制保留位数的舍入误差会在边界处打架。解决方式很简单:用printf("%.6lf\n", l)输出左端点(或者右端点),并且保证 eps 足够小,二者舍入后一致。我自己的习惯是输出左端点,因为整个二分过程里l始终是"可行解的下界",语义上更稳妥。

还有一个很多人忽略的点:C 语言的 printf 浮点数舍入是四舍六入五成双(银行家舍入)还是四舍五入?实际上在绝大多数主流平台和编译环境里,printf 对二进制浮点数转十进制输出采用的是"当前舍入模式"(通常是到最近偶数),但因为你已经把 eps 压到了远小于输出精度的程度,这个舍入模式差异基本不会影响结果。只有在你 eps 恰好压线 1e-6 时才可能出现 0.000000 和 0.000001 的分野。再次归结到核心建议:eps 取输出精度的百分之一甚至千分之一,是浮点二分最稳的打法。

4. 二分的判断条件与单调性证明

二分能不能用,核心在于单调性。这道题和"有序数组查找"还不完全一样,你面对的是一个连续函数 f(x) = x^3,它是不是单调递增的?答案显然是,但为了心里踏实,还是要走一遍逻辑:

  • 任意取 x1 < x2,那么 x1^3 < x2^3,这个结论对负数也成立。比如 -2 < 1,(-2)^3 = -8 < 1^3 = 1。所以整个实数轴上 x^3 严格单调递增。
  • 对任意给定的 n,方程 x^3 = n 有且仅有一个解。这意味着二分的判断条件"mid 的三次方大于等于 n 就往左收"永远是良定义的,不存在多解歧义。

于是核心循环体就三行:

while (r - l > eps) { double mid = (l + r) / 2; if (mid * mid * mid >= n) r = mid; else l = mid; }

这段代码的判断条件是mid * mid * mid >= n。为什么大于等于时往左收?因为三次方根函数是单调递增的,如果 mid 的三次方已经大于 n,说明 mid 偏大,真正解在 mid 左边,所以把右边界拉到 mid。如果 mid 的三次方还小于 n,说明 mid 偏小,真正解在 mid 右边,所以把左边界拉到 mid。整个过程就是不停地把"藏着答案的区间"缩小。

这里我想强调一个容易思维混乱的地方:有些同学会把判断条件和线性查找类比,写成 "如果 n 大于 mid 的三次方就l = mid",但代码里却是if (mid * mid * mid >= n)。其实等价的,只是方向问题。我的建议是:每次写二分都先写清楚"当前 mid 是偏大还是偏小?偏大往哪收?"这个思维链条,不要背模板。模板是给人用的,但考场上一紧张模板会忘,思维链不会。

还有一个性能细节:为什么这里用mid * mid * mid而不是pow(mid, 3)?一是pow走的是通用幂运算,内部可能调用 exp/log 组合,性能比三次乘法慢一个量级,在这种循环四五十次的场景里差别不大,但在更复杂的浮点二分里会有明显差距;二是pow在负数底数、小数指数时可能出现定义域问题或复数分支,虽然指数是整数 3 可以绕开,但乘法在语义上绝对安全。

你在 AcWing 上提交代码后会看到实际耗时,通常都是个位数毫秒,但这不代表你可以随意挥霍性能——尤其后面学到三分、牛顿迭代、自适应辛普森,性能习惯从现在就养起。

5. 从 AcWing 790 到通用浮点二分模板的抽象

说实话,AcWing 790 这道题的代码量非常小,核心部分可能就十行。但它的价值在于:让你把浮点二分的骨架抽出来,作为一种解决"单调连续函数求零点"的通用方法。现在我每次遇到"给定 f(x),找 x 使得 f(x) = target"这类问题,无论 f 是三次方根、指数函数、还是自定义的复杂函数,都会直接套这个骨架:

// 通用浮点二分模板 const double eps = 1e-8; double l = -1e5, r = 1e5; // 根据题目边界调整 while (r - l > eps) { double mid = (l + r) / 2; if (f(mid) >= target) r = mid; else l = mid; } // 输出 l 或 r printf("%.6lf\n", l);

唯一需要改的是f(mid)的具体实现和目标值 target。比如求平方根,判断条件换成mid * mid >= n即可;求x + sin(x) = c这种方程的近似解,只要保证左式单调(当然这里 x + sin(x) 不是全局单调,需要先找单调区间),照样可以用同一套模板。

所以我的建议是:别把 790 当成一道即将被遗忘的签到题,它是你浮点二分模板库的起点。把这个模板默写下来,比背任何奇技淫巧都划算。

5.1 浮点二分和整数二分的模板对照表

这里我放一张对照表,让两种二分各自的特征更清晰:

维度整数二分浮点二分
mid 计算mid = l + r >> 1(有取整方向)mid = (l + r) / 2(精确)
边界更新l = mid + 1 / r = mid - 1(跳过 mid)l = mid / r = mid(区间包含 mid)
终止条件l > r 或 l == rr - l <= eps
死循环风险存在,需模板配合几乎不存在,只有精度顾虑
适用场景离散序列查找、边界定位连续函数求零点、方程近似解
典型复杂度O(log n) 次比较O(log((R-L)/eps)) 次迭代

从表里能看出来,浮点二分的模板通用性更强,心智负担更低。而整数二分的两个模板(l = mid + 1配合mid = l + r + 1 >> 1,以及r = mid配合mid = l + r >> 1)就是为了应对离散性和死循环风险才演化出来的。学完这道浮点二分再回头看整数二分,你会更容易理解那些别扭的规则到底在防什么事。

5.2 073 变体:如果题目要求负数的三次方根怎么办

有的同学可能在别的 OJ 上看到这道题的变体版本:输入可能包含负数,问你输出它的三次方根。其实 AcWing 790 本身就包含负数输入,网上有一些题解额外写if (n < 0) return -cbrt(-n)这种分支,实际上是完全多余的。直接在 [-1e5, 1e5] 区间上做二分就能正确处理负数,因为三次方根函数是整个实数域单调的。这个多余分支反映了作者对浮点二分单调性的理解还不到位,你不用学它。

但如果你真遇到了一个"规定数值范围为负"的极端场景,比如 n = -1e12,那二分一样处理:mid 在负数域内不断调整,最终收敛到约 -10000,整个过程中mid * mid * mid一直也是负数,和 n 的比较逻辑依然正确。这是浮点数二分的漂亮之处——它不关心你的数值是正是负,只关心单调方向。

6. 实测代码与运行效果

光说不练假把式,我贴一份我实际提交过的 C++ 完整代码,再贴一份 Python 版本,给你做个双语言参考。

C++ 版本:

#include <cstdio> int main() { double n; scanf("%lf", &n); double l = -1e5, r = 1e5; const double eps = 1e-8; while (r - l > eps) { double mid = (l + r) / 2; if (mid * mid * mid >= n) r = mid; else l = mid; } printf("%.6lf\n", l); return 0; }

Python 版本:

n = float(input()) l, r = -1e5, 1e5 eps = 1e-8 while r - l > eps: mid = (l + r) / 2 if mid ** 3 >= n: r = mid else: l = mid print(f"{l:.6f}")

两个版本逻辑完全一致。C++ 跑 AcWing 平台快,Python 日常验证思路方便,你随便选一个学透都行。

6.1 几组手算验证数据

我拿几个容易出错的 n 值,把二分跑的中间过程手算一下,方便你对答案:

  1. n = 27:预期输出 3.000000。区间初始 [-1e5, 1e5],mid = 0,0^3 = 0 < 27,l 跳到 0;mid = 50000,显然大于 27,r 跳到 50000……不断折半后收敛到 3.000000。这个例子看似简单,但验证了算法在正数上的基本表现。

  2. n = -8:预期输出 -2.000000。注意 mid 在负数区域时,mid * mid * mid也是负数,和 n 比较时大小关系正确。比如 mid = -1e5 时,mid^3 = -1e15 < -8,说明当前 mid 偏小(-100000 比 -2 小得多),所以 l 跳到 -1e5 ?不对,是 l = mid?看判断条件:-1e15 < -8,条件为假,走 else,所以 l = mid = -1e5——等等,这里需要仔细走一遍:mid = 0 时,0^3 = 0 >= -8,成立,r = 0;mid = -50000 时,(-50000)^3 = -1.25e14 >= -8?显然不成立(负数比较大小,-1.25e14 远远小于 -8),所以 l = -50000。这样区间不断往 0 附近收,最终收敛到 -2。整个过程中 l 始终小于真实解,r 始终大于真实解,符合二分的闭环性质。

  3. n = 0.008:预期输出 0.200000。这个数据如果 l = 0, r = n 就会 WA,但用 ±1e5 的边界毫无压力。收敛后区间在 0.2 附近,输出 0.200000。

  4. n = 0:预期输出 0.000000。mid = 0 时 mid^3 = 0 >= 0,r = 0,此后 l 不断往 0 靠,输出 0,没毛病。

6.2 实测中常见的坑:midmidmid 溢出与精度

有些人用 float 而不是 double,在 n 比较大时mid * mid * mid会先算成 float,精度丢失严重,导致二分收敛不稳定。解法很简单:全部用 double,不要混用。如果你的编译器把mid声明成 double,那mid * mid * mid自动是 double 运算,没有风险。但如果你写成float mid,哪怕 r 和 l 是 double,中间运算也会退化成 float,精度直接掉一个量级。我见过有人用 float 交这道题 WA 了三次找不到原因,最后就是把类型改成了 double 就好了。

另一个隐藏坑是:有的 OJ 开了-O2优化后,浮点运算的中间精度可能在不同架构上有细微差异,但 AcWing 的评测环境很标准,不需要担心这个。你只需要保证自己的逻辑是对的,然后用 double 运算,这 6 分就是手拿把攥的。

7. 我的常见错误总结:三份 WA 代码的复盘

7.1 错误一:把右边界直接设成 n

这是我在网上解答时看到的最频繁的错误代码形式:

double l = 0, r = n;

只要 n 是大于 1 的正数,这段代码确实能过。但它隐藏着两个隐患:一是 n 小于 1 时直接 WA,二是 n 为负数时 l = 0, r = n 导致区间为空。有些同学说自己"运气好,过了",那是只测了 n = 8 之类的用例,一旦在 OJ 上遇到 n = 0.001 的测试点就直接挂掉。所以边界设计不是"能跑就行",而是要"在所有合法输入下都正确"。我后来做了一道数据范围更大的浮点二分题,深刻体会到边界写不对,改起来比写新题还痛苦。

7.2 错误二:终止条件写成r - l >= 1e-6

这个错误在思路上更隐蔽:你觉得自己取 1e-6 和输出精度 1e-6 刚好对齐很合理,但实际上属于"极限操作"。前面我们已经算过,最坏情况下输出误差是满 1e-6,会导致结果在第 6 位小数上摇摆。把 eps 改成 1e-8 后,这个问题直接消失。我的经验是:浮点二分的 eps 永远要比输出精度小两个数量级,这是看起来"浪费"但永远正确的策略。

7.3 错误三:判断条件写成mid * mid * mid <= n且对应地l = mid

这个写法其实在数学上也是对的(因为函数单调递增),但方向反着写容易和r = mid搞混。我不建议你反向写的原因是:当你的目标函数不是严格单调、而只是单调不降时,反向判断容易漏掉相等边界。对于三次方根这种严格单调的情形,两种方向都行;但为了养成好习惯,我一直保留"大于等于则收右边界"的写法,这样在复杂函数里也更安全。

8. 延伸:很多算法题背后都是浮点二分的变形

最后说点"这道题以外"的东西。浮点二分在竞赛和面试题里出场率其实不低,只是很多题目穿上了别的外衣。比如"求一个数的平方根"可以二分,"求两个有序数组的第 k 小距离对的距离"可以二分答案,"给定一个函数,求它和某个常数的交点"可以二分。这种"二分答案"的思路和"二分查找"完全不同,它是把"最优解问题"转化成"判定可行性问题":给你一个候选答案 mid,问你它能不能满足条件。如果满足,答案继续往更好的方向收;不满足,就换方向。

AcWing 790 就是最简单的二分答案模型:候选答案 mid 是"三次方根",可行性判定是"mid^3 是否大于 n"。一旦你掌握了这个模型,后面遇到"让最大值最小"或"让最小值最大"的题(比如经典的二分答案题 POJ 3258 River Hopscotch、AcWing 里 249 题"奶牛排队"),就会觉得很眼熟。

我个人在刷完这题之后,又把同一个模板应用到了计算自然对数近似值、牛顿法和二分法对比测试这些场景里,每次用都有新的体会——递归、二分、迭代三种逼近思想,浮点二分是最好上手、最不容易出错的入门方式。如果你正在学算法基础课,建议你拿这道题做一次深度笔记,把它和后面的题目横向对比,收获会比单纯 AC 一道题大得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询