Boost库中Math 模块的根搜索 / 根求解和示例

概览

boost::math::tools 提供两大类根求解工具:

  1. 无需导数(derivative-free)的方法

    • bisect(二分,简单且健壮)
    • bracket_and_solve_root + toms748_solve(先扩区间再用 TOMS 748 算法求解,效率高)
    • 还有方便函数用于"已知单调性并有初值猜测"的情况。
  2. 有导数的方法(基于导数的迭代法)

    • newton_raphson_iterate(牛顿/牛顿-拉弗森,二阶收敛)
    • halley_iterateschroder_iterate(三阶方法)
    • 以及 complex_newton 等。此类要求开发者能同时计算一阶(或二阶)导数。

另外,Boost 的这些工具支持停止准则策略(tolerance functors)Policies(控制最大迭代、错误行为、抛异常或返回值等) ,以及调试宏 BOOST_MATH_INSTRUMENT 用于打印迭代信息。


重要概念

  • 带括号(bracketed) vs 无括号(unbracketed)

    • 带括号方法(例如 bisecttoms748_solve)需要使用者提供一个区间 [a,b] 且有根(或 f(a)*f(b) <= 0)。优点:健壮、收敛有保证(在连续函数上)。
    • 无括号方法(例如牛顿)只需要初始猜测,但对初值敏感、可能发散或落入错误根,适合能提供导数且初值较好时使用。
  • TOMS 748 :在无需导数的算法里是很高效的选择(混合插值法,针对平滑函数接近最优)。Boost 提供 toms748_solvebracket_and_solve_root 的封装。

  • 停止准则 :Boost 使用"tolerance 函数对象"来决定达到多少精度停止。常用的是 eps_tolerance<T>(digits)(按二进制位数)或自定义 lambda(如 abs(r - l) < tol)。也可以使用最大迭代次数的 Policy。

  • 错误处理 :当前置条件不满足(如 f(a)*f(b)>0),Boost 默认会抛 boost::math::evaluation_error,但可通过 Policy 改变行为(例如不抛而返回区间端点)。


常用函数签名与使用说明

下列示例基于 Boost 文档与官方示例;可直接在支持 C++17 的环境编译(需链接 Boost)并运行。


1) bisect(二分) --- 最稳健的通用方法

适用场景 :若有连续函数 f,并且已知区间 [a,b] 满足 f(a)*f(b) <= 0(即区间包含根或端点为根)。

基本用法(示例):

cpp 复制代码
#include <boost/math/tools/roots.hpp>
#include <iostream>
#include <cmath>

int main(){
    using boost::math::tools::bisect;
    auto f = [](double x){ return x*x - 2.0; };
    // 停止准则:区间长度小于 1e-12
    auto tol = [](double l, double r){ return std::abs(r - l) < 1e-12; };
    auto result = bisect(f, 0.0, 2.0, tol);
    double root = (result.first + result.second) / 2.0;
    std::cout << "root ~ " << root << "\n";
}

要点bisect 返回 std::pair<T,T>(保守地返回最终左右界)。如果前提 f(a)*f(b) <= 0 不满足,会触发错误(可通过 Policy 改变行为)。


2) bracket_and_solve_root + toms748_solve --- 自动扩区间并用 TOMS748 高效收敛

适用场景 :若只有一个初始猜测 x0(或对根数量级有估计),函数光滑且想比二分更快。bracket_and_solve_root 会先尝试从猜测扩展区间直到包含根,然后用 toms748_solve 求解。

用法示例

cpp 复制代码
#include <boost/math/tools/roots.hpp>
#include <iostream>
#include <cmath>

int main(){
    using boost::math::tools::toms748_solve;
    using boost::math::tools::bracket_and_solve_root;
    auto f = [](double x){ return x*x - 2.0; };

    // 容差按二进制有效位(例如 double 的 max_digits)
    auto tol = boost::math::tools::eps_tolerance<double>(30); // 30 bits
    std::size_t max_iter = 100;

    // bracket_and_solve_root 会在内部用 toms748_solve 求根
    auto result = bracket_and_solve_root(f, 1e-6, 1.0, true, tol, max_iter);
    // 返回 pair<lower, upper>
    double root = 0.5 * (result.first + result.second);
    std::cout << "root ~ " << root << "\n";
}

bracket_and_solve_root 有多种重载(可传 Policy、是否假设单调性等),并在面对根所在数量级与猜测相差极大时会有退避机制,但扩区间可能需要很多次函数评估。TOMS748 在平滑场景下效率很好。


3) newton_raphson_iterate / halley_iterate(基于导数的迭代)

适用场景 :若可以准确计算 f(x)f(x)f(x) 及其导数(或二阶导数),且能给出较好起始猜测。优点:收敛速度快(牛顿二阶,Halley 三阶);缺点:可能发散或收敛到错误根,尤其在函数平缓或导数近 0 时。

