期望概率题目选讲

期望概率题目选讲

期望概率题目是 oi 中一类很特别的题型,常常与 dp、概率和期望知识、线性代数、数据结构等密不可分。在做题中,对于期望一类的题目来说,积累经验技巧就尤为关键,本文旨在通过一些典型例题的讲解来分析期望及概率类型题目的一些常用技巧,以后会不定时更新。

\(E=\frac{1}{p}\)

  • 结论:某一事件 \(A\) 每次发生的概率均为 \(p\),则期望 \(\frac{1}{p}\) 次会发生。

    证明

    \(E\) 表示发生 \(A\) 的期望次数,第一次就发生事件 \(A\) 的概率为 \(p\),第二次才发生的概率为 \(p(1-p)^1\),第三次才发生的概率为 \(p(1-p)^2\dots\) 因此就有

    \[E=p+2p(1-p)+3p(1-p)^2+\dots \]

    同除 \(p\),就有

    \[\frac{E}{p}=1+2(1-p)+3(1-p)^2+\dots\ (1) \]

    两边同乘 \(1-p\),就有

    \[\frac{1-p}{p}E=(1-p)+2(1-p)^2+3(1-p)^3+\dots\ (2) \]

    \((1)-(2)\),就有

    \[E=1+(1-p)+(1-p)^2+\dots\ (3) \]

    对于 \((3)\) 两边同乘 \(1-p\)

    \[(1-p)E=(1-p)+(1-p)^2+(1-p)^3+\dots\ (4) \]

    \((3)-(4)\),就有

    \[pE=1 \]

    因此 \(E=\frac{1}{p}\),证毕。

    注:严谨来讲上面的证明中 \((3)-(4)\) 的结论应为 \(E=\frac{1-(1-p)^n}{p}\),由于 \(n\) 趋近于无穷大,因此 \((1-p)^n\) 趋近于 \(0\),可以忽略,因此得到 \(E=\frac{1}{p}\)。为了证明更容易看懂,因此忽略了这点。

四倍经验

SP1026 FAVDICE - Favorite Dice

P1291 [SHOI2002] 百事世界杯之旅

UVA10288 优惠券 Coupons

P4550 收集邮票

  • SP1026 解析:
    本题中就用到了上面的结论。可以设当前已经掷出了 \(x\) 个面,则还有 \(n-x\) 个面未被掷出过,因此掷出一个新面的概率为 \(\frac{n-x}{n}\),期望次数为 \(\frac{n}{n-x}\),因此答案为 \(\sum_{i=0}^{n-1}\frac{n}{n-i}\),直接递推即可。

    参考代码
    #include<iostream>
    using namespace std;
    int n;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        int T;
        cin>>T;
        while(T--)
        {
            cin>>n;
            double res=0;
            for(int i=0;i<n;i++)res+=(double)n/(n-i);
            printf("%.2lf\n",res);
        }
        return 0;
    }
    
  • P4550 解析:
    同样还是上面的结论,与其他题不同的是本题中每次计算的贡献不为 \(1\),因此我们考虑在递推途中直接计算贡献。设 \(sum\) 表示当前已经计算的次数,每次统计时用 \(\frac{n}{i}\times sum\) 计算当前的贡献即可。

    参考代码
    #include<iostream>
    using namespace std;
    int n;
    double res,sum;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            double x=(double)n/i;
            sum+=x;
            res+=sum*x;
        }
        printf("%.2lf",res);
        return 0;
    }
    

[ARC150D] Removing Gacha

  • 解析:
    我们设 \(E(u)\) 表示 \(u\) 点期望被选中的次数,则答案为 \(\sum_{i=1}^nE_u\),因此转化为求 \(E_u\)
    \(d_u\) 表示点 \(u\) 到根节点的深度。我们考虑一条 \(1\to u\) 的路径,如果选到的点不在这条路径上,不会对这条路径产生贡献,因此我们只考虑选择的点在这条路径上即可。因此可以转化为有 \(d_u\) 个点,每个点选到的概率相等,求所有点都被抽到的期望次数。
    然后显然可以转化成上面的结论,路径 \(1\to u\) 上的所有点都被抽到的概率为 \(\sum_{i=0}^{d_u-1}\frac{d_u}{d_u-i}\),提出 \(d_u\) 就有 \(d_u\sum_{i=0}^{d_u-1}\frac{1}{d_u-i}=d_u\sum_{i=1}^{d_u}\frac{1}{i}\),由于每个点选到的概率相等,所以第 \(d_u\) 个点被抽到的概率为 \(\sum_{i=1}^{d_u}\frac{1}{i}\),这个东西可以预处理。
    如果在预处理中用的快速幂,时间复杂度为 \(O(n\log n)\),如果预处理阶乘和逆元,时间复杂度为 \(O(n+m)\),部分细节可以参考代码。

    参考代码
    #include<iostream>
    using namespace std;
    typedef long long ll;
    const int N=2e5+10,M=N,mod=998244353;
    int n,f[N];
    int h[N],e[M],ne[M],idx;
    int dep[N],fa[N];
    int pow(int a,int b)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    void add(int a,int b)
    {
        e[++idx]=b,ne[idx]=h[a],h[a]=idx;
    }
    void dfs(int u,int depth)
    {
        dep[u]=depth;
        for(int i=h[u];i;i=ne[i])
        {
            int j=e[i];
            dfs(j,depth+1);
        }
    }
    void init()
    {
        for(int i=1;i<=n;i++)f[i]=(f[i-1]+pow(i,mod-2))%mod;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=2,a;i<=n;i++)
        {
            cin>>a;
            add(a,i);
        }
        dfs(1,1);
        init();
        int res=0;
        for(int i=1;i<=n;i++)res=(res+f[dep[i]])%mod;
        cout<<res;
        return 0;
    }
    

推式子

OSU 专题三倍经验

P1365 WJMZBMR打osu! / Easy

CF235B Let's Play Osu!

P1654 OSU!

  • P1365 解析:
    计算 \((x+1)^2-x^2=2x+1\),因此直接维护长度 \(x\) 即可算得答案,对于 ? 的情况,将贡献到答案的和长度的部分除以二即可。

    参考代码
    #include<iostream>
    using namespace std;
    int n;
    double res,len;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        while(n--)
        {
            char ch;
            cin>>ch;
            switch(ch)
            {
                case 'o':
                    res+=2*len+1;
                    len++;
                    break;
                case 'x':
                    len=0;
                    break;
                default:
                    res+=(2*len+1.0)/2;
                    len=(len+1)/2.0;
            }
        }
        printf("%.4f\n",res);
        return 0;
    }
    
  • CF235B 解析:
    实际上和上题差不了多少,只是给定了变成 ‘o' 的概率,因此计算答案时乘上对应的 \(p\) 即可。

    参考代码
    #include<iostream>
    using namespace std;
    int n;
    double p,res,len;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>p;
            res+=(2.0*len+1)*p;
            len=(len+1)*p;
        }
        printf("%.7f",res);
        return 0;
    }
    
  • P1654 解析:
    计算 \((x+1)^3-x^3=3x^2+3x+1\),因此分别维护 \(x^2\)\(x\) 的期望,就有以下三个式子:

    \[x_i=(x_{i-1}+1)p\\ x_i^2=(x_{i-1}^2+2x_{i-1}+1)p\\ res_i=res_{i-1}+(3x_{i-1}^2+3x_{i-1}+1)p \]

    直接转移即可,如果滚动需要注意转移顺序。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1e5+10;
    int n;
    double p,x_1,x_2,res;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>p;
            res+=(3*x_2+3*x_1+1)*p;
            x_2=(x_2+2*x_1+1)*p;
            x_1=(x_1+1)*p;
        }
        printf("%.1f",res);
        return 0;
    }
    

    同时本题可以加强到 \(m\) 次方,扩展请参考对于一类期望问题的探讨——by Maxwell

