\(\rm{0x01}\) 前言

首先阐明一点,自适应辛普森算法(\(\rm{Adaptive ~Simpson’s~ rule}\) )是一类近似算法(\(\rm{Approximation ~algorithm}\)),主要用于求较难反导的函数的积分。大概在信息计算的时候中很常用?

其思想是利用二次函数来不断拟合(\(\rm{Overfitting}\))所求曲线,而所谓的\(Adapative\)(自适应)则是用于优化时间复杂度的方法。

嗝…总之…比较简单?

表示看了两篇外国学者的论文,感觉好像思路已经比较清晰的样子。

\(\rm{0x02}\) \(\mathcal{Simpson~Formula}\) 辛普森公式

稍等,这个跟算法的关系不大,主要是公式:$$\rm{\int _{l}^{r} f(x) \rm{dx}\approx \frac{f(r) +f(l)+ 4 \cdot f(m)}{6}} ,$$

事实上吧,求积分的话,大多数都是直接套辛普森公式的。并且这个公式是广泛适用的……只不过误差有点儿人大\(233\)

这其实是我们用二次函数不断拟合的结果,推导过程大致如下\(^{[1]}\):

因为 \(g(x)\) 是用来拟合 \(f(x)\) 的,所以有:

\(\int\limits_L^Rf(x)dx\approx \int\limits_L^Rax^2+bx+c\space dx\)

求 \(g(x)\) 的不定积分为:

\(\int g(x)dx=\frac13ax^3+\frac12bx^2+cx+C\)

然后再带入 RR 和 LL :

\(\int\limits_L^Rf(x)dx=\frac13a(R^3-L^3)+\frac12b(R^2-L^2)+c(R-L)\)

然后提公因式,原式为:

\(\frac{R-L}{6}(2a(R^2+LR+L^2)+3b(R+L)+6c)6R−L(2a(R2+LR+L2)+3b(R+L)+6c)\)

把里面展开来:

\(\frac{R-L}{6}(2aR^2+2aLR+2aL^2+3bR+3bL+6c)\)

重新整理一下式子:

\(\frac{R-L}{6}((aR^2+bR+c)+(aL^2+bL+c)+aR^2+aL^2+2aLR+2bR+2bL+4c)\)

再整理:

\(\frac{R-L}{6}((aR^2+bR+c)+(aL^2+bL+c)+4(a(\frac{R+L}{2})^2+b(\frac{R+L}{2})+c))\)

代换可得:

\(aR^2+bR+c\approx f(R)\)

\(aL^2+bL+c\approx f(L)\)

\(4(a(\frac{R+L}{2})^2+b(\frac{R+L}{2})+c)\approx 4f(\frac{R+L}{2})\)

把这三个式子带回去, 最后我们就得到了\(\rm{Simpson}\) 积分公式:

\(\int\limits_L^Rf(x)dx \approx \frac{R-L}{6}(f(L)+4f(\frac{L+R}{2})+f(R)))\)

于是我们就得到了所谓的\(\rm{Simpson~Fomula}\)。但事实上,对于一段“跌宕起伏”的函数,我们还是无法准确地去用一个二次函数的拟合来刻画。于是——

\(\rm{0x03}\) \(\mathcal{Adaptive~Simpson~Rule}\) 自适应辛普森法

我们考虑,如果把区间们稍微划分地更细一点,那是不是会好呢?答案是一定的。那么我们可以考虑定向二分。但是……定向二分始终存在一个问题,就是它太笨了,笨到如果\([l_1,r_1]\)已经满足精度要求了,我们却还要一直分;笨到\([l_2,r_2]\)分的次数根本不够用——但我们并不可以得到反馈。

于是考虑自适应

所谓自适应,说的直白点,无非就是需要多分治几次的地方,多分治几次;不需要的则可以少分治几次

你会发现,其实他节约的就是一个点——时间效率

举个栗子\(^{[2]}\):

比如有这么个函数\(\rm{f (x) = 13(x − x^2)e^{\frac{−3x}{2}} }\),我们要求\(\int_{0}^{4}f(x) \rm{~dx}\) 并要求精度误差在\(1e-5\) 以内。而我们有两种方法去解决:

  • 以固定的比例以及约束二分。
  • 运用自适应策略分配

那么我们首先要知道他真正的\(value:\)

看起来好像海星?然后我们用两种方法都试一试:

首先是自适应法,我们发现最后只需要求\(20\)段区间。表中的\(a_k \& b_k\)表示左右区间,\(S(l,r)\)表示\([l,r]\)内、运用\(0x01\)中的公式计算的,\(\rm{Error~Bound}\)表示误差界,\(\rm{Tolerance}\)表示计算时需要的误差(程序设计时会讲)。