要求 :传入的 callable 必须返回 tuple / pair / boost::tuple

  • 对于 newton_raphson_iterate:返回 (f(x), f'(x))
  • 对于 halley_iterate:返回 (f(x), f'(x), f''(x))。([boost.org][2])

示例(牛顿法)

cpp 复制代码
#include <boost/math/tools/roots.hpp>
#include <tuple>
#include <iostream>

int main(){
    using boost::math::tools::newton_raphson_iterate;
    // f(x) = x^2 - 2 ; f' = 2x
    auto f_and_df = [](double x)->std::pair<double,double>{
        return {x*x - 2.0, 2.0*x};
    };

    double guess = 1.0;
    double min = 0.0, max = 2.0;
    const int digits = std::numeric_limits<double>::digits; // binary digits
    // newton_raphson_iterate returns the root estimate and uses template args to set accuracy
    double root = newton_raphson_iterate(f_and_df, guess, min, max, digits);
    std::cout << "root ~ " << root << "\n";
}

注意newton_raphson_iterate / halley_iterate 的模板参数和返回细节可参考 Boost 文档。若迭代遇到问题,通常要限制步长、加入信赖域或退回到带括号方法。

停止准则(tolerance)与 Policy 的细节

  • eps_tolerance<T>(bits):按二进制位精度停止(常用于要求"得到二进制位数的精度")。
  • 自定义 lambda :例如 [](T l, T r){ return std::abs(r - l) < tol; },适合你想按绝对距离停止。
  • Policies :Boost.Math 有 policies::policy<>,可设置 max_root_iterations 等(通过 policies::get_max_root_iterations<Policy>() 获取)。还能控制错误是否抛出异常或返回特定值。

数值与实现注意(实践要点与陷阱)

  1. 一定要保证/检查括号条件 :对带括号算法,先检查 f(a)*f(b) <= 0。若不满足,考虑先用 bracket_and_solve_root 自动扩区间或用物理先验缩小初始区间。Boost 的 bisect 会在前条件不满足时报错(或按 Policy 行为处理)。

  2. 单调性与多根toms748_solve 对平滑单根或单调段效果极好;如果区间内有多个根或函数不光滑,结果可能不可预期;务必先分段或检测多根。

  3. 导数方法风险 :牛顿 / Halley 对初值敏感;若 f'(x) 接近 0,会产生大步长并可能发散。常见对策:限制步长、使用信赖域或在不稳定时退回到二分/Bracketing。

  4. 精度 vs 迭代次数 :请求非常高精度(如接近机器极限)会显著增加函数评估次数;合理设置 digitsmax_iter。可以通过 policies::get_max_root_iterations<Policy>() 管控迭代上限。

  5. 调试 :定义 #define BOOST_MATH_INSTRUMENT(在包含 roots.hpp 前)可打印迭代轨迹,帮助诊断为何迭代失败或为何收敛到错误根(注意输出可能非常庞大)。

  6. 数值病态 :在指数级差距(root 在 1e-300,但猜测在 1e-1)情况下,自动扩区间耗费巨大;如果知道尺度,先对变量做对数变换或缩放会更稳妥。bracket_and_solve_root 有部分鲁棒性,但也不是万能。


错误与已知问题(from upstream)

  • Boost issue tracker 中有关于 newton_raphson_iterate 在某些极端函数下表现不当的报告(在某些版本里可能出现问题)。使用前建议查阅所用 Boost 版本的文档与 issue(若遇到异常行为,尝试换版本或退回 bracket 方法)。

实战建议

  • 当可提供可靠括号时 :优先 toms748_solve(或 bracket_and_solve_root + toms748),兼顾速度与鲁棒性。若无法括号但能估计根量级,先 bracket_and_solve_root

  • 当能给出良好初值并能计算导数时 :优先 newton_raphson_iteratehalley_iterate(更快),但务必设置边界或回退策略(如每次迭代检查是否越界或步长过大,必要时切换到 bracket 方法)。

  • 极端鲁棒/保守场景 :使用 bisect(简单、可预测,但速度最慢)。


完整示例:一个"智能"求根封装(示意)

下面是一个示意性的 C++ 函数模板,演示如何结合 bracket_and_solve_root(TOMS748)与 newton_raphson_iterate 做"先快速尝试导数法,失败则退回到 bracket"策略。

cpp 复制代码
#include <boost/math/tools/roots.hpp>
#include <iostream>
#include <tuple>
#include <functional>
#include <limits>

template<class Real>
Real robust_solve(std::function<std::pair<Real,Real>(Real)> f_df, // returns {f, f'}
                  std::function<Real(Real)> f_only,
                  Real guess, Real lo, Real hi)
{
    using boost::math::tools::newton_raphson_iterate;
    using boost::math::tools::bracket_and_solve_root;
    using boost::math::tools::eps_tolerance;

    // try Newton first (fast) but safe-guard with bracket bounds
    try{
        const int digits = std::numeric_limits<Real>::digits;
        Real root = newton_raphson_iterate(
            [&](Real x){ return f_df(x); },
            guess, lo, hi, digits);
        return root;
    } catch(...){
        // fallback to bracket + TOMS 748
        auto tol = eps_tolerance<Real>(std::numeric_limits<Real>::digits - 3);
        std::size_t max_iter = 200;
        auto sol = bracket_and_solve_root(f_only, guess, 1, tol, max_iter);
        return (sol.first + sol.second) / 2;
    }
}

相关推荐
我搞slam1 小时前
EM Planner算法与代码解读
算法
CodeWizard~2 小时前
线性筛法求解欧拉函数以及欧拉反演
算法
45288655上山打老虎2 小时前
右值引用和移动语义
算法
liulilittle2 小时前
C++ 并发双阶段队列设计原理与实现
linux·开发语言·c++·windows·算法·线程·并发
白狐_7982 小时前
【项目实战】我用一个 HTML 文件写了一个“CET-6 单词斩”
前端·算法·html
Jasmine_llq2 小时前
《P3811 【模板】模意义下的乘法逆元》
数据结构·算法·线性求逆元算法·递推求模逆元
Jacob程序员2 小时前
欧几里得距离算法-相似度
开发语言·python·算法
ffcf3 小时前
消息中间件6:Redis副本数变为0和删除PVC的区别
算法·贪心算法
CoderYanger3 小时前
动态规划算法-斐波那契数列模型:2.三步问题
开发语言·算法·leetcode·面试·职场和发展·动态规划·1024程序员节