P5498 [LnOI2019] 脸滚键盘

  • 解析:
    本题的套路也同样可以用到其他求子段内的所有区间积的和中,思路来源是皎月半洒花大佬,这里仅将大体做法,具体内容和正确性请参考这里

    构造数列 \(f\),使得有 \(f_i=f_{i-1}a_i+a_i\),构造数组 \(sf\) 记录 \(f\) 的前缀和,考虑查询区间 \([l,r]\) 时除去多余的部分,因此再记录一个 \(p\) 数组记录前缀积,\(sp\) 数组记录 \(p\) 数组的前缀和。然后就是式子推推推,发现区间的贡献转化为 \(sf_r-sf_{l-1}-\frac{(sp_r-sp_{l-1})f_2}{p_2}\),设 \(len=r-l+1\),则区间个数为 \(\frac{len(len-1)}{2}\),答案为二者相除。

    总之这道题难点不在期望,算是 trick 性的东西,建议直接看大佬的博客学习,因为蒟蒻不能保证一定能讲清楚,这个东西讲起来有点抽象,最好是看完大佬的博客后自己画几个三角数列出来找找规律,观察是怎么将多余的部分删去的。

    代码中好多地方都需要注意,例如模数是 \(10^8+7\),及时取模,注意负数。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1e6+10,mod=1e8+7;
    typedef long long ll;
    typedef __int128 ull;
    int n,q,a[N],f[N],sf[N],p[N]={1},sp[N];
    ll pow(int a,int b=mod-2)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    int get(ll x)
    {
        return x>mod?(x>=(mod<<1)?x%mod:x-mod):x;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>q;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            f[i]=get((f[i-1]+1ll)*a[i]);
            sf[i]=get(sf[i-1]+f[i]);
            p[i]=get((ll)p[i-1]*a[i]);
            sp[i]=get(sp[i-1]+p[i]);
        }
        while(q--)
        {
            int l,r,len;
            cin>>l>>r;
            len=r-l+1;
            cout<<get(((sf[r]-sf[l-1]+mod)-get(get((ll)f[l-1]*(sp[r]-sp[l-1]+mod))*pow(p[l-1]))+mod)*pow(get((1ll+len)*len/2)))<<'\n';
        }
        return 0;
    }
    

CF749E Inversions After Shuffle

  • 解析:
    遇到这种题,一般套路就是分别记录每个逆序对对答案的贡献。
    考虑对于一对数对 \((i,j)\),对区间 \([l,r]\) 的贡献。分两种情况讨论:

    1. 数对 \((i,j)\) 完全包含于区间 \([l,r]\) 中。不难求出总区间个数为 \(\frac{n(n+1)}{2}\),因此发生当前情况的概率为 \(\frac{i(n-j+1)}{\frac{n(n+1)}{2}}=\frac{2i(n-j+1)}{n(n+1)}\)。重新排序后 \((i,j)\) 成为逆序对的概率为 \(\frac{1}{2}\),因此数对 \((i,j)\) 对答案的贡献为 \(\frac{i(n-j+1)}{n(n+1)}\),当前情况的总贡献为

    \[\sum_{i=1}^n\sum_{j=i+1}^n\frac{i(n-j+1)}{n(n+1)}\\=\sum_{i=1}^n\frac{i}{n(n+1)}\sum_{j=i+1}^n(n-j+1)\\=\sum_{i=1}^n\frac{i(n-i)(n-i+1)}{2n(n+1)} \]

    1. 数对 \((i,j)\) 不完全包含于区间 \([l,r]\) 中。发生当前情况的总概率为 \(1-\frac{2i(n-j+1)}{n(n+1)}\)。由于不完全包含于区间,因此重排后相对顺序不变,因此当且仅当 \((i,j)\) 为逆序对时才会对答案有贡献。因此当前情况的总贡献为 \(\sum_{i=1}^n\sum_{j=i+1}^n[a_i>a_j](1-\frac{2i(n-j+1)}{n(n+1)})\),设 \(num_i\) 表示以 \(i\) 为数对左端点的逆序对个数,因此原式转化为

    \[\sum_{i=1}^n(num_i-\frac{2i\times num_i\sum_{a_i>a_j}(n-j+1)}{n(n+1)})\\=\sum_{i=1}^n\frac{num_i(n+1)(n-2i)-2i\sum_{a_i>a_j}j}{n(n+1)} \]

    \(num_i\)\(\sum_{a_i>a_j}j\) 可以用树状数组维护,因此时间复杂度为 \(O(n\log n)\)

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1e5+10;
    typedef long long ll;
    int n,a[N];
    double res;
    struct Fenwick_Tree
    {
        int t[N];
        ll sum[N];
        #define lowbit(x) ((x)&-(x))
        void add(int a,int x)
        {
            for(int i=a;i<=n;i+=lowbit(i))
            {
                t[i]++;
                sum[i]+=x;
            }
        }
        int query_num(int a)
        {
            int res=0;
            for(int i=a;i;i-=lowbit(i))res+=t[i];
            return res;
        }
        ll query_sum(int a)
        {
            ll res=0;
            for(int i=a;i;i-=lowbit(i))res+=sum[i];
            return res;
        }
    }t; 
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int i=n;i;i--)
        {
            int x=t.query_num(a[i]);
            res+=0.5*i*(n-i+1)*(n-i);
            res+=(ll)x*(n+1)*(n-2*i)+2ll*i*t.query_sum(a[i]);
            t.add(a[i],i);
        }
        printf("%.12lf",res/n/(n+1));
        return 0;
    }
    

BZOJ2720

  • 解析:组合数学+期望。根据期望的线性性,我们可以考虑分别对每个人求期望的贡献值。我们发现只有所有比当前这个人更矮的人能给当前这个人产生 \(1\) 的贡献,因此我们可以直接求所有比当前这个人矮的人对当前这个人产生贡献的概率 + 1,就是当前这个人的贡献。

    我们设比当前这个人更矮的人有 \(sum\) 个,则有 \(n-sum-1\) 个人比他更高。我们拎出 \(sum\) 个比当前这个人更矮的人,首先挑出一个放在当前这个数的前面,有 \(sum\) 种,同时对剩下的部分进行全排列,则方案数为 \(A_{n}^{sum-1}\),剩下不小于的部分有 \(n-sum\) 个数,对这些数可以进行全排列,因此方案数为 \((n-sum)!\),总方案数则有 \(\frac{A_{n}^{s-1}(n-s)!s}{n!}+1\),继续推式子,最终得出答案为 \(\frac{n-1}{n-sum+1}\),因为值域很小,可以直接开个桶递推即可。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1010;
    int n,h[N],sum;
    double res;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1,x;i<=n;i++)
        {
            cin>>x;
            h[x]++;
        }
        for(int i=1;i<=1000;i++)
        {
            res+=h[i]*(n+1.0)/(n-sum+1);
            sum+=h[i];
        }
        printf("%.2f",res);
        return 0;
    }    
    

随机游走

随机游走问题是期望问题中一类很典型的问题,常常与 dp 和高斯消元相结合。

