记录115
cpp
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-4;//浮点数会有一定的误差,epsilon浮点数比较时允许的误差范围(精度容差)
double a,b,c,d;
double f(double x){
return a*x*x*x+b*x*x+c*x+d;
}
int main(){
cin>>a>>b>>c>>d;
for(int i=-100;i<=100;i++){//fabs()函数,浮点数的绝对值
double L=i,R=i+1,mid;//数<1e-4 时候,当它就近似是0了
if(fabs(f(L))<eps) cout<<fixed<<setprecision(2)<<L<<" ";//左端点是根输出
else if(fabs(f(R))<eps) continue;//右端点是根跳过,因为在下一轮循环,它是左端点(去重)
else if(f(L)*f(R)<0){//如果两点之间存在根
while(R-L>eps){//两个界限没有重合
mid=(L+R)/2;//二分答案来找
if(f(mid)*f(R)>0) R=mid;//向右边靠拢找
else L=mid;//向坐标左边靠拢
}
cout<<fixed<<setprecision(2)<<L<<" ";//输出
}
}
return 0;//结束程序
}
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
题目传送门
https://www.luogu.com.cn/problem/P1024
突破口
有形如:ax3+bx2+cx+d=0 这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在 −100 至 100 之间),且根与根之差的绝对值 ≥1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后 2 位。
提示:记方程 f(x)=0,若存在 2 个数 x1 和 x2,且 x1<x2,f(x1)×f(x2)<0,则在 (x1,x2) 之间一定有一个根。
🔍 一、题目核心要求
🎯 问题本质
给定一个一元三次方程:
f(x)=ax3+bx2+cx+d=0f(x)=ax3+bx2+cx+d=0
已知:
- 存在三个不同的实根
- 所有根 ∈ [−100, 100]
- 任意两根之差的绝对值 ≥ 1(关键!)
要求:
- 从小到大输出三个实根
- 保留两位小数
✅ 这是一个数值求根问题 ,不能用公式法(卡丹公式复杂且易出错),需用数值逼近方法
🧠 二、核心思路:利用"根隔离" + "二分法"
关键提示解析
若 x1<x2x1<x2 且 f(x1)⋅f(x2)<0f(x1)⋅f(x2)<0 ,则 (x1,x2)(x1,x2) 内必有一实根。
这是介值定理的直接应用(函数连续 ⇒ 符号变化 ⇒ 必有零点)。
利用"根间距 ≥1"的性质
- 因为任意两根距离 ≥1,所以在区间长度为 1 的小区间
[i, i+1]中:- 最多包含一个根
- 不会出现多个根挤在一个小区间导致漏检
💡 这是本题能用"步长为1扫描"的理论基础!
算法策略
- 遍历所有整数区间 :
i从 −100 到 99,考察区间[i, i+1] - 对每个区间:
- 若
f(i) ≈ 0→i是根,直接输出 - 若
f(i+1) ≈ 0→ 跳过(下一轮i+1会作为左端点处理,避免重复) - 若
f(i) * f(i+1) < 0→ 区间内有唯一根,用二分法逼近
- 若
- 由于总共只有 3 个根,循环中恰好会找到 3 次,按
i递增顺序输出即为从小到大
代码分析
cpp
#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-4; // 浮点误差容忍度
eps = 1e-4:用于判断"是否接近0"- 为什么不是
1e-7?因为最终只需 2 位小数精度 ,1e-4足够保证四舍五入正确
cpp
double a, b, c, d;
double f(double x) {
return a*x*x*x + b*x*x + c*x + d;
}
- 定义多项式函数
f(x),避免重复写表达式
cpp
int main() {
cin >> a >> b >> c >> d;
- 读入系数(均为实数)
cpp
for(int i = -100; i <= 100; i++) {
double L = i, R = i + 1, mid;
- 遍历每个单位区间
[i, i+1] - 注意:
i从 −100 到 100 (含),但R = i+1最大为 101 - 实际有效区间是
[−100, 100],而根在 [−100,100] 内,所以覆盖完整
情况 1:左端点是根
cpp
if(fabs(f(L)) < eps)
cout << fixed << setprecision(2) << L << " ";
fabs(f(L)) < eps:判断f(L) ≈ 0- 直接输出
L(即i),保留两位小数 - 使用
fixed和setprecision(2)确保格式
情况 2:右端点是根(跳过,防重复)
cpp
else if(fabs(f(R)) < eps)
continue; // 下一轮 i=i+1 时,R 成为新的 L,会被输出
- 避免同一个根被输出两次
- 例如根为 2.0,则在
i=1时R=2是根,跳过;i=2时L=2是根,输出
✅ 巧妙去重!
情况 3:区间内有根(符号变化)
cpp
else if(f(L) * f(R) < 0) {
f(L)和f(R)异号 → 中间有根
cpp
while(R - L > eps) {
mid = (L + R) / 2;
if(f(mid) * f(R) > 0)
R = mid; // f(mid) 与 f(R) 同号 → 根在左半
else
L = mid; // f(mid) 与 f(R) 异号 → 根在右半
}
🔍 二分法细节:
- 终止条件 :
R - L ≤ eps(区间足够小) - 更新策略 :
- 若
f(mid)与f(R)同号 → 说明mid和R在根的同一侧 → 根在[L, mid]→R = mid - 否则 → 根在
[mid, R]→L = mid
- 若
💡 也可以用
f(L)*f(mid) < 0判断左半,等价。
cpp
cout << fixed << setprecision(2) << L << " ";
}
}
return 0;
}
- 输出近似根
L(此时L ≈ R ≈ 真实根) - 由于
i从小到大遍历,输出自然有序
⚠️ 关键设计与易错点
| 问题 | 说明 |
|---|---|
| eps 选择 | 1e-4 足够保证两位小数正确(误差 < 0.005) |
| 去重机制 | 右端点根跳过,左端点输出,避免重复 |
| 根的顺序 | 因 i 递增扫描,找到的根自然从小到大 |
| 浮点比较 | 永远不要用 == 比较浮点数,必须用 fabs(x) < eps |
| 区间覆盖 | i 到 100,确保 [99,100] 被检查,根在 [−100,100] 内全覆盖 |
为何不用牛顿迭代或其他方法?
- 牛顿法:需要导数,且可能不收敛(对初值敏感)
- 公式法:三次方程有解析解,但涉及复数运算和三角函数,NOIP 环境下易出错
- 本题特性 (根隔离 + 间距 ≥1)使得简单二分扫描 成为最优解:
- 稳定、可靠、易实现
- 时间复杂度:200 个区间 × log₂((1)/1e-4) ≈ 200 × 14 = 2800 次函数求值,极快