1. 程式人生 > >[LOJ2267][SDOI2017]龍與地下城-FFT-自適應辛普森積分

[LOJ2267][SDOI2017]龍與地下城-FFT-自適應辛普森積分

龍與地下城

Description

小Q同學是一個熱愛學習的人,但是他最近沉迷於各種遊戲,龍與地下城就是其中之一。

在這個遊戲中,很多場合需要通過擲骰子來產生隨機數,並由此決定角色未來的命運,因此骰子堪稱該遊戲的標誌性道具。

骰子也分為許多種類,比如4面骰、6面骰、8面骰、12面骰、20面骰,其中20面骰用到的機會非常多。當然,現在科技發達,可以用一個隨機數生成器來取代真實的骰子,所以這裡認為骰子就是一個隨機數生成器。

在戰鬥中,骰子主要用來決定角色的攻擊是否命中,以及命中後造成的傷害值。舉個例子,假設現在已經確定能夠命中敵人,那麼YdX(也就是擲出Y個X面骰子之後所有骰子顯示的數字之和)就是對敵人的基礎傷害。在敵人沒有防禦的情況下,這個基礎傷害就是真實傷害。

眾所周知,骰子顯示每個數的概率應該是相等的,也就是說,對於一個XX面骰子,顯示0,1,2,,X1中每一個數字的概率都是1x

更形式地說,這個骰子顯示的數W滿足離散的均勻分佈,其分佈列為

除此之外還有一些性質

W的一階原點矩(期望)為v1(W)=E(W)=X1i=0iP(W=i)=X12
W的二階中心矩(方差)為μ2(W)=E((WE(W))2)=X1i=0(iE(W))2P(W=i)=X2112


言歸正傳,現在小Q同學面對著一個生命值為A的沒有防禦的敵人,能夠發動一次必中的YdX攻擊,顯然只有造成的傷害不少於敵人的生命值才能打倒敵人。但是另一方面,小Q同學作為強迫症患者,不希望出現overkill,也就是造成的傷害大於B的情況,因此只有在打倒敵人並且不發生overkill的情況下小Q同學才會認為取得了屬於他的勝利。

因為小Q同學非常謹慎,他會進行10次模擬戰,每次給出敵人的生命值A以及overkill的標準B,他想知道此時取得屬於他的勝利的概率是多少,你能幫幫他嗎?

Input

第一行是一個正整數T,表示測試資料的組數,
對於每組測試資料:
第一行是兩個整數X , Y,分別表示骰子的面數以及骰子的個數;
接下來10行,每行包含兩個整數A , B,分別表示敵人的生命值A以及overkill的標準B。

Output

對於每組測試資料,輸出10行,對每個詢問輸出一個實數,要求絕對誤差不超過0.013579,也就是說,記輸出為aa,答案為bb,若滿足|ab|0.013579,則認為輸出是正確的。

Sample Input

1
2 19
0 0
0 1
0 2
0 3
0 4
0 5
0 6
0 7
0 8
0 9

Sample Output

0.000002
0.000038
0.000364
0.002213
0.009605
0.031784
0.083534
0.179642
0.323803
0.500000

Hint

對於100%的資料,T102X201Y2000000AB(X1)Y,保證滿足Y>800的資料不超過22組。

被題目名字吸引進來了……
居然沒有找到題解……
然後就被坑了好久

思路:
很顯然可以有一個DP。
f[i][j]表示丟了i個骰子,點數為j的概率。
那麼顯然很好轉移:
f[i][j]=kj,kx1f[i1][jk]1n

然後可以發現這是個卷積,那麼FFT成點值表示式後快速冪直接上即可~

才怪。這隻有70分。
正當咱完全沒有思路的時候,知乎上此題的出題人說,這題的idea 基於“中心極限定理”。

學習一下這個東西可以發現,其實出題人想要讓咱們用正態分佈:
所謂正態分佈,就是滿足位置引數為μ、尺度引數為 σ 的概率分佈的隨機變數的分佈曲線。
其中,μ為期望,σ為方差的平方根。
具體詳見百度百科,本蒟蒻水平有限~

可以發現出題人已經在題面中將期望和方差給出了。然而並沒有什麼人知道這兩句話有用
同樣可以發現然而咱還是沒發現,這題的“擲骰子”事件滿足正態分佈。
那麼咱們可以根據正態分佈的曲線來估計答案!

不加證明的(不會證明)給出正態分佈的解析式:
f(x)=12πσexp((xμ)22σ2)
同時給出正態分佈的影象:
pic
然後現在目標轉換為,求圖上兩點間函式影象的面積。
考慮到求函式的面積,還是這麼奇怪的面積,顯然需要估算了~

聽說考場上有人用拆成小梯形的方法過了,然而明顯這樣做精度比較令人著急……
那麼考慮更靠譜一點的方法:自適應辛普森積分。