P2973 [USACO10HOL] Driving Out the Piggies G

  • 解析:非常典的题。
    \(f_u\) 表示节点 \(u\) 处爆炸的概率,\(p\) 为炸弹爆炸的概率,\(d_u\) 表示节点 \(u\) 的度数,则对于所有与 \(u\) 相连的节点 \(j\) 有以下转移:

    \[f_u=\sum_{j\in u}\frac{(1-p)\times f_j}{d[j]} \]

    注意因为炸弹原来在 \(1\),因此 \(f_1=p+\sum_{j\in u}\frac{(1-p)\times f_j}{d[j]}\)
    发现对于每个点都能写出一个像这样的式子,因此高斯消元即可解决。

    参考代码
    #include<iostream>
    #include<cmath>
    using namespace std;
    const int N=310;
    const double eps=1e-10;
    int n,m,a,b,d[N][N],din[N];
    double p,mat[N][N];
    void gauss()
    {
        for(int c=1,r=1;c<=n;c++)
        {
            int t=r;
            for(int i=r;i<=n;i++)
                if(fabs(mat[i][c])>fabs(mat[t][c]))t=i;
            for(int i=c;i<=n+1;i++)swap(mat[r][i],mat[t][i]);
            for(int i=n+1;i>=c;i--)mat[r][i]/=mat[r][c];
            for(int i=r+1;i<=n;i++)
                if(fabs(mat[i][c])>eps)
                    for(int j=n+1;j>=c;j--)
                        mat[i][j]-=mat[i][c]*mat[r][j];
            r++;
        }
        for(int i=n;i;i--)
            for(int j=i+1;j<=n;j++)
                mat[i][n+1]-=mat[j][n+1]*mat[i][j];
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        cin>>n>>m>>a>>b;
        p=(double)a/b;
        for(int i=1,a,b;i<=m;i++)
        {
            cin>>a>>b;
            if(a>b)swap(a,b);
            d[a][b]=1,din[a]++,din[b]++;
        }
        mat[1][n+1]=-p;
        for(int i=1;i<=n;i++)
            for(int j=i;j<=n;j++)
                if(i==j)mat[i][i]=-1;
                else if(d[i][j])
                {
                    mat[i][j]=(1-p)/din[j];
                    mat[j][i]=(1-p)/din[i];
                }
        gauss();
        for(int i=1;i<=n;i++)printf("%.10lf\n",mat[i][n+1]);
        return 0;
    }
    

P3232 [HNOI2013] 游走

  • 解析:首先有一个贪心的思想,期望经过次数多的边尽量令编号更小,因此转化为求每个边期望经过次数,而边的期望经过次数又可以转化成边两端的点的期望经过次数。具体来说,设 \(f_u\) 表示点 \(u\) 期望经过次数,\(d_u\) 表示 \(u\) 节点的度数,则有 \(E(edge_{a,b})=\frac{f_a}{d_a}+\frac{f_b}{d_b}\),因此我们考虑求 \(f_u\)

    \(link_u\) 表示与 \(u\) 有连边的点,则有

    \[\begin{array}{l} \left\{\begin{matrix} f_u=1+\sum_{j\in link_u}\frac{f_j}{d_j}&(u=1)\\ f_u=\sum_{j\in link_u}\frac{f_j}{d_j}&(u\ne 1 \wedge u\ne n)\\ f_u=1&(u=n) \end{matrix}\right. \end{array} \]

    因此,我们可以找出 \(n\) 个多元一次方程,高斯消元即可。注意不同高斯消元的方式对精度的要求不同,注意 eps 值设定。

    参考代码
    #include<iostream>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    const int N=510,M=2.5e5+10;
    const double eps=1e-10;
    int n,m,d[N];
    int h[N],e[M],ne[M],idx;
    double mat[N][N];
    struct Edge
    {
        int a,b;
        double w;
        bool operator<(const Edge e)const 
        {
            return w<e.w;
        }
    }edge[M];
    void add(int a,int b)
    {
        e[++idx]=b,ne[idx]=h[a],h[a]=idx;
    }
    void gauss()
    {
        for(int c=1,r=1,t=r;c<=n;c++,t=++r)
        {
            for(int i=r;i<=n;i++)
                if(fabs(mat[i][c])>fabs(mat[t][c]))t=i;
            swap(mat[t],mat[r]);
            for(int i=n+1;i>=c;i--)mat[r][i]/=mat[r][c];
            for(int i=r+1;i<=n;i++)
                if(fabs(mat[i][c])>eps)
                    for(int j=n+1;j>=c;j--)
                        mat[i][j]-=mat[i][c]*mat[r][j];
        }
        for(int i=n;i;i--)
            for(int j=i+1;j<=n;j++)
                mat[i][n+1]-=mat[j][n+1]*mat[i][j];
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>m;
        for(int i=1,a,b;i<=m;i++)
        {
            cin>>a>>b;
            add(a,b),add(b,a);
            edge[i]={a,b};
            d[a]++,d[b]++;
        }
        mat[1][n+1]=-1,mat[n][n]=mat[n][n+1]=1;
        for(int u=1;u<n;u++)
        {
            mat[u][u]=-1;
            for(int i=h[u];i;i=ne[i])
            {
                int j=e[i];
                if(j==n)continue;
                mat[u][j]=1.0/d[j];         
            }
        }
        gauss();
        for(int i=1;i<=m;i++)
        {
            int a=edge[i].a,b=edge[i].b;
            double w=0;
            if(a!=n)w+=mat[a][n+1]/d[a];
            if(b!=n)w+=mat[b][n+1]/d[b];
            edge[i].w=w;
        }
        sort(edge+1,edge+1+m);
        double res=0;
        for(int i=1;i<=m;i++)res+=edge[i].w*(m-i+1);
        printf("%.3f",res);
        return 0;
    }
    

CF1823F Random Walk

  • 解析:根据上一题的经验,不难想到用高斯消元,我们设 \(f_u\) 表示经过点 \(u\) 的期望次数,设 \(d_u\) 为点 \(u\) 的度数,\(link_u\) 表示与 \(u\) 有连边的点,则有

    \[\begin{array}{l} \left\{\begin{matrix} f_u=1+\sum_{j\in link_u}\frac{f_j}{d_j}&(u=s)\\ f_u=\sum_{j\in link_u}\frac{f_j}{d_j}&(u\ne s \wedge u\ne t)\\ f_u=1&(u=t) \end{matrix}\right. \end{array} \]

    但是由于本题 \(n\le 2\times 10^5\),因此不能直接进行高斯消元,因此我们就需要引入一个小 trick ——树上高斯消元

    啥是树上高斯消元,简单来说,就是把式子化成 \(f_u=g_uf_{fa_u}+c_u\) 的形式,然后通过一次 dfs 预处理出来所有 \(g_u\)\(c_u\) 的值,然后在第二次的 dfs 递归求解所有的 \(f_u\)

    回到本题,我们可以用这个技巧尝试解决本题,因此我们尝试将原来的式子转化为上面所说的形式。

    \[\begin{array}{l} f_u=[u=s]+\sum_{v\in link_u,v\ne t}\frac{f_v}{d_v}\\ f_u=[u=s]+\frac{f_{fa_u}}{d_{fa_u}}+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{f_v}{d_v}\\ f_u=[u=s]+\frac{f_{fa_u}}{d_{fa_u}}+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_vf_u+c_v}{d_v}\\ f_u=[u=s]+\frac{f_{fa_u}}{d_{fa_u}}+f_u\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_v}{d_v}+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{c_v}{d_v}\\ (1-\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_v}{d_v})f_u=[u=s]+\frac{f_{fa_u}}{d_{fa_u}}+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{c_v}{d_v}\\ f_u=\frac{f_{fa_u}}{(1-\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_v}{d_v})d_{fa_u}}+\frac{[u=s]+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{c_v}{d_v}}{1-\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_v}{d_v}} \end{array} \]

    上面的部分就是本题转化的过程,最后得出

    \[g_u=\frac{1}{(1-\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac {g_v}{d_v})d_{fa_u}}\\ c_u=\frac{[u=s]+\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{c_v}{d_v}}{1-\sum_{v\in link_u,v\ne t\wedge v\ne fa_u}\frac{g_v}{d_v}} \]

    由于所有的 \(d_u\) 已知,同时其他需要的所有信息都只和子节点信息有关,因此可以使用 dfs 递归处理,转移出所有的 \(g_u\)\(c_u\)。然后再进行一遍 dfs 求出所有的 \(f_u\) 即可。

    注意由于每次我们根据父节点转移到子节点,因此我们需要以一个已知的信息为根,而开始时我们就知道 \(f_t=1\),因此我们就以 \(t\) 为根节点进行 dfs 即可。部分细节请参考代码。

    参考代码
    #include<iostream>
    using namespace std;
    typedef long long ll;
    const int N=2e5+10,M=4e5+10,mod=998244353;
    int n,s,t,d[N],f[N],g[N],c[N];
    int h[N],e[M],ne[M],idx;
    void add(int a,int b)
    {
        e[++idx]=b,ne[idx]=h[a],h[a]=idx;
    }
    int pow(int a,int b=mod-2)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    void dfs(int u,int fa)
    {
        g[u]=1,c[u]=(u==s);
        for(int i=h[u];i;i=ne[i])
        {
            int j=e[i];
            if(j==fa)continue;
            dfs(j,u);
            int dj=pow(d[j]);
            g[u]=(g[u]-(ll)g[j]*dj)%mod;
            c[u]=(c[u]+(ll)c[j]*dj)%mod;
        }
        g[u]=pow(g[u]+mod);
        c[u]=(ll)c[u]*g[u]%mod;
        g[u]=(ll)g[u]*pow(d[fa])%mod;
    }
    void dfs_ans(int u,int fa)
    {
        f[u]=((ll)g[u]*f[fa]+c[u])%mod;
        for(int i=h[u];i;i=ne[i])
        {
            int j=e[i];
            if(j==fa)continue;
            dfs_ans(j,u);
        }
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>s>>t;
        for(int i=1,a,b;i<n;i++)
        {
            cin>>a>>b;
            add(a,b),add(b,a);
            d[a]++,d[b]++;
        }
        dfs(t,0);
        dfs_ans(t,0);
        f[t]=1;//注意 t 节点遍历后被更改了,因此需要特判
        for(int i=1;i<=n;i++)cout<<f[i]<<' ';
        return 0;
    }
    

