概率期望学习笔记

· · 个人记录

我 爱 数 学

突然发现我不会概率期望,就来补题了。

I.[CTSC2018]假面

期望第一题,居然能独立做出来。

首先这个数据范围明显是暗示我们一个O(Qm+Cn^2)的算法可以过去。

我们设pos_{i,j}表示敌人i剩余血量为j的概率。

则当使用一个“锁定”技能后,就相当于对pos_i做了一个背包,单次复杂度O(m_i)

再考虑“结界”技能。

我们设一个f_i,表示该技能使用时,恰有i个人有血的概率。这个可以O(n^2)背包搞出来。

然后,再对于每个iO(n)地从这个背包中“删掉”它。

需要注意的是,当i一定有血的时候,这个“删掉”操作与其它情况不同,需要特判一下。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y){
    int z=1;
    for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
    return z;
}
int n,lim[210],q,pos[210][110],p[210],f[210],g[210],inv[210];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&lim[i]),pos[i][lim[i]]=1;
    for(int i=1;i<=n;i++)inv[i]=ksm(i,mod-2);
    scanf("%d",&q);
    for(int qwq=1,a,b,c,d;qwq<=q;qwq++){
        scanf("%d",&a);
        if(a==0){
            scanf("%d%d%d",&b,&c,&d),c=1ll*c*ksm(d,mod-2)%mod,d=(mod+1-c)%mod;
            (pos[b][0]+=1ll*pos[b][1]*c%mod)%=mod;
            for(int i=1;i<=lim[b];i++)pos[b][i]=(1ll*pos[b][i]*d+1ll*pos[b][i+1]*c)%mod;
        }else{
            scanf("%d",&b);
            for(int i=1;i<=b;i++)scanf("%d",&p[i]);
            for(int i=0;i<=b;i++)f[i]=0;
            f[0]=1;
            int zero=0;
            for(int i=1;i<=b;i++){
                c=pos[p[i]][0],d=(mod+1-c)%mod;
                if(!c){zero++;continue;}
                for(int j=i-zero;j>=0;j--){
                    f[j]=1ll*f[j]*c%mod;
                    if(j)(f[j]+=1ll*f[j-1]*d%mod)%=mod;
                }
            }
//          printf("P:");for(int i=1;i<=b;i++)printf("%d ",pos[p[i]][0]);puts("");
//          printf("F:");for(int i=0;i<=b;i++)printf("%d ",f[i]);puts("");
            for(int i=1;i<=b;i++){
                c=pos[p[i]][0],d=(mod+1-c)%mod;
                c=ksm(c,mod-2);
                for(int j=0;j<=b-zero;j++)g[j]=f[j];
                if(c){
                    for(int j=0;j<b-zero;j++){
                        g[j]=1ll*g[j]*c%mod;
                        (g[j+1]+=mod-1ll*g[j]*d%mod)%=mod;
                    }                   
                }
//              printf("%d:GGG",i);for(int j=0;j<b;j++)printf("%d ",g[j]);puts("");
                int res=0;
                for(int j=0;j<=b-zero;j++)(res+=1ll*inv[j+zero+(c!=0)]*g[j]%mod)%=mod;
                printf("%d ",1ll*res*d%mod);
            }puts("");
        }
    }
    for(int i=1;i<=n;i++){
        int res=0;
        for(int j=0;j<=lim[i];j++)(res+=1ll*pos[i][j]*j%mod)%=mod;
        printf("%d ",res);
    }
    return 0;
}

II.[HAOI2012]高速公路

本题已经在我的任务列表里挂了1年多了

我们将这里的“期望”转成“\dfrac{\text{区间中任意两点间距离之和}}{\text{区间中选取两点的方案数}}。然后,我们考虑使用线段树维护区间中任意两点间距离之和。

在每个节点上,我们维护如下东西:

