[SDOI2017]硬币游戏

考虑生成函数来做

g(x)函数就是0+0*x+...+1*x^s+...+|∑|^(n-s)x^n

就是最后s位必须填这个串,但是前面随便填的方案数

然后枚举之前出现了哪个串(包括自己),如果没有相交,就是fj(x)*g(x),还有就是有前后缀有相交部分,

Pji(x)中的第k位,表示i的长度为m-k的前缀和j的长度为m-k的后缀是不是一样 0/1。

再加上最后一个

$\sum f_i=1$因为游戏最终会停止

 

现在有n+1个方程,g(x)直接当做未知数的话会有交叉项解不出来

把$(1-|\Sigma|x)$乘过去,求导来搞搞事情:

由于带入$x=1/|\Sigma|$使得这个$(1-|\Sigma|x)$等于0,求导可以消去一些项

$-|\Sigma|fi=S(x)^{S-1}-Sx^{S-1}(\sum_jf_j)-x^S(\sum_jf'_j)+|\Sigma|(\sum_jf_jP_{ji})$

把$(\sum_jf'_j)$当做另外一个未知量

就有n+1个未知量,n+1个方程了

 

 根据“洛必达法则”(一种极限处理)由于最后是收敛的,一定有极限,所以带入当x=1/|∑|的时候不会除以0成为很大的数(我在口胡)

 

代码:
卡精度,不用eps反而更好

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define reg register int
#define il inline
#define ld long double 
#define numb (ch^'0')
using namespace std;
typedef long long ll;
il void rd(int &x){
    char ch;x=0;bool fl=false;
    while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true);
    for(x=numb;isdigit(ch=getchar());x=x*10+numb);
    (fl==true)&&(x=-x);
}
namespace Miracle{
const int N=303;
const double eps=1e-8;
const ll mod[2]={1000000007,998244353};
const ll jin[2]={131,13331};
ll pw[N][2];
ll has[N][N][2];
char s[N];
ld f[N][N];
ld ans[N];
int n,m;
ld m0,m1;
bool cmp(ld x){
    return 1;
}
void Guass(int n){
    for(reg i=1;i<=n;++i){
        int id=0;
        for(reg j=i;j<=n;++j){
            if(cmp(f[j][i])&&fabs(f[j][i])>fabs(f[id][i])) id=j;
        }
        if(id!=i){
            for(reg j=1;j<=n+1;++j){
                swap(f[i][j],f[id][j]);
            }
        }
        for(reg j=i+1;j<=n;++j){
            if(cmp(f[j][i])){
                ld tmp=f[j][i]/f[i][i];
                for(reg k=1;k<=n+1;++k){
                    f[j][k]=f[j][k]-f[i][k]*tmp;
                }
            }
        }
    }
    for(reg i=n;i>=1;--i){
        for(reg j=1;j<=n;++j){
            if(cmp(ans[j])) f[i][n+1]-=ans[j]*f[i][j];
        }
        ans[i]=f[i][n+1]/f[i][i];
    }
}
int main(){
    rd(n);rd(m);
    pw[0][0]=pw[0][1]=1;
    for(reg i=1;i<=m;++i){
        for(reg j=0;j<=1;++j){
            pw[i][j]=pw[i-1][j]*jin[j]%mod[j];
        }
    }
    for(reg i=1;i<=n;++i){
        scanf("%s",s+1);
        
        for(reg j=1;j<=m;++j){
            for(reg l=0;l<=1;++l){
                has[i][j][l]=(has[i][j-1][l]*jin[l]+s[j]-'A')%mod[l];
            }
        }
    }
    m0=m1=1.0;
    for(reg i=1;i<=m;++i) m1=m1*0.5;
    m0=m1*2;
    
    for(reg i=1;i<=n;++i){
    //    cout<<" turns "<<i<<" ----------------------- "<<endl;
        for(reg j=1;j<=n;++j){
            
        //    cout<<" with "<<j<<endl;
            ld tmp=0.5;
            ld val=0;
            for(reg k=1;k<=m-1;++k){
                int to=m-k;
                ll bac0=(has[j][m][0]-has[j][m-to][0]*pw[to][0]%mod[0]+mod[0])%mod[0];
                ll bac1=(has[j][m][1]-has[j][m-to][1]*pw[to][1]%mod[1]+mod[1])%mod[1];
                
                if(bac0==has[i][to][0]&&bac1==has[i][to][1]){
                //    cout<<" ok "<<to<<endl;
                    val+=tmp;
                }
                tmp*=0.5;
            }
        //    cout<<" val "<<val<<endl;
            f[i][j]=m*m0-2.0*val;
        }
        f[i][i]-=2.0;
        f[i][n+1]=m1;
        f[i][n+2]=m*m0;
    }
    
    for(reg i=1;i<=n;++i){
        f[n+1][i]=1.0;
    }
    f[n+1][n+2]=1.0;
    
//    for(reg i=1;i<=n+1;++i){
//        for(reg j=1;j<=n+2;++j){
//            cout<<f[i][j]<<" ";
//        }cout<<endl;
//    }
    Guass(n+1);
    
    for(reg i=1;i<=n;++i){
        printf("%.10Lf\n",ans[i]);
    }
    return 0;
}

}
signed main(){
    Miracle::main();
    return 0;
}

/*
   Author: *Miracle*
   Date: 2019/2/18 17:41:21
*/

 总结:

对于连续一些位置有值的时候,
生成函数确实很好用

求导由于可以降次,这里就把交叉项成功分开了。

 

PS:这个题也有直接上概率的做法,本质和生成函数相同。但是解释起来最好的绝对是生成函数

posted @ 2019-02-18 21:07  *Miracle*  阅读(243)  评论(0编辑  收藏  举报