条件概率与期望线性性

P3802 小魔女帕琪

  • 解析:首先发现题目满足 \(E(X)=P(X)\),因此我们可以转化成求解概率。我们设 \(S=\sum_{a_i}\),首先我们发现,虽然魔法是按顺序使用的,但是实际上也相当于将所有的魔法放在一起打乱实施,然后统计连续七个都是不同魔法的组数的期望,因此连续的数与剩余的数量无关,因此本题其实可以看作与条件概率无关

    接下来我们就考虑如何求连续生成七个不同的魔法的概率即可。对于长度为 \(S\) 的序列,总共有 \(S-6\) 个长度为 \(7\) 的长度的子串,设 \(P\) 表示连续 \(7\) 位魔法都不同的概率,因此最终答案为 \(P\times(S-6)\),不难得出 \(P=7!\frac{\Pi_{i=1}^7a_i}{\frac{S!}{(S-7)!}}\),因此最终答案为 \(P\times(S-6)=7!\frac{\Pi_{i=1}^7a_i}{S(S-1)(S-2)(S-3)(S-4)(S-5)}\)

    参考代码
    #include<iostream>
    using namespace std;
    int a[10],s;
    double res=1;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        for(int i=1;i<=7;i++)
        {
            cin>>a[i];
            s+=a[i];
            res*=a[i]*i;
        }
        for(int i=s;i>=s-5;i--)res/=i;
        printf("%.3f",res);
        return 0;
    }
    

HDU5753

  • 解析:根据期望线性性,我们可以分别考虑每一个位置对答案的贡献。我们已知每个位置对答案的贡献为 \(c_i\),因此只需求每个位置产生贡献的概率即可。我们发现所有位置可以被分为两类,一类是两边的点,只需满足比相邻的一个点大即可,概率为 \(\frac{1}{2}\),另一类就是剩下在中间的点,需要满足同时比两边的点都要大,因此我们只要考虑三个点的大小关系即可,总共有六种可能,其中有两种可以对答案产生贡献,概率为 \(\frac{1}{3}\),递归求解即可。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1010;
    int n;
    double c[N];
    int main()
    {
        while(scanf("%d",&n)!=EOF)
        {
            for(int i=1;i<=n;i++)scanf("%lf",&c[i]);
            if(n==1)printf("%.6f\n",c[1]);
            else 
            {
                int res=0,ans=0;
                for(int i=1;i<=n;i++)
                    if(i==1||i==n)res+=c[i];
                    else ans+=c[i];
                printf("%.6f\n",res/2.0+ans/3.0);
            }
        }
        return 0;
    }
    

    还有一个问题值得思考,为什么后者的概率不是 \(\frac{1}{4}\)?实际上,我们发现,如果是四种情况,每一种的概率不是相同的,因此我们需要考虑三个数整体的大小关系,而不只是中间的数与旁边两个数的大小关系。

HDU5036

  • 解析:根据期望线性性,我们所求的答案等于对每个门使用炸弹的期望次数之和。我们设有 \(k\) 扇门可以通向当前门,对当前门使用的炸弹的期望次数为 \(\frac{1}{k}\),具体来说,当没有到达能到当前点的门时,炸掉当前门期望消耗一颗炸弹,概率是 \(\frac{1}{k}\),当到达过能到当前点的门时,不需要再消耗炸弹,因此对当前门炸弹的期望次数为 \(1\times\frac{1}{k}+0\times(1-\frac{1}{k})=\frac{1}{k}\),直接递推即可。求传递闭包需要用 bitset 优化。

    参考代码
    #include<iostream>
    #include<bitset>
    using namespace std;
    const int N=1010;
    int n;
    bitset<N>bt[N];
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        int T;
        cin>>T;
        for(int t=1;t<=T;t++)
        {
            cin>>n;
            for(int i=1,m;i<=n;i++)
            {
                bt[i].reset();
                bt[i][i]=1;
                cin>>m;
                for(int j=1,x;j<=m;j++)
                {
                    cin>>x;
                    bt[i][x]=1;
                }
            }
            for(int j=1;j<=n;j++)
                for(int i=1;i<=n;i++)
                    if(bt[i][j])bt[i]|=bt[j];
            double res=0;
            for(int i=1;i<=n;i++)
            {
                int cnt=0;
                for(int j=1;j<=n;j++)
                    if(bt[j][i])cnt++;
                res+=1.0/cnt;
            }
            printf("Case #%d: %.5f\n",t,res);
        }
        return 0;
    }
    

trick

忽略精度误差