辛普森積分嘛,就是用一根迷之二次函式曲線,試圖湊出某個奇怪的函式在某一小部分內圍成的面積的方法~原理自行百度~
自適應辛普森積分就是,不停二分割槽間,如果到某一時刻,用辛普森積分對當前的(l,r)積分得到的結果f(l,r),與f(l,mid)+f(mid,r)之間的差很小,咱就認為它足夠精確了。
這演算法的原理有種自欺欺人的感覺

然而這樣積分出面積,只有80pts。
因為剛才咱們使用的是普通的正態分佈,有很多除法,不夠精確。
需要化成標準正態分佈以提高精度。

具體做法是,對於傳入的引數x,進行如下處理:
y=xμσ
y作為以前的x使用。
同時將正態分佈的公式換成標準正態分佈:
f(x)=12πσexp(x22)
這樣就少了很多除法,可過100pts!

注意,小資料由於精度誤差,仍需要使用dp(咱用的FFT)

#include<bits/stdc++.h>

using namespace std;

inline int read()
{
    int x=0;char ch=getchar();
    while(ch<'0' || '9'<ch)ch=getchar();
    while('0'<=ch && ch<='9')x=x*10+(ch^48),ch=getchar();
    return x;
}

typedef double db;

db x,y;

namespace Fast_Fouriet_Transform
{
    typedef complex<db> cp;
    const int M=800009;
    const db pi=acos(-1);

    cp a[M],b[M];
    db per,ans[M];
    int m,l,rev[M];

    inline void FFT(cp *a,int n,int f)
    {
        for(int i=0;i<n;i++)
            if(i<rev[i])
                swap(a[i],a[rev[i]]);
        for(int h=2;h<=n;h<<=1)
        {
            cp w(cos(2*pi/h),f*sin(2*pi/h));
            for(int j=0;j<n;j+=h)
            {
                cp wn(1,0),x,y;
                for(int k=j;k<j+(h>>1);k++)
                {
                    x=a[k],y=wn*a[k+(h>>1)];
                    a[k]=x+y;
                    a[k+(h>>1)]=x-y;
                    wn*=w;
                }
            }
        }
        if(f==-1)
            for(int i=0;i<n;i++)
                a[i].real()/=(db)n;
    }

    int mian()
    {
        per=1.0/(db)x;

        for(m=1,l=0;m<(y*x);m<<=1)l++;
        for(int i=0;i<m;i++)
            rev[i]=(rev[i>>1]>>1)|((i&1)<<(l-1));

        memset(a,0,sizeof(a));
        memset(b,0,sizeof(b));

        a[0].real()=1.0;
        for(int i=0;i<x;i++)
            b[i].real()=per;

        FFT(a,m,1);FFT(b,m,1);
        for(int i=0;i<m;i++)
        {
            int cnt=y;
            while(cnt)
            {
                if(cnt&1)
                    a[i]=a[i]*b[i];
                b[i]=b[i]*b[i];
                cnt>>=1;
            }
        }
        FFT(a,m,-1);

        ans[0]=a[0].real();
        for(int i=1;i<m;i++)
            ans[i]=a[i].real()+ans[i-1];

        for(int i=1;i<=10;i++)
        {
            int a=read(),b=read();
            if(a==0)
                printf("%.8lf\n",ans[b]);
            else
                printf("%.8lf\n",ans[b]-ans[a-1]);
        }
        return 0;
    }
}

namespace simpsons
{
    const db pi2=sqrt(acos(-1)*2.0);
    const db eps=1e-12;
    db sigma,mu;
    inline db sqr(db x){return x*x;}
    inline db f(db x){return exp(-sqr(x)/2)/pi2;}
    inline bool dcmp(db x){return fabs(x)<15*eps;}
    inline void init()
    {
        sigma=sqrt((sqr(x)-1.0)/12.0);
        mu=(x-1.0)/2.0;
    }

    inline db simpson(db a,db b,db fl,db fmid,db fr)
    {
        return (b-a)*(fl+4.0*fmid+fr)/6.0;
    }

    inline db solve(db l,db r)
    {
        db mid=(l+r)/2.0,lm=(l+mid)/2.0,mr=(mid+r)/2.0;
        db fl=f(l),fmid=f(mid),fr=f(r),flm=f(lm),fmr=f(mr);

        db sipl=simpson(l,mid,fl,flm,fmid);
        db sipr=simpson(mid,r,fmid,fmr,fr);
        db sipm=simpson(l,r,fl,fmid,fr);

        if(dcmp(sipl+sipr-sipm))
            return sipl+sipr+(sipl+sipr-sipm)/15;
        else return solve(l,mid)+solve(mid,r);
    }

    int mian()
    {
        init();
        db base=solve(0,(x-1)*y);
        for(int i=1;i<=10;i++)
        {
            db a=(((db)read()-0.5)/y-mu)*sqrt(y)/sigma;
            db b=(((db)read()+0.5)/y-mu)*sqrt(y)/sigma;
            printf("%.8lf\n",solve(0,b)-solve(0,a));
        }
    }
}

int main()
{
    int T=read();
    while(T--)
    {
        scanf("%lf%lf",&x,&y);