那么最后算出来的值是$ −1.54878823413$ ,与真实值误差为$0.00000013840 $,一共调用了\(79\)次函数估值(留个坑,后文会讲)。

那么绘制出来的函数图像大概长这样:

好像很流畅?\(233\)

那么第二种方法是定值分段。我们考虑分成区间\([0,4]\)分为长度为\(0.03125\)的\(128\)段,并运用\(0x01\)的\(Formula\),最后得出的结果为\(−1.54878844029\),误差为\(0.00000006776\)……

好像是第一个误差的二分之一?听起来好像误差小了,但是却需要\(257\)次函数估值的调用……相比之下,我们可以获得更优越的性能,而那误差也是不需要考虑的啦的啦~

但是比起\(1e-5\)精度来说……这波稳赚啊\(233\)

\(\rm{0x04}\) \(\mathcal{About~Code~}\) 代码实现

首先是\(LuoguP4525\)的暴力解法:

#include <cmath>
#include <cstdio>
#include <iostream> #define mid (l + r) / 2 using namespace std ;
const double Eps = 5e-8 ;
double Ans, A, B, C, D, L, R ; inline double F(double x){
return (C * x + D) / (A * x + B) ;
}
inline double Simp_calc(double l, double r){
return (r - l) / 6 * (F(l) + F(r) + 4 * F(mid)) ;
}
inline double do_divide(double l, double r){
double Lv, Rv, v, t ;
Lv = Simp_calc(l, mid), Rv = Simp_calc(mid, r), v = Simp_calc(l, r) ;
if (fabs(t = (Lv + Rv - v)) <= Eps) return Lv + Rv ; return do_divide(l, mid) + do_divide(mid, r) ;
}
int main(){
scanf("%lf%lf%lf%lf%lf%lf", &A, &B, &C, &D, &L, &R) ;
Ans = do_divide(L, R) ; printf("%.6lf", Ans) ; return 0 ;
}

这…严格意义上讲,不能算是自适应辛普森法——更准确地说,四不像,或者“东施效颦”之类的,都挺合适。这是我在初学这一块儿内容时的写法,他不严格正确,但是……他对的很?

至于进化版\(LuoguP4526\),也是完全可以violently艹过去的:

#include <cmath>
#include <cstdio>
#include <iostream> #define mid (l + r) / 2 using namespace std ;
const double Eps = 5e-8 ;
double Ans, A, B, C, D, L, R ; inline double F(double x){
return pow(x, A / x - x) ;
}
inline double Simp_calc(double l, double r){
return (r - l) / 6 * (F(l) + F(r) + 4 * F(mid)) ;
}
inline double do_divide(double l, double r, double Lans){
double Lv, Rv, v, t ; Lv = Simp_calc(l, mid), Rv = Simp_calc(mid, r), v = Lans ;
if (fabs(t = (Lv + Rv - v)) <= Eps) return Lv + Rv ; return do_divide(l, mid, Lv) + do_divide(mid, r, Rv) ;
} int main(){
scanf("%lf", &A) ; L = Eps, R = 23.3 ;
if (A < 0) { puts("orz"), cout << endl ; return 0 ;}
Ans = do_divide(L, R, Simp_calc(L, R)) ; printf("%.5lf", Ans) ; return 0 ;
}

但是,其真正的实现应该是这样:

#include <cmath>
#include <cstdio>
#include <iostream> #define v Lans
#define hps eps * 0.5
#define mid (l + r) * 0.5 using namespace std ;
double Ans, A, L, R ;
const double Eps = 1e-7 ;
const double Liu = 1.0 / 6 ; inline double F(double x){
return pow(x, A / x - x) ;
}
inline double Simp_calc(double l, double r){
return (r - l) * Liu * (F(l) + F(r) + 4.0 * F(mid)) ;
}
inline double do_divide(double l, double r, double Lans, double eps){
double Lv, Rv, t ;
Lv = Simp_calc(l, mid), Rv = Simp_calc(mid, r) ;
if (fabs(t = (Lv + Rv - v)) <= eps * 15) return Lv + Rv + t / 15 ;
return do_divide(l, mid, Lv, hps) + do_divide(mid, r, Rv, hps) ; //据说eps×15来自于Wiki……
}
int main(){
scanf("%lf", &A) ; L = Eps, R = 30 ;
if (A < 0) { puts("orz"), cout << endl ; return 0 ;}
Ans = do_divide(L, R, Simp_calc(L, R), Eps) ; printf("%.5lf", Ans) ; return 0 ;
}

