【洛谷 P1024 】[NOIP2001 提高组] 一元三次方程求解 - 详细分析与C++实现

题目链接https://www.luogu.com.cn/problem/P1024

一、题目简介

题目描述

给定形如 (ax^3 + bx^2 + cx + d = 0) 的一元三次方程,给出各项系数 (a,b,c,d)(均为实数)。

题目保证该方程存在三个不同实根,根的范围在 (-100,100) 之间,且任意两根之差的绝对值 (\ge 1)。

要求:按从小到大的顺序,在同一行输出这三个实根,保留两位小数,根与根之间用空格隔开。

输入格式

一行,4 个实数 (a,b,c,d)。

输出格式

一行,3 个实根,从小到大输出,精确到小数点后 2 位。

输入输出样例

输入

复制代码
1 -5 -4 20

输出

复制代码
-2.00 2.00 5.00

题目提示

记方程 (f(x)=0),若存在两个数 (x_1) 和 (x_2),且 (x_1 < x_2),(f(x_1) \times f(x_2) < 0),则在区间 ((x_1, x_2)) 之间一定有一个根。

二、解题思路详细分析

1、核心数学原理:零点存在定理

题目提示里的这句话,就是本题的核心依据:如果连续函数 (f(x)) 在区间 (l,r) 端点处的函数值异号((f(l) \times f® < 0)),说明函数图像穿过 x 轴,区间内必有一个根。

因为题目保证根与根之差的绝对值 (\ge 1),所以每一个长度为 1 的区间 (i, i+1) 内,最多只有一个根。这意味着我们可以把整个区间 (-100, 100) 划分为 200 个长度为 1 的小区间,逐个检查区间端点的函数值,就能找到所有根所在的区间。

2、解题步骤拆解

  1. 定义函数:实现 (f(x) = ax^3 + bx^2 + cx + d) 的计算。
  2. 遍历区间 :遍历 i 从 (-100) 到 99,检查区间 (i, i+1):
    • 若 (f(i) = 0),说明 i 本身就是一个根,直接输出。
    • 若 (f(i) \times f(i+1) < 0),说明区间内有根,用二分法求解。
  3. 二分法求根:在确定的区间内,用二分法不断缩小区间范围,直到区间长度足够小(小于 (10^{-4}),保证保留两位小数的精度),输出近似根。

3、为什么用二分法?

  • 三次函数是连续的,满足二分法的使用条件。
  • 题目保证每个区间内只有一个根,不会出现多根的情况,二分法可以稳定找到唯一解。
  • 二分法的精度可控,通过调整循环条件(如 (r-l > 0.0001)),可以轻松满足 "保留两位小数" 的要求。

4、时间复杂度分析

  • 区间遍历:200 次循环,时间复杂度 (O(200))。
  • 每个区间的二分法:循环次数约为 (\log_2(\frac{1}{0.0001}) \approx 14) 次。
  • 整体复杂度:(O(200 \times 14)),非常高效,不会超时。

三、AC 代码(超详细注释)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

// 方程系数,设为全局变量,方便函数调用
double a, b, c, d;

// 定义三次函数 f(x) = a*x³ + b*x² + c*x + d
double fun(double x) {
    return a * x * x * x + b * x * x + c * x + d;
}

// 二分法在区间 [l, r] 内求根
double find(double l, double r) {
    // 当区间长度小于 0.0001 时,精度足够,停止循环
    while (r - l > 0.0001) {
        double mid = (l + r) / 2; // 取区间中点
        
        // 判断中点与右端点的函数值是否异号
        if (fun(mid) * fun(r) < 0) {
            // 异号,说明根在 [mid, r] 区间内,更新左边界
            l = mid;
        } else {
            // 同号,说明根在 [l, mid] 区间内,更新右边界
            r = mid;
        }
    }
    // 返回近似根(取左边界或右边界均可,精度已足够)
    return l;
}

