【考试总结】2022-01-07


猜拳游戏

按照猎人杀的思想,带平局的情况改成一直打到分出胜负为止,此时胜利概率为 \(\frac{win}{win+lose}\)

如果能求出最大的 \(\frac{win}{win+lose}\) 问题可以考虑使用 \(\rm DP\) 求胜率,即设 \(f_i\) 表示胜负场数为 \(i\) 时的胜率

方程为 \(f_{i}=f_{i+1}win+f_{i-1}lose\),初始化 \(f_{m1}=1,f_{-m2}=0\),使用高斯消元解决即可

诶写到这里发现我昨天读错题了,题目含义是一方比另一方多赢 \(m1/m2\) 局,我读成连赢了

遗留问题是求解 \(\max\frac{win}{win+lose}\),这等价于求最大的 \(k\) 满足 \(win=k \times lose\),考虑使用 01分数规划

设现在需要判断 $ win= k\times lose$ 是不是合法,那么胜利得 \(1\) 分,失败扣 \(k\) 分,如果最终第一个人的得分 \(\ge 0\) 就说明胜利概率大于等于失败概率的 \(k\)

\(f_{i,j}\) 表示进行第 \(i\) 轮之前双方胜负差距为 \(j\) 场时的最大积分数,倒序 \(\rm DP\),枚举出石头剪刀还是布得到转移,初始化 \(f_{n+1,0}=0,f_{n+1,[x>0]}=1,f_{n+1,[x<0]}=-k\)

Code Display
const int N=1010;
double a[N][N],res[N],per[N][3];
int m1,m2,n;
inline double Guass(double win){
    double lose=1-win;
    memset(a,0,sizeof(a));
    int n=m1+m2-1;
    a[1][1]=1; a[1][2]=-win;
    for(int i=-m2+2;i<=m1-2;++i){
        a[i+m2][i+m2]=1;
        a[i+m2][i+m2-1]=-lose;
        a[i+m2][i+m2+1]=-win;
    }
    a[n][n]=1; a[n][n-1]=-lose; a[n][n+1]=win;
    for(int i=1;i<=n;++i){
        if(a[i][i]==0){
            for(int j=i+1;j<=n;++j) if(fabs(a[j][i])>1e-9) swap(a[i],a[j]); 
        }
        if(a[i][i]<1e-9) continue;
        for(int j=i+1;j<=n;++j) if(a[j][i]){
            double tmp=a[j][i]/a[i][i];
            for(int k=n+1;k>=i;--k) a[j][k]-=a[i][k]*tmp;
        }
    }
    res[n]=a[n][n+1]/a[n][n];
    for(int i=n-1;i>=1;--i){
        for(int j=i+1;j<=n;++j) a[i][n+1]-=a[i][j]*res[j];
        res[i]=a[i][n+1]/a[i][i];
    }
    return res[m2];
}
const int U=1003;
double dp[1010][2010];
inline bool check(double cst){
    rep(i,1,n+1) rep(j,-(n+1),n+1) dp[i+1][j+U]=0;
    for(int i=-(n+1);i<=n+1;++i){
        if(i>0) dp[n+1][i+U]=1;
        if(i==0) dp[n+1][i+U]=0;
        if(i<0) dp[n+1][i+U]=-cst;
    }
    for(int i=n;i;--i){
        for(int j=-i;j<=i;++j){
            double vv=-1e18;
            for(int cho=0;cho<=2;++cho){
                double val=dp[i+1][j+U]*per[i][cho]+dp[i+1][j+1+U]*per[i][(cho+2)%3]+dp[i+1][j-1+U]*per[i][(cho+1)%3];
                ckmax(vv,val);
            }
            dp[i][U+j]=vv;
        }
    }
    return dp[1][U]>=0;
}
signed main(){
    while(1){
        n=read(); m1=read(); m2=read();
        if(n+m1+m2==0) exit(0);
        for(int i=1;i<=n;++i){
            for(int j=0;j<=2;++j) per[i][j]=read()*1.0/100;
        }
        double l=0,r=1e18;
        while(r-l>1e-9){
            double mid=(l+r)/2;
            if(check(mid)) l=mid;
            else r=mid;
        }
        printf("%.5lf\n",Guass(l/(l+1)));
    }
    return 0;
}