$len$:区间长度 $tag$:区间懒标记 $pre$:区间中每个点到区间中第一个点的距离之和 $suf$:区间中每个点到区间中最后一个点的距离之和 $all$:区间中任意两点间距离之和 这个可以通过简单拼凑得到。 时间复杂度$O(n\log n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m; ll gcd(ll x,ll y){return y?gcd(y,x%y):x;} #define lson x<<1 #define rson x<<1|1 #define mid ((l+r)>>1) #define C(x) (1ll*((x)+1)*(x)/2) struct SegTree{ int sum,len,tag; ll all,pre,suf; SegTree(){sum=len=tag=all=pre=suf=0;} friend SegTree operator +(const SegTree &x,const SegTree &y){ SegTree z; z.len=x.len+y.len; z.sum=x.sum+y.sum; z.pre=x.pre+y.pre+1ll*x.sum*y.len; z.suf=y.suf+x.suf+1ll*y.sum*x.len; z.all=x.all+y.all+x.suf*y.len+y.pre*x.len; return z; } void ADD(int x){ sum+=x*len; tag+=x; pre+=C(len)*x; suf+=C(len)*x; all+=1ll*len*(len+1)*(len+2)/6*x; } }seg[400100]; #define pushdown(x) seg[lson].ADD(seg[x].tag),seg[rson].ADD(seg[x].tag),seg[x].tag=0 void build(int x,int l,int r){ seg[x].len=r-l; if(l+1==r)return; build(lson,l,mid),build(rson,mid,r); } void modify(int x,int l,int r,int L,int R,int val){ if(l>=R||r<=L)return; if(L<=l&&r<=R){seg[x].ADD(val);return;} pushdown(x),modify(lson,l,mid,L,R,val),modify(rson,mid,r,L,R,val),seg[x]=seg[lson]+seg[rson]; } SegTree query(int x,int l,int r,int L,int R){ if(l>=R||r<=L)return SegTree(); if(L<=l&&r<=R)return seg[x]; pushdown(x); return query(lson,l,mid,L,R)+query(rson,mid,r,L,R); } void iterate(int x,int l,int r){ printf("[%d,%d):%d,%lld,%lld,%lld\n",l,r,seg[x].sum,seg[x].pre,seg[x].suf,seg[x].all); if(l+1<r)pushdown(x),iterate(lson,l,mid),iterate(rson,mid,r),seg[x]=seg[lson]+seg[rson]; printf("[%d,%d):%d,%lld,%lld,%lld\n",l,r,seg[x].sum,seg[x].pre,seg[x].suf,seg[x].all); } char c[10]; int main(){ scanf("%d%d",&n,&m),build(1,1,n); for(int i=1,l,r,v;i<=m;i++){ scanf("%s%d%d",c,&l,&r); if(c[0]=='C')scanf("%d",&v),modify(1,1,n,l,r,v); else{ ll x=query(1,1,n,l,r).all,y=C(r-l); ll GCD=gcd(y,x); printf("%lld/%lld\n",x/GCD,y/GCD); } // iterate(1,1,n); } return 0; } ``` # III.[[HNOI2015]亚瑟王](https://www.luogu.com.cn/problem/P3239) 观察题目,我们会发现两个性质: 1. 一张卡片最多只能在一轮游戏中被成功使用。 2. 一轮游戏最多只能成功使用一张卡片。 这样,我们纵向考虑每一张卡片,判断它在某局游戏中被成功使用的概率。 设我们当前有$t$轮游戏,且该卡片成功概率是$p$。则我们有$(1-p)^t$的概率在这局游戏中没有使用它,其余情况则在某局游戏中成功使用。 现在考虑这个$t$是怎么来的。显然,假如前面的卡片中有$s$张被使用了,这就相当于占用了$s$轮,在剩下的$r-s$轮里该卡片才有可能被使用。故此时我们有$t=r-s$。 我们设$f[i][j]$表示前$i$张卡片里,恰有$j$张卡片被成功施放的概率。则我们有 $$f[i][j]=f[i-1][j]\times(1-p_i)^{r-j}+f[i-1][j-1]\times\Big(1-(1-p_i)^{r-j+1}\Big)$$ 我们有 $$\sum\limits_{j}f[i][j]\times\Big(1-(1-p_i)^{r-j}\Big)$$ 的概率在所有游戏中成功施放该卡片。故直接用此概率与造成的伤害求积即可得到期望。 复杂度$O(Tnr\log r)$,假如你用快速幂的话。尽管倒着计算可以将复杂度优化至$O(Tnr)$,但是上述复杂度已经可以通过。 代码: ```cpp #include<bits/stdc++.h> using namespace std; double ksm(double p,int q){ double r=1; for(;q;q>>=1,p=p*p)if(q&1)r=p*r; return r; } int T,n,m,a[300]; double p[300],res,f[300][300]; int main(){ scanf("%d",&T); while(T--){ scanf("%d%d",&n,&m); for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)f[i][j]=0; for(int i=1;i<=n;i++)scanf("%lf%d",&p[i],&a[i]); f[0][0]=1,res=0; for(int i=1;i<=n;i++)for(int j=0;j<=min(m,i-1);j++){ double pos=ksm(1-p[i],m-j); f[i][j]+=f[i-1][j]*pos; f[i][j+1]+=f[i-1][j]*(1-pos); res+=(1-pos)*f[i-1][j]*a[i]; } printf("%.10lf\n",res); } return 0; } ``` # IV.[[六省联考2017]分手是祝愿](https://www.luogu.com.cn/problem/P3750) 首先,本题的基础是想到一种求解的方式: 当前第$n$盏灯只能被第$n$个开关控制,故我们只能操纵第$n$个开关将其搞灭。当其熄灭后,又相当于进入了$n-1$的游戏—— 因此,我们可以发现(或者瞎猜出来),任意局面都有唯一的最优方法,它操作在一组特定位置上。假如一次操作作用在应该作用的位置上,则总还需要的操作次数减一;否则,加一。 (详细的证明是因为我们如果把每个开关看作一个$n$维向量,则$n$个开关所对应的向量是线性无关的;换句话说,任意一个开关都是不可替代的) 于是我们可以先通过上面的方法,在$O(n\sqrt{n})$或者$O(n\ln n)$的时间内——这取决于你使用的算法——求出初始状态需要多少次操作,设为$P$。 则如果$P\leq k$,直接按照$P$次操作输出即可;否则,我们考虑它期望多少次操作才能够只剩$k$次操作。 一组naive的想法是设$f_i$表示当剩下$i$次操作时,消减到$k$的期望操作次数。则我们有 $$f_i=\dfrac{i}{n}\times f_{i-1}+\dfrac{n-i}{n}\times f_{i+1}+1$$ 特别地,我们令对于所有$i\leq k$,有$f_i=i$。 则我们就可以暴力高斯消元得出答案。详情可见某道[经典老题](https://www.luogu.com.cn/problem/CF24D)。 当然,这种方法有一个坏处,就是写起来太麻烦了,因为你开不下$n^2$的数组,只能记录对角线周围几行,这就导致操作极其恶心,特别是当主对角线上元素为$0$,需要手动交换相邻两行时。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=100003; int n,m,P; bool a[100100]; int g[100100][7],f[100100]; int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod; return z; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=n;i;i--)if(a[i]){ P++; for(int j=1;j*j<=i;j++){ if(i%j)continue; a[j]^=1; if(j*j<i)a[i/j]^=1; } } if(P<=m){for(int i=1;i<=n;i++)P=1ll*P*i%mod;printf("%d\n",P);return 0;} int invn=ksm(n,mod-2); for(int i=1;i<=n;i++){ g[i][6]=i; if(i<=m){g[i][2]=1,g[i][5]=i;continue;} int p=1ll*i*invn%mod,q=(mod+1-p)%mod; g[i][1]=p; g[i][2]=mod-1; g[i][3]=q; g[i][5]=mod-1; } for(int i=1;i<n;i++){ if(!g[i][2])swap(g[i][2],g[i+1][1]),swap(g[i][3],g[i+1][2]),swap(g[i][4],g[i+1][3]),swap(g[i][5],g[i+1][5]),swap(g[i][6],g[i+1][6]); int delta=1ll*g[i+1][1]*ksm(g[i][2],mod-2)%mod; (g[i+1][1]+=mod-1ll*delta*g[i][2]%mod)%=mod; (g[i+1][2]+=mod-1ll*delta*g[i][3]%mod)%=mod; (g[i+1][3]+=mod-1ll*delta*g[i][4]%mod)%=mod; (g[i+1][5]+=mod-1ll*delta*g[i][5]%mod)%=mod; } f[n]=1ll*g[n][5]*ksm(g[n][2],mod-2)%mod; for(int i=n-1;i;i--){ (g[i][5]+=mod-1ll*g[i][4]*f[i+2]%mod)%=mod; (g[i][5]+=mod-1ll*g[i][3]*f[i+1]%mod)%=mod; f[i]=1ll*g[i][5]*ksm(g[i][2],mod-2)%mod; } int res=f[P]; for(int i=1;i<=n;i++)res=1ll*res*i%mod;printf("%d\n",res); return 0; } ``` 还有一种做法就比较小清新了。 我们设$f_i$表示当前还剩$i$次操作时,削减掉一次操作的期望时间。 则有 $$f_i=\dfrac{i}{n}\times 1+\dfrac{n-i}{n}\times(f_{i}+f_{i+1}+1)$$ 理解:前一半是抽到一个需要的位置的概率;后一半是抽到一个不需要的位置时,你需要将次数从$i+1$先消减到$i$再消减到$i-1$。 将其化简,最终得到 $$f_i=\dfrac{f_{i+1}(n-i)+n}{i}$$ 故直接递推即可。最终结果只要对$f$求和即可。(别忘记最后还得加上一个$k$!) 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=100003; int n,m,P; bool a[100100]; int f[100100]; int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod; return z; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=n;i;i--)if(a[i]){ P++; for(int j=1;j*j<=i;j++){ if(i%j)continue; a[j]^=1; if(j*j<i)a[i/j]^=1; } } if(P<=m){for(int i=1;i<=n;i++)P=1ll*P*i%mod;printf("%d\n",P);return 0;} for(int i=n;i>=0;i--)f[i]=1ll*(1ll*(n-i)*f[i+1]%mod+n)%mod*ksm(i,mod-2)%mod; for(int i=n;i>=0;i--)(f[i]+=f[i+1])%=mod; int res=(f[m+1]-f[P+1]+m+mod)%mod; for(int i=1;i<=n;i++)res=1ll*res*i%mod;printf("%d\n",res); return 0; } ``` # V.[[SHOI2014]概率充电器](https://www.luogu.com.cn/problem/P4284) 这题实际上很简单,但是我却想歪了…… 我们我们可以设$f_i$表示$i$节点**熄灭**的概率。之所以不设为亮起的概率,是因为熄灭当且仅当周边节点没有一个连得到它,但是亮起却是周边至少有一个能连到它——用脚趾头想都知道哪个容易求。 设$p_i$表示$i$节点本身通电的概率,$p_{x,y}$表示$(x,y)$边通电的概率,则我们有方程 $$f_i=(1-p_i)\prod\limits_{(i,j)\in\mathbb{E}}(1-p_{i,j})+p_{i,j}\times f_j$$ 方程很好理解。但是,注意的是,此处的$f_j$中**不应考虑**$f_i$——因为我们此处是**以$i$为中心**,因此这里的$f_j$并不是真实的$f_j$。 所以,我们还要再二次扫描一下,对每个位置求出以它为根时的真实的$f_i$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,head[500100],cnt; struct node{ int to,next; double prob; }edge[1001000]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].prob=0.01*w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].prob=0.01*w,head[v]=cnt++; } double p[500100],f[500100],res; void dfs1(int x,int fa){ f[x]=(1-p[x]); for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)dfs1(edge[i].to,x),f[x]*=(1-edge[i].prob)+edge[i].prob*f[edge[i].to]; } void dfs2(int x,int fa){ for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)f[edge[i].to]*=(1-edge[i].prob)+edge[i].prob*f[x]/((1-edge[i].prob)+edge[i].prob*f[edge[i].to]),dfs2(edge[i].to,x); } int main(){ scanf("%d",&n),memset(head,-1,sizeof(head)); for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z); for(int i=1,x;i<=n;i++)scanf("%d",&x),p[i]=0.01*x; dfs1(1,0),dfs2(1,0); for(int i=1;i<=n;i++)res+=1-f[i]; printf("%lf\n",res); return 0; } ``` # VI.[[LnOI2019]加特林轮盘赌](https://www.luogu.com.cn/problem/P5249) 我们考虑设$f[i][j]$表示$i$个人中,第$j$个人最终存活的概率。 我们先考虑$j>1$的情况。此时,有$p$的概率排在首位的人挂掉,局面变为$f[i-1][j-1]$;反之,有$1-p$的概率首位存活,这就相当于所有人向前进一格,局面变为$f[i][j-1]$。 于是我们有 $$f[i][j]=p\times f[i-1][j-1]+(1-p)\times f[i][j-1]\qquad(1<j\leq i)$$ 下面我们考虑特殊情况$f[i][1]$。显然,只有这一枪他幸存下来了,他最终才有可能存活。故我们有 $$f[i][1]=(1-p)f[i][i]$$ 到这里我们就可以DP了。我们初始有$f[1][1]=1$;接着,我们从小往大枚举$i$,更新DP状态。 我们设$f[i][1]=a\times f[i][i]+b$。显然,此时有$a=1-p,b=0$。这样,我们就可以把$f[i][2]$表示成$c\times f[i][i]+d$的形式,接着把$f[i][3]$表示成$e\times f[i][i]+f$的形式…… 最终,我们得到一个方程: $$f[i][i]=x\times f[i][i]+y$$ 直接解该方程即可得出$f[i][i]$。再DP一圈即可得出所有的$f[i][j]$。 时间复杂度$O(n^2)$。(注意特判$p=0$时的情况) 代码: ```cpp #include<bits/stdc++.h> using namespace std; const long double eps=1e-9; int n,m; long double p,f[2][10100]; int main(){ cin>>p>>n>>m; if(p<eps){if(m==1)puts("1");else puts("0");return 0;} f[1][1]=1; for(int i=2;i<=n;i++){ double a=1,b=0; for(int j=1;j<=i;j++)a*=1-p,b=(1-p)*b+p*f[!(i&1)][j-1]; f[i&1][i]=b/(1-a); f[i&1][1]=(1-p)*f[i&1][i]; for(int j=2;j<i;j++)f[i&1][j]=(1-p)*f[i&1][j-1]+p*f[!(i&1)][j-1]; } printf("%.10Lf\n",f[n&1][m]); return 0; } ``` # VII.[[NOI2012]迷失游乐园](https://www.luogu.com.cn/problem/P2081) [题解戳这儿](https://www.luogu.com.cn/blog/Troverld/solution-p2081) # VII.[[ZJOI2015]地震后的幻想乡](https://www.luogu.com.cn/problem/P3343) 本题有两种思路。 一种思路是从暴力入手并优化状态。 我们考虑边的一组排列$\{p_1,\dots,p_m\}$。它是将边按照边权从小到大排列的结果。则我们在这组排列上跑Kruskal,设在加入排名为$i$的边时跑出了一棵生成树,则这组排列的答案就是排名为$i$的边权期望,按照题面中的提示,它就是$\dfrac{i}{m+1}$。 枚举每一种排列——它们都是等概率出现的——就能求出总概率。则这个算法的时间复杂度是$O(m\times m!)$。 考虑优化。因为我们要求的是**生成树中最大边**,所以多加入一些边并不会影响最大边的大小,于是我们实际上只要找出一个使得**之前图不连通,在加入这条边后图联通了**的位置$i$即可。 于是,我们发现在我们Kruskal加入一条新边之时,我们只考虑**之前放了多少条边**以及**图的连通性**即可。 发现单独求出加入多少条边时刚好联通不好求;于是我们干脆做个后缀和,设$f[i][j]$表示加入$i$条边后集合$j$联通的方案数,最后做一个差分即可求出所有刚好联通的方案数。 发现$f[i][j]$不好求。于是我们采取正难则反,考虑设$g[i][j]$表示集合$j$不连通的方案数。 我们考虑如何求出$j$。一个显然的想法是$O(3^n)$枚举子集,将$j$集合切成两半处理。为了不重不漏地计数,我们就强制令一半联通,另一半随便连,同时两半间不连边。这里我们采取计数问题中的经典策略,找出$j$中的lowbit位,设为$p$,并且强制令联通的集合中必须包含$p$(这就相当于枚举$p$所在的连通块)。 我们设$|j|$表示$j$集合内部共有多少条边。则我们有 $$g_{i,j}=\sum\limits_{k\subset j,p\in k}\sum\limits_{l=0}^if_{l,k}\times\dbinom{\Big|j\setminus k\Big|}{i-l}$$ 其中,$k$枚举子集,$l$枚举$k$中有多少条边,二项式系数的意义是从集合$j\setminus k$(其中$\setminus$是集合减符号)中选出$i-l$条边的方案数。 另,我们又有 $$f_{i,j}+g_{i,j}=\dbinom{|j|}{i}$$ 这很好理解,因为联通数加上不连通数必定等于总方案数。故我们就可以直接通过该式求出$f_{i,j}$。 则我们设$h_i$表示加入$i$条边时整张图联通的概率。于是有 $$h_i=\dfrac{f_{i,\mathbb{V}}}{\binom{m}{i}}$$ 其中$\mathbb{V}$是全体点集。 对$h_i$做差分就得到位置$i$刚好联通的概率;概率再乘上权值($\dfrac{i}{m+1}$)就得到了期望。 时间复杂度$O(m^23^n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,lim; bool s[20][20]; ll f[100][1<<10],g[100][1<<10],C[100][100],sz[1<<10]; double h[100],res; int main(){ scanf("%d%d",&n,&m),lim=1<<n; for(int i=0;i<=m;i++)C[i][0]=1; for(int i=1;i<=m;i++)for(int j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j]; for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),x--,y--,s[x][y]=s[y][x]=true; for(int i=0;i<lim;i++)g[0][i]=1; for(int i=0;i<n;i++)f[0][1<<i]=1,g[0][1<<i]=0; for(int i=1;i<lim;i++){ sz[i]=sz[i^(i&-i)]; int p=__builtin_ctz(i); for(int j=0;j<n;j++)if(i&(1<<j))sz[i]+=s[j][p]; } for(int i=1;i<=m;i++)for(int j=1;j<lim;j++){ int p=__builtin_ctz(j); for(int k=(j-1)&j;k;k=(k-1)&j)if(k&(1<<p))for(int l=0;l<=i;l++)g[i][j]+=f[l][k]*C[sz[j^k]][i-l]; f[i][j]=C[sz[j]][i]-g[i][j]; } for(int i=0;i<=m;i++)h[i]=1.0*f[i][lim-1]/C[m][i]; for(int i=m;i>=1;i--)h[i]-=h[i-1]; for(int i=0;i<=m;i++)res+=h[i]*i; printf("%lf\n",res/(m+1)); return 0; } ``` 另一种做法是从积分角度分析。 我们设$p(x)$为最大边为$x$的概率。则我们要求的期望则为 $$\mathrm{E}X=\int_0^1p(x)x\mathrm{d}x$$ 仿照前一种思路,我们设 $P(X)=\int_X^1p(x)\mathrm{d}x$,则有 $$\begin{aligned}\mathrm{E}X=&\int_0^1p(x)x\mathrm{d}x\\=&\int_0^1p(x)\Big(\int_0^x1\mathrm{d}t\Big)\mathrm{d}x\\=&\int_0^1\Big(\int_t^1p(x)\mathrm{d}x\Big)\mathrm{d}t\\=&\int_0^1P(t)\mathrm{d}t\end{aligned}$$ 下面我们考虑如何求出上式。观察到$P(X)$的实际意义是最大边大于等于$X$的概率;于是我们设$P_{\mathbb{S}}(X)$表示$\mathbb{S}$集合中最大边大于等于$X$的概率,则就有$P(X)=P_{\mathbb{V}}(X)$,其中$\mathbb{V}$是全部节点集合。 我们考虑转移出$P_{\mathbb{S}}(X)$:我们这次考虑枚举$1$所在的那个连通块,设此连通块为$\mathbb{T}$,则$\mathbb{T}$联通的概率是$1-P_{\mathbb{T}}(X)$,$\mathbb{T}$不与其它部分联通的概率是$(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}$,其中$e(\mathbb{T},\mathbb{S}-\mathbb{T})$是$\mathbb{S}$与其补集间边数。 于是就有 $$P_{\mathbb{S}}(X)=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\Big(1-P_{\mathbb{T}}(X)\Big)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}$$ 考虑到我们最终要求的是$\int_0^1P_{\mathbb{V}}(X)\mathrm{d}X$;于是开始推式子: $$\begin{aligned}\int_0^1P_{\mathbb{S}}(X)\mathrm{d}X&=\int_0^1\Bigg(\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\Big(1-P_{\mathbb{T}}(X)\Big)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\Bigg)\mathrm{d}X\\&=\int_0^1\Bigg(\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}-P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\Bigg)\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\int_0^1(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\int_0^1X^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\dfrac{0^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\end{aligned}$$ 到这里,我们停一下,设一个 $$F^k(\mathbb{S})=\int_0^1(1-X)^kP_{\mathbb{S}}(X)\mathrm{d}X$$ 则代入上面的式子,就有 $$F^0(\mathbb{S})=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-F^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}(\mathbb{T})$$ 于是现在的目标就是求出任意的$F^k(\mathbb{S})$;这很简单,只需要类似地代入式子中强推即可得到 $$F^k(\mathbb{S})=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+k+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-F^{k+e(\mathbb{T},\mathbb{S}-\mathbb{T})}(\mathbb{T})$$ 显然这就可以$O(m3^n)$地DP了。最终答案即为$F^0(\mathbb{V})$。 代码(极致压行版——它甚至没有我一个推导的式子长): ```cpp #include<stdio.h> int n,m,in[1<<10],lim; double f[1<<10][100]; int main(){ scanf("%d%d",&n,&m),lim=1<<n; for(int i=1,x,y;i<=m;i++){scanf("%d%d",&x,&y),x--,y--;for(int j=0;j<lim;j++)if((j&(1<<x))&&(j&(1<<y)))in[j]++;} for(int i=0;i<lim;i++)if(i&1)for(int j=(i-1)&i;j;j=(j-1)&i)if(j&1)for(int k=0,l=in[i]-in[j]-in[i^j];k+l<=m;k++)f[i][k]+=1.0/(k+l+1)-f[j][k+l]; printf("%.6lf\n",f[lim-1][0]); return 0; } ``` # VIII.[随机数生成器](https://www.luogu.com.cn/problem/P3600) 这题能自己做出来(虽然想了整整3天),我已经满足了。 我们设$p(x)$表示最大值刚好为$x$的概率。则答案为$\sum\limits_{i=1}^mp(i)i$。 有了上一题的经验,我们很容易想到刚好为$x$的概率不好求,必须做一个前缀和/后缀和才好求。那么到底是用前缀和还是后缀和呢? 假如你用后缀和的话,~~运用人类智慧,发现很不好求~~;于是我们采取前缀和。 我们设$g(x)$表示最大值$\leq x$的概率。则,每一个区间里都至少有一个位置是$\leq x$的。 我们考虑DP求出$g(x)$。我们设$pos_i$表示所有右端点在$i$之前的区间的左端点的最大值。我们再设$f(i)$表示强制位置$i$出现一个$\leq x$的数,且前$i$个位置的所有区间里都至少存在一个$\leq x$的数的概率;则有 $$f(i)=\dfrac{x}{m}\sum\limits_{j=pos_{i-1}}^{i-1}f(j)\Big(\dfrac{m-x}{m}\Big)^{i-1-j}$$ 其中,$j$是枚举$i$之前最后一个$\leq x$的数所在的位置。 则有 $$g(x)=\sum\limits_{j=pos_n}^nf(j)\Big(\dfrac{x}{m}\Big)^{n-j}$$ 对于$g(x)$差分即可得到$p(x)$,并进一步求出期望出来。 明显$pos_i$是单调不降的;故我们可以直接使用双针$O(n)$求出$f(i)$。另外还要$O(m)$枚举$x$,故总复杂度$O(nm)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=666623333; int n,m,invm,q,pos[2010],f[2010],g[2010],res; int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod; return z; } int main(){ scanf("%d%d%d",&n,&m,&q),invm=ksm(m,mod-2); for(int i=1,x,y;i<=q;i++)scanf("%d%d",&x,&y),pos[y]=max(pos[y],x); for(int i=1;i<=n;i++)pos[i]=max(pos[i-1],pos[i]); for(int i=1;i<=m;i++){ int p=1ll*i*invm%mod,q=1ll*(m-i)*invm%mod; f[0]=1; for(int j=1,k=0;j<=n;j++){ if(j>=1)for(int l=pos[j-2];l<pos[j-1];l++)(k+=mod-1ll*f[l]*ksm(q,j-2-l)%mod)%=mod; k=1ll*k*q%mod; (k+=f[j-1])%=mod; f[j]=1ll*k*p%mod; } for(int j=pos[n];j<=n;j++)(g[i]+=1ll*f[j]*ksm(q,n-j)%mod)%=mod; } for(int i=m;i>=1;i--)g[i]=(g[i]-g[i-1]+mod)%mod; for(int i=1;i<=m;i++)(res+=1ll*i*g[i]%mod)%=mod; printf("%d\n",res); return 0; } ``` # IX.[[TJOI2015]概率论](https://www.luogu.com.cn/problem/P3978) ~~O E I S 大 法 好~~ 我们设$f(x)$表示$x$个节点的二叉树的叶子节点个数之和,$g(x)$表示$x$个节点的二叉树总数。则答案就是$\dfrac{f(n)}{g(n)}$。 显然$g$就是卡特兰数;$f$通过$O(n^4)$暴力DP可以打出表来,发现是 ```1, 2, 6, 20, 70, 252, 924, 3432, 12870...``` 丢进OEIS一看,发现它就是$\dbinom{2n-2}{n-1}$。然后$g$的递推公式就是$\dfrac{\binom{2n}{n}}{n+1}$;两个一除,推推式子就得到了答案$\dfrac{n(n+1)}{2(2n-1)}$。 考虑它的实际意义—— 一棵$n$个点的二叉树,假设它又长出了一片叶子,则这就对应了一棵$n+1$个节点的二叉树。显然这片叶子有$2n-(n-1)=n+1$种长法。 我们再来考虑一棵$n+1$个节点的二叉树。删去它的每个叶子,都会得到一棵$n$个点的二叉树。显然这两种情况是一一对应的。 故我们就有$(n+1)g(n)=f(n+1)$。代入$g$的递推式,就能得到$f$。 代码(注释部分是$O(n^4)$的DP): ```cpp #include<bits/stdc++.h> using namespace std; int n; //int f[110][110],g[110],p[110]; int main(){ scanf("%d",&n); printf("%.10Lf",(long double)(n+1)*n/2/(2*n-1)); // f[0][0]=f[1][1]=1; // for(int i=2;i<=n;i++)for(int j=0;j<=i;j++)for(int k=0;k<i;k++)for(int l=0;l<=j;l++)f[i][j]+=f[k][l]*f[i-1-k][j-l]; // for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)p[i]+=f[i][j]*j; // g[0]=1; // for(int i=1;i<=n;i++)for(int j=0;j<i;j++)g[i]+=g[j]*g[i-j-1]; // for(int i=1;i<=n;i++)printf("(%lld/%lld)",p[i],g[i]); return 0; } ``` # X.[[SDOI2012]走迷宫](https://www.luogu.com.cn/problem/P6030) 这题本来是一个SCC+高斯消元的模板题来着的……但关键是DP状态的设计。 首先先判一下无解。显然,如果从起点出发能够走到一个**走不到终点的点**,则为无解。这很好想——只要答案有为无穷大的可能,无论概率多小,最终答案都会为无穷大。 然后就是DP设计了。我们无论设什么**从起点出发**的状态都是不太好的——所以,我们应该反过来,设**从终点出发**的状态。即,设$f_x$表示从节点$x$出发到终点的期望长度,则有 $$f_x=\dfrac{\sum\limits_{(x,y)\in\mathbb{E}}f_y}{deg_x}+1$$ 因为我们发现在不同SCC间转移是有序的,就可以直接按照拓扑序DP;只有在同一个SCC内部才会出现环,于是有环就直接用高斯消元暴力消掉即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,S,T,col[10100],in[10100],ord[10100],out[10100],sz[10100],c; vector<int>ele[10100]; namespace SCC{ int dfn[10100],low[10100],tot; stack<int>stk; vector<int>v[10100],u[10100]; bool vis1[10100],vis2[10100]; void dfs1(int x){ vis1[x]=true; for(auto y:u[x])if(!vis1[y])dfs1(y); } void dfs2(int x){ if(!vis1[x]){puts("INF");exit(0);} vis2[x]=true; for(auto y:v[x])if(!vis2[y])dfs2(y); } void Tarjan(int x){ dfn[x]=low[x]=++tot,stk.push(x); for(auto y:v[x]){ if(!dfn[y])Tarjan(y),low[x]=min(low[x],low[y]); else if(!col[y])low[x]=min(low[x],dfn[y]); } if(dfn[x]!=low[x])return; c++; int y; do{y=stk.top(),col[y]=c,ord[y]=sz[c]++,ele[c].push_back(y),stk.pop();}while(y!=x); } } namespace DAG{ double f[10100],g[110][110]; vector<int>v[10100],u[10100]; void Gauss(int x){ for(auto i:ele[x]){ if(i==T){g[ord[i]][ord[i]]=1,g[ord[i]][sz[x]]=0;continue;} for(auto j:SCC::v[i])if(col[i]==col[j])g[ord[i]][ord[j]]+=1.0/out[i];else g[ord[i]][sz[x]]-=f[j]/out[i]; g[ord[i]][sz[x]]-=1; g[ord[i]][ord[i]]+=-1; } for(int i=0;i<sz[x];i++){ int mx=i; for(int j=i+1;j<sz[x];j++)if(abs(g[mx][i])<abs(g[j][i]))mx=j; if(i!=mx)for(int j=i;j<=sz[x];j++)swap(g[mx][j],g[i][j]); for(int j=0;j<sz[x];j++){ if(i==j)continue; double tmp=g[j][i]/g[i][i]; for(int k=i;k<=sz[x];k++)g[j][k]-=tmp*g[i][k]; } } for(auto i:ele[x])f[i]=g[ord[i]][sz[x]]/g[ord[i]][ord[i]]; for(int i=0;i<sz[x];i++)for(int j=0;j<=sz[x];j++)g[i][j]=0; } queue<int>q; void Topo(){ q.push(col[T]); while(!q.empty()){ int x=q.front();q.pop(); Gauss(x); for(auto y:v[x]){ in[y]--; if(!in[y])q.push(y); } } } } int main(){ scanf("%d%d%d%d",&n,&m,&S,&T); for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),SCC::v[x].push_back(y),SCC::u[y].push_back(x),out[x]++; SCC::dfs1(T),SCC::dfs2(S); for(int i=1;i<=n;i++)if(!SCC::dfn[i])SCC::Tarjan(i); for(int i=1;i<=n;i++)for(auto j:SCC::v[i])if(col[i]!=col[j])DAG::v[col[j]].push_back(col[i]),in[col[i]]++; DAG::Topo(); printf("%.3lf\n",DAG::f[S]); return 0; } ``` # XI.[[HNOI2011]XOR和路径](https://www.luogu.com.cn/problem/P3211) 同上题一样,本题采用倒序DP的方式。 我们考虑按位处理。设当前处理到第$p$位,再设$f_i$表示从位置$i$出发,到达终点时的期望结果。 则对于一条边$(x,y,z)$,如果$z$在第$p$位上是$1$,则有$f_x\leftarrow 1-f_y$;否则,则有$f_x\leftarrow f_y$。 对于一个节点$x$,它可以从所有与它有边的节点转移过来;故直接高斯消元跑一下即可。 时间复杂度$O(n^3\log n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,head[110],out[110],cnt; struct node{ int to,next,val; }edge[20100]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++,out[u]++; if(u!=v)edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++,out[v]++; } long double g[110][110],res; void Gauss(int x){ for(int i=1;i<=n;i++){ if(i==n){g[i][i]=1;continue;} for(int j=head[i];j!=-1;j=edge[j].next){ if(edge[j].val&(1<<x))g[i][edge[j].to]-=1.0/out[i],g[i][n+1]-=1.0/out[i]; else g[i][edge[j].to]+=1.0/out[i]; } g[i][i]-=1; } for(int i=1;i<=n;i++){ int mx=i; for(int j=i+1;j<=n;j++)if(abs(g[mx][i])<abs(g[j][i]))mx=j; if(i!=mx)for(int j=i;j<=n+1;j++)swap(g[mx][j],g[i][j]); for(int j=1;j<=n;j++){ if(i==j)continue; double tmp=g[j][i]/g[i][i]; for(int k=i;k<=n+1;k++)g[j][k]-=tmp*g[i][k]; } } res+=(g[1][n+1]/g[1][1])*(1<<x); for(int i=1;i<=n;i++)for(int j=1;j<=n+1;j++)g[i][j]=0; } int main(){ scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)); for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z); for(int i=0;i<=30;i++)Gauss(i); printf("%.3Lf\n",res); return 0; } ``` # XII.[[NOI2005]聪聪与可可](https://www.luogu.com.cn/problem/P4206) 这题一个naive的思路是设$p_{i,j}$表示$i$时刻老鼠在位置$j$的概率,然后求出$f_i$表示猫$i$时刻前抓到老鼠的概率(因为如果$i$时刻猫可以抓到老鼠,则$i+1$时刻猫一定仍可以抓到老鼠;而$i$时刻猫能抓到老鼠的位置只有可能距猫的起点$\leq 2i$),最后差个分就能得出答案。 但是我们发现猫并不能保证所有$\leq 2i$的位置都能抓到——举例子来说,我们有这样一个样例: ![](https://cdn.luogu.com.cn/upload/image_hosting/r5uqg0gb.png) 猫从$1$出发,第一时刻到达$3$;但是,如果老鼠此时走到了$5$,虽然$1$到$5$的距离$\leq 2i=2$,但是猫仍不能一次抓到老鼠。 所以我们不得不考虑一种新的想法,即记录下猫和鼠的位置,求出此时它期望多少轮能够捉到鼠。 我们设$f_{i,j}$表示猫在$i$时,鼠在$j$时的期望次数;我们再设$nex2_{i,j}$表示此时猫下一步会走到哪里。$nex2$可以通过$O(nm)$地预处理出来距离数组,再$O(nm)$地预处理出来$nex1$数组表示猫走一步的目标,然后通过$nex1$预处理出来$nex2$得出。 则有: $f_{i,j}=0$,如果$i=j$; $f_{i,j}=1$,如果猫能直接从$i$走到$j$; 否则,设这里有一条边$(j,k)$,则有 $$f_{i,j}=\dfrac{f_{nex2_{i,j},j}\sum f_{nex2_{i,j},k}}{deg_k+1}$$ 凭直觉发现这不可能有环;故直接记忆化搞掉即可。 时间复杂度$O(nm)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,dis[1010][1010],nex1[1010][1010],nex2[1010][1010],cat,mouse; double f[1010][1010]; bool vis[1010][1010]; vector<int>v[1010]; void bfs(int S){ queue<int>q; memset(dis[S],0x3f,sizeof(dis[S])),dis[S][S]=0; q.push(S); while(!q.empty()){ int x=q.front();q.pop(); for(auto y:v[x])if(dis[S][y]==0x3f3f3f3f)dis[S][y]=dis[S][x]+1,q.push(y); } } double dfs(int x,int y){ if(vis[x][y])return f[x][y]; vis[x][y]=true; double &now=f[x][y]; if(x==y)return now=0; if(nex2[x][y]==-1)return now=1; x=nex2[x][y]; for(auto z:v[y])now+=dfs(x,z)+1; now+=dfs(x,y)+1; now/=int(v[y].size()+1); return now; } int main(){ scanf("%d%d",&n,&m); scanf("%d%d",&cat,&mouse); for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x); for(int i=1;i<=n;i++)bfs(i); // for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",dis[i][j]);puts("");} for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){ if(dis[i][j]==0x3f3f3f3f)continue; if(dis[i][j]<=1){nex1[i][j]=-1;continue;} int mp=-1; for(auto k:v[i])if(mp==-1||dis[mp][j]>dis[k][j]||dis[mp][j]==dis[k][j]&&mp>k)mp=k; nex1[i][j]=mp; } for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){ if(dis[i][j]==0x3f3f3f3f)continue; if(nex1[i][j]==-1||nex1[nex1[i][j]][j]==-1){nex2[i][j]=-1;continue;} nex2[i][j]=nex1[nex1[i][j]][j]; } // for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",nex2[i][j]);puts("");} // for(int i=1;i<=n;i++)printf("%d ",dis[i]);puts(""); printf("%.3lf\n",dfs(cat,mouse)); return 0; } ``` # XIII.[[JXOI2018]游戏](https://www.luogu.com.cn/problem/P4562) 这题好像根本不算概率期望罢…… 我们考虑$[l,r]$中,如果删去了区间中所有**不是区间中其他任何数的倍数**的数,则整个区间内所有的数都会被删去;反之,假如剩下了某些不是区间中其他任何数的倍数的数,则此区间一定不会被全部删完。 于是我们考虑求出区间中上述数的个数。考虑一个数满足上述条件的充分必要条件,就是它的最大约数$<l$。一个数的最大约数,可以直接通过线性筛求出来,复杂度$O(n)$。 然后我们考虑计算答案。我们考虑最后一个满足条件的数出现在序列中第$i$个位置的情形(应有$i\geq m$): 此时,共有 $$(i-1)!\times m\times\dbinom{n-m}{i-m}\times (n-i)!$$ 个排列满足上述条件。其中,$(i-1)!$是枚举前$i-1$个位置填哪些数,$m$是枚举第$i$个位置填什么数,$\dbinom{n-m}{i-m}$找出哪些不满足条件的数应被填入前$i$个位置中,$(n-i)!$枚举剩下位置的方案数。 组合数和阶乘可以通过$O(n)$预处理得到;则总复杂度$O(n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=1e9+7; int l,r,n,m,pri[10010000],fac[10010000],inv[10010000],res; void Ural(){ if(l==1){m=1;return;} for(int i=2;i<=r;i++){ if(!pri[i])pri[++pri[0]]=i,m+=(l<=i); for(int j=1;j<=pri[0]&&i*pri[j]<=r;j++){ pri[i*pri[j]]=true; m+=(l<=i*pri[j]&&l>i); if(!(i%pri[j]))break; } } } int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod; return z; } #define C(x,y) (1ll*fac[(x)]*inv[(y)]%mod*inv[(x)-(y)]%mod) int main(){ scanf("%d%d",&l,&r),n=r-l+1; Ural(); fac[0]=1; for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod; inv[n]=ksm(fac[n],mod-2); for(int i=n-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod; for(int i=m;i<=n;i++)(res+=1ll*i*m%mod*fac[i-1]%mod*C(n-m,i-m)%mod*fac[n-i]%mod)%=mod; printf("%d\n",res); return 0; } ``` # XIV.[[JXOI2018]排序问题](https://www.luogu.com.cn/problem/P4561) 本题好像又不算期望罢…… 根据一些简单的推理,我们发现最终答案就是 $$\dfrac{(n+m)!}{\prod\limits_{i}cnt_i!}$$ 其中$cnt_i$表示有多少个数是$i$。(这很简单,因为只有每个位置一一对应才能排序成功;但是值相同的数之间两两可以相互替换,故要除掉) 我们发现,对于$i\notin[l,r]$,这个$cnt_i$就已经固定了,可以直接通过哈希表/离散化预处理出来;而对于$i\in[l,r]$,$cnt_i$并不固定,而我们希望它尽量平均,故我们先用这$m$个位置尽量补,补到对于$i\in[l,r]$,$cnt_i$全部相等,然后剩下的直接平均分即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=998244353; const int N=10200000; int T,n,m,l,r,a[200100],b[200100],c[200100],fac[10200100],inv[10200100],res; vector<int>v; int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod; return z; } int main(){ fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod; inv[N]=ksm(fac[N],mod-2);for(int i=N-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod; scanf("%d",&T); while(T--){ scanf("%d%d%d%d",&n,&m,&l,&r),res=fac[n+m],v.clear(); for(int i=1;i<=n;i++)scanf("%d",&a[i]),v.push_back(a[i]); sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin()); for(int i=1;i<=n;i++)b[lower_bound(v.begin(),v.end(),a[i])-v.begin()]++; c[0]=r-l+1; for(int i=0;i<v.size();b[i++]=0)if(l<=v[i]&&v[i]<=r)c[0]--,c[b[i]]++;else res=1ll*res*inv[b[i]]%mod; int lim=0; for(lim=0;c[lim]!=r-l+1;lim++){ if(c[lim]<=m)m-=c[lim],c[lim+1]+=c[lim],c[lim]=0; else{c[lim+1]+=m,c[lim]-=m,m=0;break;} } if(c[lim]==r-l+1){ int tmp=m%c[lim],now=m/c[lim]; res=1ll*res*ksm(inv[lim+now+1],tmp)%mod*ksm(inv[lim+now],c[lim]-tmp)%mod; }else for(int i=lim,j=0;j!=r-l+1;i++)res=1ll*res*ksm(inv[i],c[i])%mod,j+=c[i]; for(int i=0;i<=n;i++)c[i]=0; printf("%d\n",res); } return 0; } ``` # XV.[小 Y 和恐怖的奴隶主](https://www.luogu.com.cn/problem/P4007) [题解](https://www.luogu.com.cn/blog/Troverld/solution-p4007) # XVI.[[BJOI2018]治疗之雨](https://www.luogu.com.cn/problem/P4457) 一眼能看出这是道高斯消元题。 我们设$f_i$表示当前英雄血量为$i$时期望多少次死掉。 则我们有 $$f_i=\dfrac{1}{m+1}\times\Big(\sum\limits_{j=0}^iq_jf_{i+1-j}\Big)+\dfrac{m}{m+1}\times\Big(\sum\limits_{j=0}^{i-1}q_jf_{i-j}\Big)+1$$ 其中$q_j$是打掉$j$点血的概率,有 $$q_j=\Big(\dfrac{1}{m+1}\Big)^j\Big(\dfrac{m}{m+1}\Big)^{k-j}\dbinom{k}{j}$$ 特殊地,当满血(即$i=n$)时,因为必定奶不到英雄,故直接有 $$f_i=\sum\limits_{j=0}^{i-1}q_jf_{i-j}+1$$ 明显这个DP有后效性,必须得高斯消元;但是$O(n^3)$你告诉我能跑$1500$? 抱歉,还真能。 我们列出转移矩阵,发现它大概长这样: $$\begin{bmatrix}&?,?,0,0,0,\dots,0\\&?,?,?,0,0,\dots,0\\&?,?,?,?,0,\dots,0\\&\dots\\&?,?,?,?,?,\dots,?\end{bmatrix}$$ 发现它十分接近倒着的上三角矩阵;故我们只需要消掉对角线上面的一行,就能把它消成一个真正的倒置上三角矩阵,然后就能$O(n^2)$代回解出了。 明显只消掉对角线上面一行的复杂度也是$O(n^2)$的;故总复杂度即为$O(n^2)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=1e9+7; int T,n,m,S,p,g[2010][2010],q[2010],inv[2010],f[2010]; int ksm(int x,int y){ int z=1; for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod; return z; } void Gauss(){ for(int i=n;i>1;i--){ if(!g[i][i])swap(g[i],g[i-1]); int tmp=1ll*g[i-1][i]*ksm(g[i][i],mod-2)%mod; for(int j=1;j<=i;j++)(g[i-1][j]+=mod-1ll*g[i][j]*tmp%mod)%=mod; (g[i-1][n+1]+=mod-1ll*g[i][n+1]*tmp%mod)%=mod; } // for(int i=1;i<=n;i++){for(int j=1;j<=n+1;j++)printf("%d ",g[i][j]);puts("");} f[1]=1ll*g[1][n+1]*ksm(g[1][1],mod-2)%mod; for(int i=2;i<=n;i++){ f[i]=g[i][n+1]; for(int j=1;j<i;j++)(f[i]+=mod-1ll*g[i][j]*f[j]%mod)%=mod; f[i]=1ll*f[i]*ksm(g[i][i],mod-2)%mod; } } int main(){ scanf("%d",&T); for(int i=1;i<=2000;i++)inv[i]=ksm(i,mod-2); while(T--){ scanf("%d%d%d%d",&n,&S,&m,&p); if(p==0||p==1&&m==0&&(S!=n||n!=1)){puts("-1");continue;} int P=ksm(m+1,mod-2),Q=1ll*m*P%mod; for(int i=0;i<=n;i++)q[i]=0; for(int i=0,j=1;i<=min(n,p);j=1ll*j*(p-i)%mod,i++,j=1ll*j*inv[i]%mod)q[i]=1ll*ksm(P,i)*ksm(Q,p-i)%mod*j%mod; for(int i=1;i<=n;i++)for(int j=1;j<=n+1;j++)g[i][j]=0; for(int i=1;i<=n;i++){ for(int j=0;j<i;j++)g[i][i-j]=1ll*q[j]*(i<n?mod+1-P:1)%mod; if(i<n)for(int j=0;j<i+1;j++)(g[i][i+1-j]+=1ll*q[j]*P%mod)%=mod; (g[i][i]+=mod-1)%=mod; g[i][n+1]=mod-1; } // for(int i=1;i<=n;i++){for(int j=1;j<=n+1;j++)printf("%d ",g[i][j]);puts("");} Gauss(); printf("%d\n",f[S]); } return 0; } ``` # XVII.[[SDOI2017]龙与地下城](https://www.luogu.com.cn/problem/P3779) 本题在模意义下和实数意义下,小范围和大范围下各有几种做法。 我们此处定义有$n$个骰子,每个骰子有$m$面。 1. 小数据范围 明显发现它就是$f(x)=\frac{\sum\limits_{i=0}^{m-1}x^i}{m}$的$n$次方。 于是直接倍增计算快速幂即可。时间复杂度$O(nm\log nm)$,卡的过去我请你喝茶。 2. 大数据范围 发现这道题精度要求很小,故我们掐头去尾,设一个 $\epsilon=10^{-13}$(亲测$10^{-9}$过不去),掐掉两边$<\epsilon$的部分。这样就能过去了。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const double pi=acos(-1); const double eps=1e-13; int T,n,m; int lim,LG,rev[500100]; struct cp{ double x,y; cp(double u=0,double v=0){x=u,y=v;} friend cp operator +(const cp &u,const cp &v){return cp(u.x+v.x,u.y+v.y);} friend cp operator -(const cp &u,const cp &v){return cp(u.x-v.x,u.y-v.y);} friend cp operator *(const cp &u,const cp &v){return cp(u.x*v.x-u.y*v.y,u.x*v.y+u.y*v.x);} }F[500100],G[500100]; void FFT(cp *a,int tp){ for(int i=0;i<lim;i++)if(i<rev[i])swap(a[i],a[rev[i]]); for(int md=1;md<lim;md<<=1){ cp rt=cp(cos(pi/md),tp*sin(pi/md)); for(int stp=md<<1,pos=0;pos<lim;pos+=stp){ cp w=cp(1,0); for(int i=0;i<md;i++,w=w*rt){ cp x=a[pos+i],y=w*a[pos+md+i]; a[pos+i]=x+y; a[pos+md+i]=x-y; } } } if(tp==-1)for(int i=0;i<lim;i++)F[i].x/=lim; } void mul(double *a,double *b,double *c,int A,int B){ LG=0,lim=1; while(lim<=A+B)lim<<=1,LG++; for(int i=0;i<lim;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(LG-1)); for(int i=0;i<lim;i++)F[i]=cp(a[i],0),G[i]=cp(b[i],0); FFT(F,1),FFT(G,1); for(int i=0;i<lim;i++)F[i]=F[i]*G[i]; FFT(F,-1); for(int i=0;i<=A+B;i++)c[i]=F[i].x; } int cut(double *a,int &LIM){ int i=0; while(a[i]<eps)i++; while(a[LIM]<eps)LIM--; for(int j=0;j+i<=LIM;j++)a[j]=a[j+i]; LIM-=i; return i; } double a[500100],b[500100]; int func(int x,int &len){ if(x==1)return 0; int tmp=func(x>>1,len); tmp<<=1; mul(a,a,a,len,len),len<<=1; tmp+=cut(a,len); if(!(x&1))return tmp; mul(a,b,a,len,m),len+=m; tmp+=cut(a,len); return tmp; } int main(){ scanf("%d",&T); while(T--){ scanf("%d%d",&m,&n); for(int i=0;i<=500000;i++)a[i]=b[i]=0; for(int i=0;i<m;i++)a[i]=b[i]=1.0/m; m--; int len=m; int tmp=func(n,len); for(int i=1;i<=len;i++)a[i]+=a[i-1]; for(int i=1,x,y;i<=10;i++){ scanf("%d%d",&x,&y),x--; double X=0,Y=0; if(x>=tmp){ x-=tmp; if(x>=len)X=1.0; else X=a[x]; } if(y>=tmp){ y-=tmp; if(y>=len)Y=1.0; else Y=a[y]; } printf("%lf\n",Y-X); } } return 0; } ``` 3. 大数据范围 我们发现在$n$很大时最终会近似于一个正态分布,故我们在较大范围内可以直接用正态分布进行积分。可以用自适应辛普森法计算(别问我,我不会)。 4. 模意义下 我们要计算 $$\Bigg(\dfrac{\sum\limits_{i=0}^{m-1}x^i}{m}\Bigg)^n$$ 中某一段的和。 我们乘上一个前缀和算子$\dfrac{1}{1-x}$,将其转成差分形式 $$\dfrac{1}{1-x}\times\dfrac{\Big(\sum\limits_{i=0}^{m-1}x^i\Big)^n}{m^n}$$ 套个等差数列求和公式,得到 $$\dfrac{1}{1-x}\times\dfrac{\Big(\dfrac{1-x^m}{1-x}\Big)^n}{m^n}$$ 继续化简 $$\dfrac{1}{1-x}\times\dfrac{(1-x^m)^n}{(1-x)^n\times m^n}$$ 于是 $$\dfrac{(1-x^m)^n}{(1-x)^{n+1}\times m^n}$$ 二项式展开 $$\dfrac{\sum\limits_{i=0}^n(-x^m)^i\dbinom{n}{i}}{(1-x)^{n+1}\times m^n}$$ 把分母上的东西当作$n+1$阶前缀和 $$\dfrac{\sum\limits_{i=0}^n(-x^m)^i\dbinom{n}{i}}{m^n}\times\sum\limits_{i=0}\dbinom{n+1+i}{i}x^i$$ 对于一次询问$[l,r]$,我们只需要管$x^{l-1}$和$x^r$的系数即可。通过预处理一大坨组合计数的东西可以简单计算。 # XVIII.[[AGC049A]Erasing Vertices](https://atcoder.jp/contests/agc049/tasks/agc049_a) 非常原教旨的概率题。假如想到这个point就应该非常easy罢。 我们考虑删掉一个节点的概率。则答案即为所有节点的概率之和。 对于某个节点来说,删去任何不能到达它的节点,对它都没有任何影响;而任意时刻,假如它未被删去,则所有能到达它的节点都未被删去。而在所有能到达它的点中随机删去一个,除非删到它本人,否则都不算数。 故我们发现实际上删掉一个节点的概率,就是 $\dfrac{1}{\text{所有能到达它的点的数量}}$。 于是传递闭包维护一下可达性即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n; char s[110][110]; double res; int main(){ scanf("%d",&n); for(int i=0;i<n;i++)scanf("%s",s[i]),s[i][i]='1'; for(int k=0;k<n;k++)for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(s[i][k]=='1'&&s[k][j]=='1')s[i][j]='1'; for(int i=0;i<n;i++){ int now=0; for(int j=0;j<n;j++)if(s[j][i]=='1')now++; res+=1.0/now; } printf("%.10lf\n",res); return 0; } ```