CF643E Bear and Destroying Subtrees

  • 解析:
    由于精度需要控制在 \(1^{-6}\) 的范围内,发现对于一个点 \(u\),超过某一深度的所有儿子对它的贡献,不会很大,该深度大概是 \(\log_2\frac{1^{-6}}{5\times 10^5}\) 约为 \(-40\),因此我们考虑只计算深度差在 \(40\) 以内的所有儿子对节点 \(u\) 的贡献。

    \(f_{u,i}\) 表示节点 \(u\) 的子树随机删边后,深度 \(\le i\) 的概率。注意当前设定的深度为点的深度,也就是点的数量(\(=ans+1\))。对于 \(u\) 的其中一个儿子 \(j\),删边的贡献为 \(1\),概率为 \(\frac{1}{2}\),不删边的贡献为 \(f_{j,i-1}\),概率为 \(\frac{1}{2}\),因此转移就有 \(f_{u,i}=\sum_{j\in son_u}\frac{1}{2}(f_{j,i-1}+1)\)

    考虑如何在加点的过程中转移,对于一个新加的点 \(u\),只会给它的 \(40\) 以内级别的祖先产生贡献,如果直接转移,单次计算次数为 \(40\times 40\) 次,并不理想。对于一个 \(u\) 节点的 \(k\) 级父亲 \(fa\),我们发现 \(u\) 只会影响 \(f_{fa,k}\) 的值,因此每次只会对一个父亲转移一次,总复杂度为 \(O(40q)\)

    考虑如何统计答案,节点 \(u\) 的子树的期望深度为每一深度 \(\times\) 当前深度的概率,即为 \(\sum_{i=1}^{40}i\times(f_{u,i}-f_{u,i-1})\),由于我们开始时假设超过 \(40\) 深度差的儿子不会对父亲产生贡献,因此可以设 \(f_{u,40}=1\),化简后原式为 \(40-\sum_{i=1}^{39}f_{u,i}\),总复杂度为 \(O(40q)\)

    具体细节请看代码。

    参考代码
    #include<iostream>
    #include<vector>
    using namespace std;
    const int N=5e5+10;
    int m,cnt=1,fa[N];
    double f[N][42];
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        cin>>m;
        for(int i=1;i<=39;i++)f[1][i]=1;
        while(m--)
        {
            int opt,x;
            cin>>opt>>x;
            if(opt==1)
            {
                fa[++cnt]=x,x=cnt;
                for(int i=1;i<=39;i++)f[x][i]=1;
                vector<int>p;
                for(int i=1;i<=40;i++,x=fa[x])p.emplace_back(x);
                for(int i=p.size()-2;i;i--)f[p[i+1]][i+1]/=(f[p[i]][i]+1.0)/2;//注意要先删除原来的贡献
                for(int i=0;i<p.size()-1;i++)f[p[i+1]][i+1]*=(f[p[i]][i]+1.0)/2;//再计算更新后的贡献乘回去,不然会算重
            }
            else 
            {
                double res=39;
                for(int i=1;i<=39;i++)res-=f[x][i];
                printf("%.10f\n",res);
            }
        }
        return 0;
    }
    

dp 的状态设计

UVA11021 Tribles

  • 解析:
    \(f_i\) 表示一个麻球以及所有它生成的麻球在 \(i\) 天内全部死亡的概率,转移就有 \(f_i=\sum_{j=0}^{n-1}p_jf_{i-1}^j\),直接转移即可,答案为 \(f_m^k\)

    参考代码
    #include<iostream>
    #include<cstring>
    using namespace std;
    const int N=1e3+10;
    int n,k,m;
    double f[N],p[N];
    double pow(double a,int b)
    {
        double res=1;
        while(b)
        {
            if(b&1)res=res*a;
            a=a*a;
            b>>=1;
        }
        return res;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        int T;
        cin>>T;
        for(int t=1;t<=T;t++)
        {
            cin>>n>>k>>m;
            for(int i=0;i<n;i++)cin>>p[i];
            memset(f,0,sizeof f);
            f[1]=p[0];
            for(int i=2;i<=m;i++)
                for(int j=0;j<n;j++)
                    f[i]+=pow(f[i-1],j)*p[j];
            printf("Case #%d: %.7lf\n",t,pow(f[m],k));
        }
        return 0;
    }
    

BZOJ1419

  • 解析:我们设 \(f_{i,j}\) 表示有 \(i\) 张红牌,\(j\) 张黑牌时在最优策略下期望能得到的钱数,则答案为 \(f_{n,m}\)

    考虑如何转移,对于 \(f_{i,j}\),我们有 \(\frac{i}{i+j}\) 的概率抽到一张红牌,得到的钱数为 \(f_{i-1,j}+1\),有 \(\frac{j}{i+j}\) 的概率抽到一张黑牌,得到的钱数为 \(f_{i,j-1}-1\),因此期望钱数为 \(\frac{i}{i+j}(f_{i-1,j}+1)+\frac{j}{i+j}(f_{i,j-1}-1)\)。注意我们是最优策略,因此需要和 \(0\) 取最大值。边界条件即为 \(f_{i,0}=i,f_{0,i}=0\)

    参考代码
        #include<iostream>
    using namespace std;
    const int N=5010;
    int n,m;
    double f[N][N];
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            f[i][0]=i;
            for(int j=1;j<=m;j++)f[i][j]=max(0.0,(f[i-1][j]+1)*i/(i+j)+(f[i][j-1]-1)*j/(i+j));
        }
        printf("%.6f",f[n][m]-(5e-7));
        return 0;
    }
    

模拟赛 T1 礼物

  • 题意:给定 \(n\) 个物品中每个物品被拿出来的概率 \(p_i\) 以及得到这个物品时增加的喜悦值 \(w_i\)。每次只能拿取某一种礼物,也可能没有一项礼物被拿出(即 \(\sum_{i=1}^np_i \le 1\)),每种物品的喜悦值只会加一次,求最大喜悦值以及得到当前喜悦值的期望拿取次数。

    数据范围 \(n\le 20,0< w_i\le 10^9,0<p_i\le 1且\sum_{i=1}^np_i\le 1\)

  • 解析:由于 \(w_i\) 非负,因此最大喜悦值一定为所有礼物的喜悦值之和。

    看到 \(n\le 20\),不难想到状压 DP。设 \(f_s\) 表示状态 \(s\) 还需要拿去多少次能达到最大喜悦值,初始状态就有 \(f_{2^n-1}=0\),转移就有 \(f_s=\frac{1+\sum_{i|s>>i\&1=0}f_{s|1<<i}p_i}{\sum p_i}\),最终答案为 \(f_0\),具体细节参考代码。

    参考代码
    #include<iostream>
    using namespace std;
    typedef long long ll;
    const int N=21,K=1<<N;
    int n;
    ll sum;
    double p[N],f[K];
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1,x;i<=n;i++)
        {
            cin>>p[i]>>x;
            sum+=x;
        }
        int m=1<<n;
        f[m-1]=0;
        for(int s=m-2;s>=0;s--)
        {
            double sum=0;
            for(int j=0;j<n;j++)
                if(!(s>>j&1))
                {
                    int k=s|(1<<j);
                    f[s]+=f[k]*p[j+1];
                    sum+=p[j+1];
                }
            f[s]=(f[s]+1)/sum;
        }
        printf("%lld\n%.3f",sum,f[0]);
        return 0;
    }
    