小 H 爱染色

\[Ans=\sum_{i=0}^{n-1}H(n-i)\sum_{j=0}^m f_ji^j \]

其中 \(H(len)\) 表示给一个长度为 \(len\) 的序列染色两次,每次选 \(m\) 个位置染黑,第一个位置必须染色

考察后面将后面的 \(i^j\) 使用第二类斯特林数展开:

\[\begin{aligned}&\sum_{i=0}^{n-1}H(n-i)\sum_{j=0}^m f_j\sum_{k=0}^jk!\binom ik \begin{Bmatrix}j\\ k\end{Bmatrix}\\ &=\sum_{i=0}^{n-1} H(n-i)\sum_{j=0}^m f_j\sum_{k=0}^m\binom ik \sum_{l=0}^k(-1)^l\binom k l(k-l)^j\\ &=\sum_{i=0}^{n-1} H(n-i)\sum_{k=0}^m\binom ik \sum_{l=0}^k(-1)^l\binom k l\sum_{j=0}^mf_j (k-l)^j\\ &=\sum_{i=0}^{n-1} H(n-i)\sum_{k=0}^m\binom ik \sum_{l=0}^k(-1)^l\binom k lF(k-l) \end{aligned}\]

如果你是代数推导大师就可以将 \(H(n-i)\) 使用“枚举多少个小球被染黑”的方法表示一下,之后使用你强大的组合恒等式背诵功底得到和使用组合意义一样的式子了!

组合意义是直接枚举总共选择了几个小球涂黑,枚举后面选择了几个小球涂色,这样子后面小球的第一个就是对应的 \(i+1\) 而其也被涂黑了,表达式如下:

\[Ans=\sum_{k=0}^m\sum_{t=m}^{2m} \binom {n}{t+k}\binom{t}{m}\binom{m}{2m-t}\sum_{l=0}^k(-1)^l\binom k lF(k-l) \]

有手 \(\rm NTT\) 就做完了

Code Display
const int N=4.2e6+10;
int fac[N],ifac[N],inv[N],comb[N],f[N],n,m,F[N],G[N];
inline int C(int n,int m){return n>1]>>1|((i&1)?(lim>>1):0);
        if(i>1; W[0]=1; W[1]=ksm(3,(mod-1)/p);
        if(opt==-1) W[1]=ksm(W[1],mod-2);
        for(int j=2;j=1;--i) ifac[i-1]=mul(ifac[i],i),inv[i]=mul(ifac[i],fac[i-1]);
    n=read(); m=read();
    comb[0]=1;
    for(int i=1;i<=3*m;++i){
        comb[i]=mul(inv[i],mul(n-i+1,comb[i-1]));
    }
    for(int i=0;i<=m;++i){
        f[i]=read();
        F[i]=mul(ifac[i],f[i]);
        G[i]=(i&1)?mod-ifac[i]:ifac[i];
    }
    int lim=1; while(lim<=(m+1)*2) lim<<=1;
    int ans=0;
    NTT(F,lim,1); NTT(G,lim,1); 
    rep(i,0,lim-1) ckmul(F[i],G[i]);
    NTT(F,lim,-1);
    rep(i,0,lim-1){
        if(i>m) F[i]=0; else ckmul(F[i],fac[i]);
        G[i]=0;
    }
    rep(k,m,2*m){
        G[k]=mul(C(k,m),C(m,2*m-k));
    }
    while(lim<=(m+1)*3) lim<<=1;
    NTT(F,lim,1); NTT(G,lim,1);
    rep(i,0,lim-1) ckmul(F[i],G[i]);
    NTT(F,lim,-1);
    for(int s=m;s<=3*m;++s) ckadd(ans,mul(F[s],comb[s]));
    print(ans);
    return 0;
}

B君的回忆

嵌套内层的计算本质上是数列给在外层模数意义下的循环节取模,回溯跑矩阵快速幂

