10 KiB
10 KiB
算法: 浮点数二分 讲义
1. 引言
-
问题引入:我们都知道 √2 大约是 1.414,但这个结果是怎么算出来的?计算器按一个键就能得到答案,但它内部到底发生了什么?我们无法通过有限次的加减乘除直接得到这个无限不循环小数。这背后其实就用到了我们今天要学的思想——不断缩小范围,无限逼近。
-
价值阐述:浮点数二分是解决连续函数求根、实数域最优解等问题的一大利器。它在科学与工程计算中扮演着至关重要的角色:
- 方程求解:求解
sin(x) + x = 5这种无法直接解出的超越方程的根。 - 物理模拟:计算物体达到特定速度或位置所需的时间。
- 几何计算:在复杂图形中寻找满足特定几何属性(如面积、周长)的参数。 它让我们能够用一种统一、简洁且高效的编程模型,来应对各种复杂的数学和工程问题。
- 方程求解:求解
-
学习目标:
- 理解浮点数二分与整数二分的本质区别。
- 掌握基于精度控制的循环条件和边界更新方式。
- 学会使用“枚举+二分”的分治策略解决复杂的多根求解问题。
- 能够独立编写代码求解一元三次方程的根。
2. 核心思想
-
思路铺垫:手动计算 √2 回到 √2 的问题。我们不求精确命中,只求去逼近它。
- 确定范围:我们知道 1²=1 (太小了),2²=4 (太大了)。所以,√2 一定在
[1.0, 2.0]这个区间里。 - 第一次猜测:取区间中点
1.5。计算1.5² = 2.25。嗯,还是太大了。说明 √2 肯定比1.5小。于是,答案范围被我们缩小到了[1.0, 1.5]。 - 第二次猜测:取新区间的中点
1.25。计算1.25² = 1.5625。这次又太小了。说明 √2 比1.25大。答案范围进一步缩小到[1.25, 1.5]。 - 继续...:我们不断重复“取中点 -> 判断 -> 缩小范围”这个过程,区间的长度会以指数级速度减小,
left和right将会无限逼近那个真正的 √2。
- 确定范围:我们知道 1²=1 (太小了),2²=4 (太大了)。所以,√2 一定在
-
浮点数二分与整数二分的三个核心区别
-
目标不同
- 整数二分:像是在一排有编号的格子里找东西,目标是精确到某个格子。
- 浮点数二分:像是在一条连续的线段上找一个点,由于浮点数无限可分,我们几乎不可能精确命中。我们的目标是无限逼近,直到我们的左右边界把这个点“夹”得足够紧,紧到我们无法分辨它们的区别为止。
-
循环条件不同:
while (r - l > eps)vs.while (l <= r)- 在整数世界,
l和r最终会“撞上”或“越过”对方,循环自然停止。 - 在浮点数世界,
l和r可能永远不会完全相等。因此,我们引入一个极小值eps(Epsilon,精度),比如1e-8。当区间的长度r - l已经比这个eps还要小的时候,我们就认为l和r已经“贴”在一起了,找到了答案。
- 在整数世界,
-
边界更新不同:
l = midvs.l = mid + 1- 整数二分中,如果
mid不是答案,那下一个可能的答案至少是mid + 1,我们可以放心地跳一格。 - 浮点数二分中,如果
mid不是答案,真正的答案可能就藏在mid和mid + 0.000...01之间。我们绝不能跳过任何一点点空间!因此,新的边界必须是mid本身,即l = mid或r = mid。
- 整数二分中,如果
-
-
浮点数二分模板
double l, r; // 答案所在的初始范围 const double eps = 1e-8; // 精度,题目要求k位小数,eps可设为1e-(k+2) while (r - l > eps) { // 当区间长度还不够小时,继续二分 double mid = l + (r - l) / 2; // 取中点 if (check(mid)) { // check函数判断mid是否满足某种偏大的条件 r = mid; // 答案在左半边,或者mid就是答案 } else { l = mid; // 答案在右半边,或者mid就是答案 } } // 循环结束时,l和r已经非常接近,输出任意一个均可 cout << l << endl;
3. 实例
-
具体案例:求解方程
ax³+bx²+cx+d=0(B0193 / 书本例13-5) 问题分析: 这是一个一元三次方程。直接求解非常复杂。但题目给了我们几个关键的信息:- 根的范围已知:
[-100, 100]。这直接给了我们一个初始的搜索大区间。 - 根的间距 ≥ 1:它意味着,如果我们把
[-100, 100]这条长长的数轴,切成 200 个长度为 1 的小区间[-100, -99],[-99, -98], ...,[99, 100],那么每一个小区间里,最多只可能有一个根。 - 零点存在性定理:
f(x₁)*f(x₂) < 0意味着f(x₁)和f(x₂)的函数值一个为正、一个为负。由于函数曲线是连续的,它必然会在这两者之间穿过x轴一次,也就是说,(x₁, x₂)之间必有一个根。
- 根的范围已知:
-
算法策略:分治法 (枚举粗筛 + 二分精搜) 我们不能直接在
[-100, 100]上二分,因为有三个根,天知道二分会逼近哪一个。正确的做法是:第一步:枚举粗筛 我们用一个
for循环,以 1 为步长,从 -100 扫描到 99。在每一步,我们检查[i, i+1]这个小区间。- 如果
f(i)和f(i+1)的符号不同 (即f(i) * f(i+1) < 0),我们就锁定了一个藏有根的区间!
第二步:二分精搜 一旦我们找到了一个区间,比如
[i, i+1],我们就知道这里面有且仅有一个根。现在问题变得简单了:在一个小区间内找一个唯一的根。我们立刻在这个小区间上套用浮点数二分模板,把这个根揪出来。 - 如果
-
流程示例
- 粗筛:
for循环检查[-3,-2],[-2,-1],[-1,0]... - 发现
f(-2)是正的,f(-1)是负的。f(-2)*f(-1) < 0!锁定区间[-2, -1]。 - 精搜:在
[-2, -1]内启动浮点二分,最终逼近根r1。 for循环继续,又发现f(0)*f(1) < 0,锁定[0, 1],二分出r2。for循环继续,发现f(2)*f(3) < 0,锁定[2, 3],二分出r3。
- 粗筛:
4. 代码实现
-
完整代码
#include <bits/stdc++.h> using namespace std; // 全局变量存储方程系数 double a, b, c, d; // 定义方程函数 f(x) double f(double x) { return a * x * x * x + b * x * x + c * x + d; } int main() { cin >> a >> b >> c >> d; // 设置输出格式为保留两位小数 cout << fixed << setprecision(2); // 第一步:枚举粗筛 for (int i = -100; i < 100; ++i) { double x1 = i; double x2 = i + 1; double y1 = f(x1); double y2 = f(x2); if (y1 == 0) { // 如果左端点恰好是根,直接输出 cout << x1 << " "; } else if (y1 * y2 < 0) { // 如果区间内有根,启动第二步:二分精搜 double l = x1, r = x2; const double eps = 1e-4; // 题目要求2位小数,1e-4的精度足够 while (r - l > eps) { double mid = l + (r - l) / 2; // 判断根在左半边还是右半边 // 如果 f(mid) 和 f(l) 同号,说明根在[mid, r] // 否则根在 [l, mid] if (f(mid) * f(l) > 0) { l = mid; } else { r = mid; } } cout << l << " "; } } return 0; } -
代码解读:
for (int i = -100; i < 100; ++i): 实现了从 -100 到 99 的步长为 1 的扫描。if (f(mid) * f(l) > 0): 这是判断根在哪一半区间的核心。如果f(mid)和f(l)同号(乘积为正),说明[l, mid]这个区间两端点函数值同号,根不在这里,所以根在另一半[mid, r],因此更新l = mid。反之r = mid。
5. 验证与深化
- 常见问题
- 为什么
eps要设为1e-(k+2)? 这是一种经验法则。二分法在最后几次迭代时,精度提升会变慢。为了确保第k位小数的正确性,我们需要计算到比它更精确几位的程度,再进行四舍五入。多算两位通常是比较保险的选择。 - 二分判断条件
if (f(mid) * f(l) > 0)可以换成if (f(mid) * f(r) > 0)吗? 可以,但逻辑要相应改变。如果f(mid)和f(r)同号,说明根在[l, mid],应更新r = mid。只要保证逻辑自洽即可。 - 如果题目没有“根的间距>=1”这个条件怎么办?
问题会变得非常复杂。一个单位区间内可能有多个根,
f(i)*f(i+1) < 0这个条件就可能失效(比如有两个根,穿过去又穿回来,导致两端点同号)。这时需要借助微积分中的导数来判断函数的单调区间,在每个单调区间内再进行二分。
- 为什么
6. 总结
- 要点回顾:
- 浮点数二分的核心是逼近,而非查找。
- 循环条件由精度
eps控制:while (r - l > eps)。 - 边界更新不加减1:
l = mid或r = mid。 - 面对复杂问题,分治思想是关键。通过枚举粗筛将大问题分解为多个简单的、适合二分的小问题。
- 思考:
我们今天所有
check的依据都是函数值的正负号。二分思想的应用远不止于此。任何一个参数,只要它能导致结果呈现出某种“单调性”(越大越如何,或越小越如何),我们都可以对这个参数进行二分,在整数和浮点数里是完全通用的。