int main() {
    // 读取方程系数
    cin >> a >> b >> c >> d;
    
    // 遍历所有长度为1的区间 [i, i+1],i从-100到99
    for (int i = -100; i < 100; i++) {
        double y1 = fun(i);     // 区间左端点函数值
        double y2 = fun(i + 1); // 区间右端点函数值
        
        // 情况1:左端点本身就是根(f(i) = 0)
        if (y1 == 0) {
            printf("%.2lf ", 1.0 * i);
        }
        // 情况2:区间两端点函数值异号,说明区间内有根
        else if (y1 * y2 < 0) {
            // 调用二分法求解并输出,保留两位小数
            printf("%.2lf ", find(i, i + 1));
        }
    }
    
    return 0;
}

四、代码核心细节讲解

1、函数定义与全局变量

  • 把 (a,b,c,d) 设为全局变量,是为了让 fun(x) 函数能直接访问这些系数,避免传递参数的麻烦。
  • fun(x) 直接按公式计算三次函数值,注意使用 double 类型,避免整数运算的精度丢失。

2、区间遍历与根的判断

  • 遍历范围:i 从 (-100) 到 99,覆盖了题目给定的根的范围 (-100,100)。
  • 两种情况:
    • y1 == 0:说明 i 本身就是一个根,直接输出。
    • y1 * y2 < 0:说明区间内有根,用二分法求解。

3、二分法的边界处理

  • 循环条件 while(r - l > 0.0001):控制精度,当区间长度小于 (0.0001) 时,近似根保留两位小数的误差已经可以忽略。
  • 更新规则:通过 fun(mid) * fun(r) 的符号判断根所在的区间,不断缩小区间范围。

4、输出格式控制

  • 使用 printf("%.2lf", x) 格式化输出,自动四舍五入保留两位小数,满足题目要求。
  • 因为题目保证有三个不同的实根,循环结束后会自动输出三个根,且顺序由遍历顺序决定,天然从小到大排列。

五、易错点与优化

1、易错点

  • 精度问题:循环条件的精度不能太大(比如 0.01),否则输出的小数可能不符合题目要求;也不能太小(比如 1e-8),会导致循环次数过多,效率降低。
  • 根的边界问题 :如果根恰好是整数,y1 == 0 的情况必须单独处理,否则二分法会在区间内找到近似值,可能导致输出错误。
  • 变量类型 :所有涉及计算的变量都要使用 double 类型,避免整数除法导致的精度丢失。

2、优化建议

  • 可以在遍历区间时,记录已经找到的根的数量,找到 3 个后提前退出循环,减少不必要的计算。
  • 可以使用 scanf/printf 代替 cin/cout,提高输入输出效率(本题数据量小,影响不大)。

六、总结

本题是二分法在解方程中的经典应用,核心思路可以概括为:

  1. 利用题目给的 "根差≥1" 的条件,把大范围拆分成小的长度为 1 的区间。
  2. 用零点存在定理判断区间内是否有根。
  3. 用二分法在有根的区间内高精度求解。

这个思路不仅适用于一元三次方程,也可以推广到其他连续函数的求根问题非常实用的技巧。

相关推荐
Matthew_zhu_1 小时前
P3374 【模板】树状数组 1 题解
算法
随意起个昵称1 小时前
区间dp-进阶题目1(进阶合并)
c++·算法·动态规划
伶俜661 小时前
鸿蒙原生应用实战(四)ArkUI 语音变声器:录音 + 4 种音效 + 音调变换算法
算法·华为·harmonyos
王老师青少年编程1 小时前
2022年CSP-X复赛真题及题解(T2:移动棋子)
c++·真题·csp·信奥赛·复赛·csp-x·移动棋子
玖玥拾1 小时前
C/C++ 数据结构(三)链表核心算法
c语言·数据结构·c++·链表
AKA__Zas2 小时前
芝士算法(滑动窗口片 2.0)
java·算法·leetcode·学习方法
变量未定义~2 小时前
摆放小球 、dp求解组合数、求解组合数2
数据结构·算法
Sunsets_Red2 小时前
ABC462D 题解
c++·数学·编程·比赛·atcoder·信息学竞赛·信息学
喵星人工作室2 小时前
C++火影忍者1.1.8
开发语言·c++·游戏