扑克牌
acwing链接
洛谷链接

  • 解析:设 \(f_{a,b,c,d,x,y}\) 表示抽出 \(a\) 张黑桃,\(b\) 张红桃,\(c\) 张梅花,\(d\) 张方块,大小王的状态分别为 \(x\)\(y\)(0~3 表示变成了黑桃/红桃/梅花/方块,4 表示未使用),从当前状态到终点状态所需的最小期望牌数。我们设没有被抽的牌的总数为 \(sum\),则抽到黑桃的概率为 \(\frac{13-num_a}{sum}f_{a+1,b,c,d,x,y}\),同理可推得剩下三种花色牌抽到的概率。

    对于大小王,我们抽到的概率为 \(\frac{1}{sum}\),大王转移则有 \(\min_{i=0}^3{\frac{1}{sum}f_{a,b,c,d,i,y}}\),小王转移则有 \(\min_{i=0}^3{\frac{1}{sum}f_{1,b,c,d,x,i}}\)。记忆化搜索即可。注意边界条件。

    参考代码
    #include<iostream>
    #include<cstring>
    using namespace std;
    const int N=14;
    const double INF=1e20;
    int A,B,C,D;
    double f[N][N][N][N][5][5];
    double dp(int a,int b,int c,int d,int x,int y)
    {
        double&v=f[a][b][c][d][x][y];
        if(v>=0)return v;
        int as=a+(!x)+(!y),bs=b+(x==1)+(y==1);
        int cs=c+(x==2)+(y==2),ds=d+(x==3)+(y==3);
        if(as>=A&&bs>=B&&cs>=C&&ds>=D)return v=0;
        int sum=a+b+c+d+(x!=4)+(y!=4);
        sum=54-sum;
        if(sum<=0)return v=INF;
        v=1;
        if(a<13)v+=(13.0-a)/sum*dp(a+1,b,c,d,x,y);
        if(b<13)v+=(13.0-b)/sum*dp(a,b+1,c,d,x,y);
        if(c<13)v+=(13.0-c)/sum*dp(a,b,c+1,d,x,y);
        if(d<13)v+=(13.0-d)/sum*dp(a,b,c,d+1,x,y);
        if(x==4)
        {
            double t=INF;
            for(int i=0;i<4;i++)t=min(t,1.0/sum*dp(a,b,c,d,i,y));
            v+=t;
        }
        if(y==4)
        {
            double t=INF;
            for(int i=0;i<4;i++)t=min(t,1.0/sum*dp(a,b,c,d,x,i));
            v+=t;
        }
        return v;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0);
        cin>>A>>B>>C>>D;
        memset(f,-1,sizeof f);
        double t=dp(0,0,0,0,4,4);
        if(t>INF/2)t=-1;
        printf("%.3lf",t);
        return 0;
    }
    

CF540D Bad Luck Island

  • 解析:
    根据数据规模能猜出大概是高斯消元或者 dp,实际上也不难看出可以 dp 来做。

    我们设 \(f_{a,b,c}\) 表示剩余这些人的概率,初始状态就有 \(f_{r,s,p}=1\),我们设 \(sum\) 表示当前状态相遇方案的数量,则有 \(sum=ab+ac+bc\),转移就有

    \[if(a>0)f_{a-1,b,c}=f_{a,b,c}\times\frac{ac}{sum} if(b>0)f_{a,b-1,c}=f_{a,b,c}\times\frac{ab}{sum} if(c>0)f_{a,b,c-1}=f_{a,b,c}\times\frac{bc}{sum} \]

    同时注意特判一下 \(sum=0\) 的情况,对于 \(sum=0\) 的情况,直接统计答案即可。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=110;
    int r,s,p;
    double f[N][N][N],res1,res2,res3;
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>r>>s>>p;
        f[r][s][p]=1;
        for(int a=r;~a;a--)
            for(int b=s;~b;b--)
                for(int c=p;~c;c--)
                {
                    double sum=a*b+a*c+b*c,now=f[a][b][c];
                    if(!sum)
                    {
                        if(a)res1+=now;
                        else if(b)res2+=now;
                        else res3+=now;
                        continue;
                    }
                    if(a)f[a-1][b][c]+=now*a*c/sum;
                    if(b)f[a][b-1][c]+=now*a*b/sum;
                    if(c)f[a][b][c-1]+=now*b*c/sum;
                }
    
        printf("%.10f %.10f %.10f",res1,res2,res3);
        return 0;
    }
    

CF768D Jon and Orbs

  • 解析:
    \(f_{i,j}\) 表示第 \(i\) 天过后,已经选择了 \(j\) 种的概率。初始状态为 \(f_{1,1}=1\),分别从当前天选中一个旧数和前一天选中一个新数转移即可。状态转移方程为 \(f_{i,j}=\frac{j}{k}f_{i-1,j}+\times\frac{k-j+1}{k}f_{i-1,j-1}\)。注意 \(p\le 1000\),因此我们可以直接预处理出 \(1000\) 以内的所有答案即可,时间复杂度为 \(O((7274+k)\times k)\)\(7274\) 为预处理的循环计算次数,也是 \(p=1000\) 时的循环计算次数)。

    代码进行了滚动数组,空间复杂度为 \(O(k)\),注意滚动数组后的枚举顺序,由于状态转移方程从后往前转移,因此需要倒序枚举。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=2010,M=1010;
    int k,q,ans[N];
    double f[M];
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>k>>q;
        f[1]=1;
        int x=k,p=1;
        for(int i=2;i<=k;i++)
            for(int j=k;j;j--)
                f[j]=f[j]*j/k+f[j-1]*(k-j+1)/k;
        while(p<=1000)
        {
            if(p<=f[k]*2000)ans[p++]=x;
            else while(p>f[k]*2000)
            {
                for(int j=k;j;j--)f[j]=f[j]*j/k+f[j-1]*(k-j+1)/k;
                x++;
            }
        }
        cout<<x<<'\n';
        while(q--)
        {
            int p;
            cin>>p;
            cout<<ans[p]<<'\n';
        }
        return 0;
    }
    

[ABC277G] Random Walk to Millionaire

  • 解析:
    本题结合了随机游走,推式子等技巧。在 osu 专题中,我们通过对 \(x^2,x,1\) 分别进行统计以解决问题,本题我们同样也可以考虑这种策略。

    我们设 \(f_{u,i,0/1/2}\) 分别表示走 \(i\) 步后走到 \(u\) 后,当前点 \(x^0,x^1,x^2\) 的期望贡献,\(d_u\) 表示点 \(u\) 的度数。转移则有 \(f_{u,i}=\sum_{j\in son_u}\frac{f_{j,i-1}}{d_j}\)。对于点权为 \(1\) 的节点,我们直接将当前点的 \(f_{u,i,2}\) 计算到答案中;对于点权为 \(0\) 的节点,我们考虑更新 \(f_{u,i,1/2}\),即 \(f_{u,i,2}=(f_{u,i,2}+2f_{u,i,1}+f_{u,i,0})\)\(f_{u,i,1}=(f_{u,i,1}+f_{u,i,0})\)。部分细节请参考代码。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=3010,M=N<<1,mod=998244353;
    typedef long long ll;
    int n,m,k,d[N],c[N],inv[N];
    int h[N],e[M],ne[M],idx;
    int res,f[N][N][3];
    void add(int a,int b)
    {
        e[++idx]=b,ne[idx]=h[a],h[a]=idx;
    }
    int pow(int a,int b=mod-2)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>m>>k;
        for(int i=1,a,b;i<=m;i++)
        {
            cin>>a>>b;
            add(a,b),add(b,a);
            d[a]++,d[b]++;
        }
        for(int i=1;i<=n;i++)
        {
            cin>>c[i];
            inv[d[i]]=pow(d[i]);
        }
        f[1][0][0]=1;
        for(int t=1;t<=k;t++)
            for(int u=1;u<=n;u++)
            {
                for(int i=h[u];i;i=ne[i])
                {
                    int j=e[i];
                    f[u][t][0]=(f[u][t][0]+(ll)f[j][t-1][0]*inv[d[j]])%mod;
                    f[u][t][1]=(f[u][t][1]+(ll)f[j][t-1][1]*inv[d[j]])%mod;
                    f[u][t][2]=(f[u][t][2]+(ll)f[j][t-1][2]*inv[d[j]])%mod;
                }
                if(c[u])res=(res+f[u][t][2])%mod;
                else 
                {
                    f[u][t][2]=(f[u][t][2]+2ll*f[u][t][1]+f[u][t][0])%mod;
                    f[u][t][1]=(f[u][t][1]+f[u][t][0])%mod;
                }
            }
        cout<<res;
        return 0;
    }
    