然后我们发现这就可以跟文章上面\(0x03\)中的例子呼应了:每次分治时计算两次,总共分治了\(39\)次,最终一共计算了\(78+1=79\)次,而二分则是一棵有\(128\)个叶子节点的递归树,总共计算了\(256 +1=257\)次。

好的,终于要扯正题了。算法的实现其实简单,我们用拟合性算法不断\(check\&calc\), 而\(check\)的方式也很简单,只需要判断一下两段子区间的函数值之和与整个区间的函数值之和的差值是否在精度要求范围之内,之后如果满足精度误差就直接\(return\),否则对于这段区间继续递归下去。

而这个地方有个要求,就是对于\(eps\),你需要不断\(half\)他,原因很简单,对于一整段区间\(U\),要求他的返回值的\(|eps(U)| \leq k\)的话,那么对于其作为子集两个连续区间\(A,B\),当\(A \bigcup B = U\)时,必须要满足\(|eps(A)| \leq \frac{k}{2}, |eps(B)| \leq \frac{k}{2}\),才能保证\(|eps(U) = eps(A) + eps(B)| \leq k\),所以要:

#define hps eps * 0.5
....................
return do_divide(l, mid, Lv, hps) + do_divide(mid, r, Rv, hps) ;

好了,唯一的问题在于有句话迷的很:

 if (fabs(t = (Lv + Rv - v)) <= eps * 15) return Lv + Rv + t / 15 ;

这个\(\leq 15 \cdot eps\)是什么意思?

好吧,笔者也不清楚,但是有个结论是长这样的:

什么?你说推什么倒?小小年纪整天想着推倒学姐可不好啊\(233\)

什么?你还想看推倒的过程?啧啧啧,左转知乎感情专区蟹蟹~

好的,以上两段是瞎扯的,推导过程十分诡异,大家自己食用好了……

于是结束,撒花花…(不是撒博主x)

\(\rm{Referance}\)

对于第二篇\(refer\),借鉴的比较多(读书人的事……咳咳……),但是改了一个数据,就是\(81 \to 79\),因为代码需要\(233\)

\(Ps:\)史上最不正经的\(reference\)诞生了……

随机推荐

  1. HDU1069(KB12-C)

    Monkey and Banana Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others ...

  2. 设计模式原则(6)--Open-Closed Principle(OCP)--开闭原则

    作者QQ:1095737364    QQ群:123300273     欢迎加入! 1.定义: 一个软件实体应当对扩展开放,对修改关闭.即软件实体应尽量在不修改原有代码的情况下进行扩展. 2.使用场 ...

  3. 护网杯 task_shoppingCart 记录

    前言 相关题目位于 https://gitee.com/hac425/blog_data/tree/master/hwb task_shoppingCart 漏洞位于 00BD9 用户输入 idx 然 ...

  4. flume使用场景 flume与kafka的比较

    Is Flume a good fit for your problem? If you need to ingest textual log data into Hadoop/HDFS then F ...

  5. LeetCode题解之Binary Number with Alternating Bits

    1.题目描述 2.问题分析 将数值转换为二进制,然后将前面的 0 去掉,再遍历一边二进制字符串,对每个字符和其后部的字符进行比较. 3.代码 bool hasAlternatingBits(int n ...

  6. scott/tiger is locked 解决办法

    在plsql developer中要是以scott/tiger登录时提示ora-28000 the account is locked. 解决办法: 新装完Oracle10g后,用scott/tige ...

  7. Sql server 使用drop database 语句,无法删除正在使用的数据库的解决办法

    使用DROP DATABASE 删除数据库  显示“无法删除数据库 ,因为该数据库当前正在使用. 解决办法:在删除某一个数据库(下例中的“DB1”数据库)前,强制kill掉该数据库上的所有数据库连接. ...

  8. MySQL crash-safe replication(1)

    MySQL 5.6 对复制功能提供了新特性:slave 支持 crash-safe,可以解决之前版本中系统异常断电可能导致的 SQL thread 信息不准确的问题. 原文:Enabling cras ...

  9. SqlServer中sqlmaint 实用工具和xp_sqlmaint扩展过程

    sqlmaint 实用工具可以对一个或多个数据库执行一组指定的维护操作.使用 sqlmaint,可以运行 DBCC 检查.备份数据库及其事务日志.更新统计以及重建索引.所有数据库维护活动都会生成报表, ...

  10. 将TXT文件 导入 sqlserver数据库

    情景一:​ 数据库已存在旧表名 old_table,列名old_column_name. 将TXT文件导入数据库已存在旧表old_table中,导入过程中需注意 数据源中列名可全部不修改 或 全部修改 ...