自适应 Simpson 积分法学习笔记

自适应 Simpson 积分法,是一种计算一段区间内,形态奇怪的函数和的算法,例如面积并和难以直接用通项公式计算的函数。

Simpson 积分

我们都知道,求解微积分需要求解一个导数的原函数,但这显然更难,比如说 \(f(x)=\dfrac{cx+d}{ax+b}\) 这个函数,就相当难以求解原函数 \(F(x)\) (对应的例题是这个)。

假如我们在求定积分,我们考虑使用一个与 \(f(x)\) 函数图像相近的函数 \(f_n(x)=\sum\limits_{i=0}^na_ix^i\)。显然,\(F_n(x)\) 比较好求,为 \(\alpha+\sum\limits_{i=0}^na'_ix^{i+1}\)。那么我们就得到:

\[\int_a^bf(x)dx\cong\int_a^bf_n(x)dx=F_n(b)-F_n(a) \]

为了方便运算,我们只考虑 \(n=2\) 的情况。相当于此时 \(f_2(x)\) 是一个二次函数。众所周知,要确定一个二次函数,需要确定其上的三个点。我们考虑选择 \(a,b,c=\dfrac{a+b}2\) 三点。我们设 \(f_2(x)=Ax^2+Bx+C\),则 \(F_2=\dfrac A3x^3+\dfrac B2x^2+Cx+\alpha\)。于是有:

\[\int_a^bf(x)dx\cong\int_a^bf_2(x)dx=F_2(b)-F_2(a) \]

\[=\dfrac A3(b^3-a^3)+\dfrac B2(b^2-a^2)+C(b-a) \]

\[=\frac{b-a}6(2A(b^2+ab+a^2)+3B(b+a)+6C) \]

\[=\frac{b-a}6((Aa^2+Ba+C)+(Ab^2+Bb+C)+(A(a+b)^2+2B(a+b)+4C)) \]

\[=\frac{b-a}6(f(a)+f(b)+4(A(c)^2+B(c)+C)) \]

\[=\frac{b-a}6(f(a)+f(b)+4f(c)) \]

于是我们就得到了 Simpson 积分的一个公式:

\[\int_a^bf(x)dx\cong\frac{b-a}6(f(a)+f(b)+4f(c)) \]

自适应 Simpson 积分法

上面这个式子很不错,但是不够好,因为他太粗略了。

比如 \(\int_{-1}^1\frac{3x+4}{x+2}dx\),用 Simpson 积分算出来的值为 \(3.77\cdots\),而实际值已经超过 \(3.8\) 了。这就错的相当离谱。

其根本原因就在于,我们选择的 \(f_2(x)\) 不像 \(f(x)\)。但是我们还有一招:

\[\int_a^bf(x)dx=\int_a^cf(x)dx+\int_c^bf(x)dx \]

为了方便,我们设 \(c=\frac{a+b}2\)。于是想到将原区间分成大量小区间,再用 Simpson 积分求出每个小区间的值,最后再全部合并起来。

比如上述问题,我们将问题转化为:

\[\int_{-1}^0\frac{3x+4}{x+2}dx+\int_0^1\frac{3x+4}{x+2}dx\cong 3.8 \]

假如肯再分解一层,就会得到 \(3.802549\cdots\),已经相当接近正确答案 \(3.802775\cdots\) 了。

但是显然,继续再分解下去一定会造成 \(TLE\),所以当务之急是确定递归的边界条件。

想法也很简单,假如满足:

\[|\int_a^bf_{a,b,2}(x)-(\int_a^cf_{a,c,2}(x)+\int_c^bf_{c,b,2}(x))|\le \alpha\times eps \]

那么就停止递归,否则继续递归,同时 \(eps=\frac{eps}2\)。这里 \(\alpha\) 是一个常量,基本上可以说卡常专用了。

#include<bits/stdc++.h>
#define fsc(x) fixed<<setprecision(x)
using namespace std;
double a,b,c,d,L,R;
double f(double x){
    return (c*x+d)/(a*x+b);
}double simpson(double l,double r){
    double mid=(l+r)/2;
    return (f(l)+f(mid)*4+f(r))*(r-l)/6;
}double dfs(double l,double r,double as,double eps){
    double mid=(l+r)/2;
    double a0=simpson(l,mid),a1=simpson(mid,r);
    if(fabs(a0+a1-as)<=eps*15)
        return a0+a1+(a0+a1-as)/15;
    return dfs(l,mid,a0,eps/2)+dfs(mid,r,a1,eps/2);
}int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>a>>b>>c>>d>>L>>R;
    cout<<fsc(6)<<dfs(L,R,simpson(L,R),1e-6);
    return 0;
}

面积并

众所周知,有一道《计算几何水题》,他叫月下柠檬树。这里将以这道题讲解面积并问题如何转化为 Simpson 积分。


计算几何部分,考虑圆的投影还是一个等大的圆,只需要确定圆心就可以了。

圆台有两种情况。假如一个大圆完全包含另一个小圆,那么圆台的投影就是大圆;否则两圆必有两条公切线,连接后形成的图形即为圆台投影。