CF908D New Year and Arbitrary Arrangement

  • 解析:
    本题状态的设计很重要,没有设计对状态就满盘皆输。

    \(f_{i,j}\) 表示前缀中有 \(i\) 个 字符 a,\(j\) 个子序列 ab 时的方案数,\(a\) 表示添加字符 a 的概率,b 表示添加字符 b 的概率,因此就有 \(a=\frac{p_a}{p_a+p_b},b=\frac{p_b}{p_a+p_b}\)。因此转移就有 \(f_{i,j}=af_{i+1,j}+bf_{i,j+i}\)

    然后考虑一下边界条件。我们发现当 \(i+j\ge n\) 时再添加一个字符 b 一定能停止,因此考虑添加一个字符 b 的期望次数,根据先前 \(E=\frac{1}{p}\) 的结论可知我们需要添加 \(\frac{1}{b}=\frac{p_a+p_b}{p_b}=1+\frac{p_a}{p_b}\) 次,因此同时添加了 \(\frac{p_a}{p_b}\) 个 a,我们设 \(c=\frac{p_a}{p_b}\),因此一共有 \(i+j+c\) 对子序列 ab(\(j+(i+c)\times 1\))。

    最后考虑如何统计答案。初始状态是 \(f_{0,0}\),但是发现 \(f_{0,0}\) 会自己递归到自己,因此我们考虑哪些会给 \(f_{0,0}\) 贡献,发现 \(f_{0,0}=\frac{p_a}{p_a+p_b}\times f_{1,0}+\frac{p_b}{p_a+p_b}\times f_{0,0}\),移项可得 \(f_{0,0}=f{1,0}\),因此答案为 \(f_{1,0}\)

    对于这种不知道从哪里转移到哪里的转移方程,建议用记忆化搜索代替 dp 的递推过程,不易出错。

    参考代码
    #include<iostream>
    using namespace std;
    const int N=1010,mod=1e9+7;
    typedef long long ll;
    int n,pa,pb,a,b,c,f[N][N];
    int pow(int a,int b)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    int dfs(int i,int j)
    {
        if(i+j>=n)return i+j+c;
        if(f[i][j])return f[i][j];
        f[i][j]=((ll)dfs(i+1,j)*a%mod+(ll)dfs(i,i+j)*b%mod)%mod;
        return f[i][j];
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n>>pa>>pb;
        a=(ll)pa*pow(pa+pb,mod-2)%mod,b=(ll)pb*pow(pa+pb,mod-2)%mod,c=(ll)pa*pow(pb,mod-2)%mod;
        cout<<dfs(1,0);
        return 0;
    }
    

杂题

CF1540B Tree Array

  • 解析:
    首先有一个很显然的结论,每次打标记的点一定与第一个打标记的点形成连通块,因此可以枚举每一个节点为第一个打上标记的节点,同时这个点也是根。
    考虑题目求期望的逆序对数,前面我们做过一道类似的题目CF749E Inversions After Shuffle,那道题中我们对每个逆序对分别考虑贡献,本题也可以参考那道题的经验,分别考虑所有的逆序对答案产生的贡献。
    考虑两个点 \(a,b\),其中 \(a\)\(b\) 先取到的概率是多少。我们发现两者 \(rt\to lca_{a,b}\) 路径相同,因此不会影响到两者谁先取到的概率,因此我们只需要考虑 \(lca_{a,b}\to a\)\(lca_{a,b}\to b\) 两条路径即可。
    我们发现某一个点被选到的概率就是路径上的所有点被选到的概率之积,而每个点被选到的概率是相同的,因此我们发现,这两个点某一个点先被取到的概率就是该点所对应的路径的长度与两个路径的长度之和的比,然后发现这个东西可以用 dp 以 \(O(n^2)\) 的时间复杂度预处理。
    再说一下 dp 的细节,设 \(f_{i,j}\) 表示往第一个点走 \(i\) 个单位长度,往第二个点走 \(j\) 个单位长度的概率。转移时就有 \(f_{i,j}=\frac{f_{i-1,j}+f_{i,j-1}}{2}\)
    然后这题就结束的,我在求 lca 时用的树剖,因此最后的时间复杂度为 \(O(n^3\log n)\),可以通过本题,还有一些细节可以参考代码。

    参考代码
    #include<iostream>
    #include<cstring>
    using namespace std;
    const int N=210,M=N<<1,mod=1e9+7;
    typedef long long ll;
    int n,f[N][N],res;
    int h[N],e[M],ne[M],idx;
    void add(int a,int b)
    {
        e[++idx]=b,ne[idx]=h[a],h[a]=idx;
    }
    int pow(int a,int b)
    {
        int res=1;
        while(b)
        {
            if(b&1)res=(ll)res*a%mod;
            a=(ll)a*a%mod;
            b>>=1;
        }
        return res;
    }
    int s[N],fa[N],dep[N],top[N],son[N];
    void dfs(int u,int father,int depth)
    {
        s[u]=1,fa[u]=father,dep[u]=depth,son[u]=0;
        for(int i=h[u];i;i=ne[i])
        {
            int j=e[i];
            if(j==father)continue;
            dfs(j,u,depth+1);
            s[u]+=s[j];
            if(s[son[u]]<s[j])son[u]=j;
        }
    }
    void dfs(int u,int t)
    {
        top[u]=t;
        if(!son[u])return;
        dfs(son[u],t);
        for(int i=h[u];i;i=ne[i])
        {
            int j=e[i];
            if(j==fa[u]||j==son[u])continue;
            dfs(j,j);
        }
    }
    int LCA(int x,int y)
    {
        while(top[x]!=top[y])
        {
            if(dep[top[x]]<dep[top[y]])swap(x,y);
            x=fa[top[x]];
        }
        if(dep[x]>dep[y])swap(x,y);
        return x;
    }
    int main()
    {
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin>>n;
        for(int i=1,a,b;i<n;i++)
        {
            cin>>a>>b;
            add(a,b),add(b,a);
        }
        for(int i=1;i<=n;i++)f[0][i]=1;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                f[i][j]=(f[i-1][j]+f[i][j-1])*(ll)pow(2,mod-2)%mod;
        for(int rt=1;rt<=n;rt++)
        {
            dfs(rt,0,0);
            dfs(rt,rt);
            for(int i=1;i<=n;i++)
                for(int j=1;j<i;j++)
                {
                    int lca=LCA(i,j);
                    res=((ll)res+f[dep[i]-dep[lca]][dep[j]-dep[lca]])%mod;
                }
        }
        cout<<(ll)res*pow(n,mod-2)%mod;
        return 0;
    }
    

习题(不定时更新)

推式子

CF696B Puzzles

先思考再看解析

\(f_u\) 表示点 \(u\) 的期望编号,\(s_u\) 表示 \(u\) 子树的大小,\(num_u\) 表示节点 \(u\) 的直接儿子数量,对于 \(s_u\)\(num_u\) 可以一遍 dfs 预处理。

初始状态有 \(f_1=1\),推式子就有 \(f_u=f_{fa_u}+\frac{2^{num_{fa_u}-2}(s_{fa_u}-s_u-1)}{2^{num_{fa_u-1}}}=f_{fa_u}+\frac{s_{fa_u}-s_u-1}{2}\),直接 dp 即可。

状态设计

CF148D Bag of mice

先思考再看解析

