利用根间距扫描整数端点与开单位区间,在变号区间内二分逼近三个互异实根。
OJ: luogu
题目 ID: P1024
难度:普及/提高-
标签:二分数学浮点数python
日期: 2026-07-16 17:49
目录
题意
给出三次方程
题目保证它在
思路
为什么可以按整数区间扫描
根之间至少相距
这里必须注意:不能说闭区间
- 整数根在它作为区间左端点时记录;
- 非整数根在开区间
内通过函数值变号找到; - 扫描只让
取 到 ,因此最后单独检查右端点 。
三次方程有三个互异实根,设为
每个根都只出现一次,是单根。
因此,如果非整数根
这样扫描所有整数端点,就能隔离出全部非整数根。
在变号区间内二分
找到满足
- 若
,根在 ,令 ; - 否则根在
,令 。
每次二分都保留一个含根区间,并把区间长度缩小一半。初始区间长度为 double / float 会先到达机器精度,但这仍远高于保留两位小数所需的精度。
实现细节
多项式使用霍纳法计算:
它只需要三次乘法,写法也比直接计算
判断整数端点时不能直接使用固定的绝对 EPS:把方程所有系数同时乘一个很小的数不会改变根,却会让所有函数值一起变小。代码先用各项绝对值之和估计当前求值规模:
再判断
判断区间是否变号之前,左右端点都要做这次判零。若右端点已经近似为整数根,就不能因为它残留的微小正负号而对当前区间执行二分;程序跳过这个区间,等下一轮让该整数成为左端点时再记录。这样每个整数根只记录一次。输出前把绝对值小于 0.0005 的结果改成 0.0,避免由逼近误差得到 -0.00。
正确性说明
- 对任意整数根,
到 会在它作为左端点时记录它,根 由最后一次检查记录;右端点判零会阻止它被前一个区间的二分提前重复记录,因此整数根不会遗漏或重复。 - 对任意非整数根
,它唯一属于某个 。互异根都是单根,所以 与 异号,扫描一定会找到这个区间。 - 二分过程中区间两端始终异号或其中一端为零,因此区间始终包含该根;区间长度不断减半,最终得到足够精确的近似值。
- 扫描方向从左到右,整数根和区间内根也都按所在位置加入,所以输出顺序天然递增。
综上,算法会按顺序找到且只找到三个实根。
brute.py 使用 60 位 Decimal 和 180 次二分作为高精度参考程序。它用于随机对拍,不是另一种更快的提交算法,因此正文不重复嵌入。
Python 知识
固定次数的浮点二分
Python 的 float 与 C++ 的 double 一样,通常是 IEEE 754 双精度浮点数。数值二分可以直接执行固定次数,不必用 while right - left > eps 反复判断终止条件;固定次数也更容易估算误差。
半开 range 与端点处理
range(-100, 100) 依次产生 [integer, integer + 1]。由于右端点 100 不会成为下一轮的左端点,代码在循环后单独检查它。
格式化与展开输出
f"{root:.2f}" 把一个根格式化为两位小数字符串。表达式
print(*(f"{root:.2f}" for root in roots))先按需产生三个字符串,再用 * 展开为 print 的参数,默认以空格分隔。
C++ 到 Python 对照
- C++ 的
fabs(x)对应 Python 的abs(x)。 - C++ 的
vector<double>对应 Python 的列表。 - C++ 的
fixed << setprecision(2)对应 Python 的f"{value:.2f}"。 - 两种语言都使用同一个霍纳法函数和相同的二分区间更新规则。
模仿清单
- 用
range(left, right)扫描整数左端点,循环后单独处理最终右端点。 - 浮点二分执行固定次数,并始终维护“答案仍在区间中”的不变量。
- 用霍纳法计算多项式,减少乘法和中间量。
- 固定小数位输出前处理接近零的负数,避免
-0.00。
相关 Python 笔记:
/home/rainboy/mycode/hugo-blog/content/program_language/python/math_tools.md:浮点误差、相对/绝对误差与Fraction/ 高精度参考程序的使用边界。/home/rainboy/mycode/hugo-blog/content/program_language/python/oj_input_output_cheatsheet.md:浮点格式化、数组展开和常见 OJ 输入输出写法。/home/rainboy/mycode/hugo-blog/content/program_language/python/generator_expression.md:生成器表达式的惰性产生与一次性消费。
代码
C++17 正解
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 09:46
* update_at: 2026-07-19 10:29
*/
#include <bits/stdc++.h>
using namespace std;
const double EPS = 1e-10;
double coefficient_a, coefficient_b, coefficient_c, coefficient_d;
double polynomial(double x) {
return ((coefficient_a * x + coefficient_b) * x + coefficient_c) * x + coefficient_d;
}
bool is_zero_at(double x, double value) {
double absolute_x = fabs(x);
double scale = ((fabs(coefficient_a) * absolute_x + fabs(coefficient_b)) * absolute_x
+ fabs(coefficient_c)) * absolute_x + fabs(coefficient_d);
return fabs(value) <= EPS * scale;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> coefficient_a >> coefficient_b >> coefficient_c >> coefficient_d;
vector<double> roots;
for (int integer = -100; integer < 100; integer++) {
double left = integer;
double right = integer + 1;
double left_value = polynomial(left);
double right_value = polynomial(right);
bool left_is_root = is_zero_at(left, left_value);
bool right_is_root = is_zero_at(right, right_value);
// 整数根只在它作为左端点时记录;右端点根留到下一轮。
if (left_is_root) {
roots.push_back(left);
} else if (!right_is_root && left_value * right_value < 0) {
for (int iteration = 1; iteration <= 80; iteration++) {
double middle = (left + right) / 2;
double middle_value = polynomial(middle);
if (left_value * middle_value <= 0) {
right = middle;
} else {
left = middle;
left_value = middle_value;
}
}
roots.push_back((left + right) / 2);
}
}
double right_endpoint_value = polynomial(100.0);
if (is_zero_at(100.0, right_endpoint_value)) {
roots.push_back(100.0);
}
cout << fixed << setprecision(2);
for (int i = 0; i < 3; i++) {
double root = roots[i];
if (fabs(root) < 0.0005) root = 0.0;
if (i > 0) cout << ' ';
cout << root;
}
cout << '\n';
return 0;
}Python 正解
a, b, c, d = map(float, input().split())
EPS = 1e-10
def polynomial(x):
return ((a * x + b) * x + c) * x + d
def is_zero_at(x, value):
absolute_x = abs(x)
scale = ((abs(a) * absolute_x + abs(b)) * absolute_x + abs(c)) * absolute_x + abs(d)
return abs(value) <= EPS * scale
roots = []
for integer in range(-100, 100):
left, right = float(integer), float(integer + 1)
left_value, right_value = polynomial(left), polynomial(right)
left_is_root = is_zero_at(left, left_value)
right_is_root = is_zero_at(right, right_value)
if left_is_root:
roots.append(left)
elif not right_is_root and left_value * right_value < 0:
for _ in range(80):
middle = (left + right) / 2
middle_value = polynomial(middle)
if left_value * middle_value <= 0:
right = middle
else:
left = middle
left_value = middle_value
roots.append((left + right) / 2)
right_endpoint_value = polynomial(100.0)
if is_zero_at(100.0, right_endpoint_value):
roots.append(100.0)
roots = [0.0 if abs(root) < 0.0005 else root for root in roots]
print(*(f"{root:.2f}" for root in roots))复杂度
设扫描范围内有
- 时间复杂度为
,在本题固定范围下就是 。 - 保存三个根,空间复杂度为
。
总结
本题的关键不是盲目缩小步长,而是先利用“根间距至少为
实现时还要完整处理整数根、右端点 -0.00。这些边界决定了程序能否稳定找到恰好三个根。