转化为公切线两端点,向各自圆心连线,加上公切线和两圆心连线所围成的图形(实为直角梯形)与两圆的面积并。

圆形部分好求,用相似三角形和勾股定理可以简单求出直角梯形的四个交点。

为减少时间复杂度,我们将圆形转为半圆,同时每个圆台只保留一个梯形,最终答案 \(\times 2\) 即可。


注:我求面积并的方法比较非主流,不喜勿喷。

那我们考虑设 \(f(x_0)\) 表示对于每个图形,和 \(x=x_0\) 的两个交点连成的线段的线段并。假如没有两个交点,那么我们就不管它。

有一个基本的思路是,面积并等于大量线段并之和,所以我们这样定义。

将梯形转化为更好求解的三角形,再进行求解即可。

#include<bits/stdc++.h>
#include<bits/extc++.h>
#define fsc(x) fixed<<setprecision(x)
#define SZ 5056577
using namespace std;
const int N=1505;
int n,m,k;double rd[N],o[N];
double al,sc[N][3][2],L,R;
struct hash_map{
    struct data{double v;int u,nex;};
    data e[SZ<<1];int h[SZ],cnt;
    double& operator[](int u){
        int hu=u%SZ;
        for(int i=h[hu];i;i=e[i].nex) if(e[i].u==u) return e[i].v;
        return e[++cnt]=(data){0.0,u,h[hu]},h[hu]=cnt,e[cnt].v;
    }
}mp;struct line{double l,r;}ans[N];
int cmp(line x,line y){return x.l<y.l;}
double dts(double *a,double *b,double x){
    return a[1]+(a[1]-b[1])/(a[0]-b[0])*(x-a[0]);
}double f(double x){
    if(mp[(int)(x*500)]) return mp[(int)(x*500)];
    for(int i=1;i<=n;i++){
        if(x<o[i]-rd[i]||x>o[i]+rd[i]) continue;
        ans[++k].l=0;
        ans[k].r=sqrt(rd[i]*rd[i]-pow(x-o[i],2));
    }for(int i=1;i<=m;i++){
        if(x<sc[i][0][0]||x>sc[i][2][0]) continue;
        ans[++k].l=dts(sc[i][0],sc[i][2],x);
        ans[k].r=dts(sc[i][1],sc[i][2*(sc[i][1][0]<x)],x);
        if(ans[k].l>ans[k].r) swap(ans[k].l,ans[k].r);
    }sort(ans+1,ans+k+1,cmp);double re=0,lst=-1e9;
    for(int i=1;i<=k;lst=max(lst,ans[i++].r))
        re+=max(0.0,ans[i].r-max(lst,ans[i].l));
    return k=0,mp[(int)(x*500)]=re*2;
}double simpson(double l,double r){
    return (f(l)+4*f((l+r)/2)+f(r))*(r-l)/6;
}double dfs(double l,double r,double as,double eps){
    double mid=(l+r)/2;
    double a0=simpson(l,mid),a1=simpson(mid,r);
    if(fabs(a0+a1-as)<=eps*15) return a0+a1+(a0+a1-as)/15;
    return dfs(l,mid,a0,eps/2)+dfs(mid,r,a1,eps/2);
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
    cin>>n>>al,L=1e9,R=-1e9;
    for(int i=1;i<=n+1;i++) cin>>o[i],o[i]+=o[i-1];
    for(int i=1;i<=n;i++){
        o[i]/=tan(al),cin>>rd[i];
        L=min(o[i]-rd[i],L);
        R=max(o[i]+rd[i],R);
    }o[n+1]/=tan(al);
    L=min(o[n+1],L),R=max(o[n+1],R);
    for(int i=1;i<=n;i++){
        if(o[i]+rd[i]>=o[i+1]+rd[i+1]) continue;
        if(o[i]-rd[i]>=o[i+1]-rd[i+1]) continue;
        sc[++m][1][0]=o[i+1],sc[m+1][0][0]=sc[m][0][0]=o[i];
        sc[m+1][2][0]=sc[m][2][0]=o[i+1]+(rd[i]-rd[i+1])*rd[i+1]/(o[i+1]-o[i]);
        sc[m+1][2][1]=sc[m][2][1]=sqrt(rd[i+1]*rd[i+1]-pow(sc[m][2][0]-o[i+1],2));
        sc[++m][1][0]=o[i]+(rd[i]-rd[i+1])*rd[i]/(o[i+1]-o[i]);
        sc[m][1][1]=sqrt(rd[i]*rd[i]-pow(sc[m][1][0]-o[i],2));
    }for(int i=1;i<=m;i++){
        if(sc[i][0][0]>sc[i][1][0])
            swap(sc[i][0],sc[i][1]);
        if(sc[i][1][0]>sc[i][2][0])
            swap(sc[i][1],sc[i][2]);
        if(sc[i][0][0]>sc[i][1][0])
            swap(sc[i][0],sc[i][1]);
    }cout<<fsc(2)<<dfs(L,R,simpson(L,R),2e-3);
	return 0;
}
posted @   长安一片月_22  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2024-02-03 [ZJOI2015]幻想乡战略游戏 题解
2024-02-03 $CDQ$ 分治总结
点击右上角即可分享
微信分享提示