\(f_{w,b}\) 表示袋中还有 \(w\) 只白鼠,\(b\) 只黑鼠,先手能赢的概率。边界为 \(f_{0,b}=0,f_{w,0}=1,f_{w,1}=\frac{w}{w+1}\),转移分别讨论即可:

  1. \(A\) 先手抽到白鼠就能赢,概率为 \(\frac{w}{w+b}\)
  2. \(A\) 先手抽到黑鼠,\(B\) 后手抽到黑鼠,跑出一只黑鼠,然后 \(A\) 赢,概率为 \(\frac{b}{w+b}\frac{b-1}{w+b-1}\frac{b-2}{w+b-2}f_{w,b-3}\)
  3. \(A\) 先手抽到黑鼠,\(B\) 后手抽到黑鼠,跑出一只白鼠,然后 \(A\) 赢,概率为 \(\frac{b}{w+b}\frac{b-1}{w+b-1}\frac{w}{w+b-2}f_{w-1,b-2}\)

可以记忆化搜索解决。

HDU5781

先思考再看解析

\(f_{i,j}\) 表示存款上限为 \(i\),还有 \(j\) 被警告的机会,取完钱的期望次数。

考虑状态转移,如果我们选定一个 \(k\),有 \(\frac{i-k+1}{i+1}\) 的概率不会超出范围,上限变为 \(i-k\),有 \(\frac{k}{i+1}\) 的概率会超出范围,上限变为 \(k-1\),因此选定一个 \(k\) 的期望为 \(\frac{i-k+1}{i+1}f_{i-k,j}+\frac{k}{i+1}f_{k-1,j-1}\),则有 \(f_{i,j}=\min_{k=1}^i\{\frac{i-k+1}{i+1}f_{i-k,j}+\frac{k}{i+1}f_{k-1,j-1}\}\)。根据二分发现取款次数不超过 \(\log_22000\),开到 \(11\) 即可。

#include<iostream>
using namespace std;
const int N=2010;
const double inf=1e18;
int k,w;
double f[N][12];
void init()
{
    for(int i=1;i<=2000;i++)
    {
        f[i][0]=inf;
        for(int j=1;j<=11;j++)    
        {
            f[i][j]=inf;
            for(int k=1;k<=i;k++)
                f[i][j]=min(f[i][j],(i-k+1.0)/(i+1)*f[i-k][j]+k/(i+1.0)*f[k-1][j-1]+1);
        }
    }
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    init();
    while(cin>>k>>w)printf("%.6f\n",f[k][min(w,11)]);
    return 0;
}

P1850 [NOIP2016 提高组] 换教室

先思考再看解析

显然可以设 \(f_{i,j}\) 表示考虑前 \(i\) 个时间段申请 \(j\) 节课的期望最小消耗体力,但是这样不方便转移,因此我们考虑加一维 \(k=0/1\) 表示在第 \(i\) 个时间段不申请/申请一节课。

考虑状态转移,我们设 \(A=c_{i-1},B=d_{i-1},C=c_i,D=d_{i},p=k_{i-1},q=k_i\),设 \(dist_{i,j}\) 表示 \(i\)\(j\) 两点间的距离。

对于不申请当前点的部分,同时分别考虑上一个点申请/不申请的情况,则有

\[f_{i,j,0}=\min(f_{i-1,j,0}+dist_{A,C},f_{i-1,j,1}+p\times dist_{B,C}+(1-p)\times dist_{A,C})\\ \]

对于申请当前点的部分,同样分申请/不申请上一个点两种情况考虑,不申请上一个点,贡献则有

\[f_{i-1,j,0}+q\times dist_{A,D}+(1-q)\times dist_{A,C} \]

申请上一个点,需要记录四种情况的贡献,总贡献则有

\[f_{i-1,j-1,1}+pq\times dist_{B,D}+(1-p)q\times dist_{A,D}+p(1-q)\times dist_{B,C}+(1-p)(1-q)\times dist_{A,C} \]

所求的 \(f_{i,j,1}\) 的结果就是对上述两种情况取最小值即可。

还有一些细节问题,对于求距离,因为点很少,可以用 floyd 实现。初始化时注意下标问题,有一些特判。

代码有点抽象,仅作参考。

#include<iostream>
#include<cstring>
using namespace std;
const int N=2010,V=310;
int n,m,v,e,c[N],d[N],edge[V][V];
double k[N],f[N][N][2];
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>v>>e;
    memset(edge,0x3f,sizeof edge);
    for(int i=1;i<=n;i++)cin>>c[i];
    for(int i=1;i<=n;i++)cin>>d[i];
    for(int i=1;i<=n;i++)cin>>k[i];
    while(e--)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edge[a][b]=edge[b][a]=min(w,edge[a][b]);
    }
    for(int k=1;k<=v;k++)
        for(int i=1;i<=v;i++)
            for(int j=1;j<=v;j++)
                if(i^j)edge[i][j]=min(edge[i][j],edge[i][k]+edge[k][j]);
                else edge[i][j]=0;
    for(int i=0;i<=n;i++)
        for(int j=0;j<=m;j++)
                f[i][j][0]=f[i][j][1]=1e9;
    f[1][0][0]=f[1][1][1]=0;
    for(int i=2;i<=n;i++)
    {
        int A=c[i-1],B=d[i-1],C=c[i],D=d[i],Dac=edge[A][C],Dad=edge[A][D],Dbc=edge[B][C],Dbd=edge[B][D];
        double p=k[i-1],q=k[i];
        auto a=f[i-1];
        f[i][0][0]=a[0][0]+Dac;
        for(int j=1;j<=min(i,m);j++)
        {
            f[i][j][0]=min(a[j][0]+Dac,a[j][1]+Dac*(1-p)+Dbc*p);
            f[i][j][1]=min(a[j-1][0]+Dac*(1-q)+Dad*q,a[j-1][1]+Dac*(1-p)*(1-q)+Dad*(1-p)*q+Dbc*p*(1-q)+Dbd*p*q);
        }
    }
    double res=1e9;
    for(int i=0;i<=m;i++)res=min(res,min(f[n][i][0],f[n][i][1]));
    printf("%.2f",res);
    return 0;
}

杂题

P1297 [国家集训队] 单选错位

先思考再看解析

我们发现其实一道题正确率的概率只与选项的个数有关,我们设一道题的答案为 \(ans\),我们选的答案为 \(res\),分类讨论两道题的选项个数:

  1. \(a_i>a_{i+1}\),当前正确的概率为 \(a_{i+1}\) 中的一个,\(res\le a_{i+1}\) 的概率为 \(\frac{a_{i+1}}{a_i}\),正确率为 \(\frac{1}{a_{i+1}}\)\(res>a_{i+1}\) 的概率为 \(\frac{a_i-a_{i+1}}{a_i}\),正确率为 \(0\),因此总正确率为 \(\frac{a_{i+1}}{a_i}\times\frac{1}{a_{i+1}}=\frac{1}{a_i}\)
  2. \(a_i=a_{i+1}\),当前正确率显然为 \(\frac{1}{a_i}=\frac{1}{a_{i+1}}\)
  3. \(a_i<a_{i+1}\),类似的分析,正确率为 \(\frac{a_i}{a_{i+1}}\times \frac{1}{a_i}=\frac{1}{a_{i+1}}\)

总结后,一道题的正确率答案为 \(\max(a_i,a_{i+1})\),答案直接统计即可。实际上写的时候是直接猜出的结论。

update/未来可能会添加的题目

SP4060

P2473 [SCOI2008] 奖励关

P5516 [MtOI2019] 小铃的烦恼

CF850F Rainbow Balls

P4316 绿豆蛙的归宿

CF280C Game on Tree

P3772 [CTSC2017] 游戏

UOJ #211. 【UER #6】逃跑

参考资料

  1. 题目选讲

    概率期望 DP 题解合集

    动态规划之经典数学期望和概率DP

  2. 学习笔记

    概率期望知识点及题目详解

    期望学习笔记(1)

posted @ 2023-10-13 09:39  week_end  阅读(38)  评论(0编辑  收藏  举报