求解循环节本质上是将转移矩阵快速幂若干次之后得到在 \(\mod p\) 意义下的单位矩阵,可以使用 \(\rm BSGS\) 来求

不难发现根据 乘法 的 原理,一个合数的循环节是其质因子循环节的 \(\rm lcm\),可以据此降低 \(BSGS\) 的根号上界来加速

我阅读了一份写 里面写了如下结论:

  • \(5\) 存在质数意义下的二次剩余那么 \(cyc|p-1\)

  • \(5\) 不存在模意义下二次剩余那么 \(cyc|(2p+2)\)

  • 模质数的次幂 \(p^k\) 的循环节满足 \(cyc|cyc(p)p^{k-1}\)

结论是任意合数 \(p\),循环节上界是 \(6p\)

这些推导过程貌似对广义斐波那契数列都是适用的,这题如果设 \(g(0)=0,g(1)=1\) 甚至发现这第 \(n\) 项是斐波那契数列第 \(2n\) 项,大概可以认为循环节是 \(3p\) 左右

Code Display
map cyc;
struct mat{
    int a[2][2];
    bool operator ==(const mat &b)const{
        return a[0][0]==b.a[0][0]&&a[1][1]==b.a[1][1]&&a[1][0]==b.a[1][0]&&a[0][1]==b.a[0][1];
    }
    mat(){} mat(int A,int b,int c,int d){
        a[0][0]=A; a[0][1]=b; a[1][0]=c; a[1][1]=d;
        return ;
    }
#define ull unsigned long long 
    inline unsigned long long sign(){
        ull res=0,bas=13331;
        rep(i,0,1) rep(j,0,1) res+=a[i][j]*bas,bas=bas*13331;
        return res;
    }
    inline void init(){memset(a,0,sizeof(a));}
}unit;
int n,a,b,k,p;
inline void Mul(mat &a,mat &b,int mod){
    int c[2][2]={};
    rep(i,0,1) rep(k,0,1) if(a.a[i][k]) rep(j,0,1) c[i][j]+=a.a[i][k]*b.a[k][j];
    rep(i,0,1) rep(j,0,1) a.a[i][j]=c[i][j]%mod;
    return ;
}
#define E 19260817
struct hashmap{
    struct edge{int val,nxt; mat to;}e[200000];
    int head[E],ecnt;
    vector hs;
    inline void clear(){
        ecnt=0;
        for(auto t:hs) head[t]=0;
        hs.clear();
        return ;
    }
    inline void insert(mat now,int x){
        ull p;
        e[++ecnt]={x,head[p=now.sign()%E],now};
        if(!head[p]) hs.pb(p);
        head[p]=ecnt;
        return ;
    }
    inline int query(mat now){
        for(int i=head[now.sign()%E];i;i=e[i].nxt){
            if(e[i].to==now) return e[i].val;
        } return -1;
    }
}hs;
inline int Get(int n,int p){
    assert(n>=0);
    mat bas=mat(0,p-1,1,3);
    int vec[2]={a,b};
    while(n){
        if(n&1){
            int tmp[2]={};
            rep(i,0,1) rep(j,0,1) tmp[j]+=vec[i]*bas.a[i][j];
            rep(i,0,1) vec[i]=tmp[i]%p,assert(vec[i]>=0);
        }
        Mul(bas,bas,p);
        n>>=1;
    }
    return vec[0];
}
inline int find(int x){
    if(x==1) return 1;
    if(cyc.count(x)) return cyc[x];
    int up=x*4,sqr=sqrt(up); 
    hs.clear();
    mat bas=mat(0,x-1,1,3),now=unit;
    hs.insert(unit,0);
    for(int i=0;i0);
    }
    if(p>1){
        int gg=find(p);
        if(~res) res=res/gcd(res,gg)*gg;
        else res=gg;
    }
    return res;
}
inline int F(int n,int k,int p){
    if(k==1) return Get(n,p);
    return Get(F(n,k-1,Div(p)),p);
}
signed main(){
    unit=mat(1,0,0,1);
    int T=read(); while(T--){
        a=read(); b=read(); n=read(); k=read(); p=read();
        print(F(n,k,p));
    }
    return 0;
}