概率期望小记

概率期望小结

基础知识:概率,期望,这个博主也是我。

一个小技巧,求方差的期望时,有特殊的方法:

\(n=|\Omega|\)

\[V(X)=\frac 1n \sum_{A\in \Omega} (X(A)-E(X)^2) \]

\[=\frac 1n (\sum_{A\in \Omega} X(A)^2+\sum_{A\in \Omega} E(X)^2 -2E(X)\sum_{A\in \Omega}X(A)) \]

\[=E(X^2)+E(X)E(X)-2E(X)E(x) \]

\[=E(X^2)-E(X)^2 \]

概率期望的核心知识点就在于求解概率/期望上,这里讲一些期望的常见求解方法:

一、

直接利用定义正向DP得到随机变量所有取值的出现概率,再利用定义计算期望

通常适用于随机的变量的取值有限的情况。

例:洛谷P3600 随机数生成器

显然答案的最大值一定 \(\in[1,x]\),于是我们可以转化为概率问题:

\[ E(ans)=\sum_{i=1}^{x} iP(ans=i)=\sum_{i=1}^{x} i(P(ans\ge i)-P(ans \ge{i-1})) \]

\[E(ans)=\sum_{i=1}^{x} P(ans\ge i)=x-\sum_{i=1}^{x} P(ans<i) \]

考虑求解 \(P(ans<a)\),也就是所有区间中都有 \(<a\) 的数的概率。

首先如果一个区间完全包含了另一个区间,那么只要满足后者,前者一定满足,于是可以去掉前者。

将剩下的区间按左端点从小到大排序,右端点一定单调递增。

此时再考虑任意一个点 \(i\),如果选择这个点的 \(w_i<a\),这样的选择发生的概率为 \(p=\dfrac{i-1}{x}\) ,那么它会对所有覆盖它的区间打上标记,排序后这样的区间一定是连续的一段 \([l_i,r_i]\)

\(f_i\) 表示选择了 \(i\) 这个点,并且第 \(1-r_i\) 个区间都已经被打上标记了。枚举上一个选择的点\(j\),转移即为:

\[ f_i=p\sum_{j=0}^{i-1}[r_j\ge l_i-1]f_j(1-p)^{i-j-1} \]

思路就是上一个选择的 \(j\) 最多能覆盖到 \(r_j\),而新选择的 \(i\) 覆盖 \([l_i,r_i]\),那么中间不能有空缺的部分,同时 \([j+1,i-1]\) 的数都不应该 \(<a\)

直接递推是 \(\mathcal O(n^2)\) 的,可以双指针优化到 \(\mathcal O(n)\)

于是 \(P(ans<i)=\sum_{i=1}^{n} [r_i=m]f_i(1-p)^{n-i}\)

总复杂度 \(\mathcal O(n^2)\)

view code
    #include<bits/stdc++.h>
    using namespace std;
    const int mod=666623333;
    const int N=2010;
    int n,x,m,cnt,flag[N],l[N],r[N],f[N];
    struct query{
    	int l,r;
    }q[N],a[N];
    inline bool cmp(query x,query y){return (x.l^y.l)?x.l<y.l:x.r>y.r;}
    inline int dec(int x,int y){return (x-y<0)?x-y+mod:x-y;}
    inline void upd(int &x,int y){x=(x+y>=mod)?x+y-mod:x+y;}
    inline void dwn(int &x,int y){x=(x-y<0)?x-y+mod:x-y;}
    inline int ksm(int x,int y){
    	int ret=1;
    	for(;y;y>>=1,x=1ll*x*x%mod) if(y&1) ret=1ll*ret*x%mod;
    	return ret;
    }
    int pwp[N],pwnp[N],pwiv[N];
    int main(){
    	scanf("%d%d%d",&n,&x,&m);
    	for(int i=1;i<=m;++i) scanf("%d%d",&q[i].l,&q[i].r);	
    	sort(q+1,q+m+1,cmp);
    	for(int i=1;i<=m;++i){
    		while(cnt&&q[i].r<=a[cnt].r) cnt--;
    		a[++cnt]=q[i];
    	}
    	int ans=x,iv=ksm(x,mod-2);
    	for(int i=1;i<=n;++i){
    		r[i]=0;l[i]=1;
    		while(r[i]<cnt&&a[r[i]+1].l<=i) ++r[i];
    		while(l[i]<=r[i]&&a[l[i]].r<i) ++l[i];
    	}
    	for(int i=1;i<=x;++i){
    		int p=1ll*(i-1)*iv%mod,np=dec(1,p),iv=ksm(np,mod-2); 
    		pwp[0]=pwnp[0]=pwiv[0]=1;
    		for(int j=1;j<=n;++j){
    			pwp[j]=1ll*pwp[j-1]*p%mod,pwnp[j]=1ll*pwnp[j-1]*np%mod;
    			pwiv[j]=1ll*pwiv[j-1]*iv%mod;
    		}
    		f[0]=1;int sum=1,now=0;
    		for(int j=1;j<=n;++j){
    			while(now<j&&r[now]<l[j]-1) dwn(sum,1ll*f[now]*pwiv[now]%mod),++now;
    			f[j]=1ll*pwnp[j-1]*p%mod*sum%mod;
    			upd(sum,1ll*f[j]*pwiv[j]%mod);
    			if(r[j]==cnt) dwn(ans,1ll*f[j]*pwnp[n-j]%mod);
    		}
    	}
    	printf("%d\n",ans);
    	return 0;
    }

二、

直接使用期望 \(dp\),设 \(f_i\) 表示当前状态是 \(i\),从当前状态到末状态这部分过程的答案期望。通常末状态的 \(f\) 为0,然后从末状态往初状态倒着递推。

例:[NOI2012]迷失游乐园

考虑求出从每个点出发的路径边权和期望 \(ans_i\),然后得到答案。

对树上的情况,每个点出发的路径有两种,第一步先走到父亲,或者第一步走到某个儿子。分别用 \(f_u\)\(g_u\) 来表示这两种情况的答案.

\(g\) 的递推显然:末状态就是叶子结点 \(g_u=0\)

其他点满足\(g_u=\frac {1}{|son_u|} \sum_{v\in son_u}g _v+w_{u,v}\)

\(v\)\(u\) 的父亲,那么

\[f[u]=w_{u,v}+\frac{g_v*|son_v|+f_v-(g_u+w_{u,v})}{|son_v|+1-1} \]

末状态为 \(u\) 为根时 \(f_u=0\)。用 \(f\)\(g\) 推出 \(ans\) 是容易的。

对于基环树的情况,仍然考虑用 \(f\)\(g\) 来计算 \(ans\)。对于环上的点,我们将向上走定义为在环上走,向下走定义为朝以自己为根的那棵树走。

\(g\) 的求法不变,但用同样的求法求 \(f\) 时,树上的点可以照搬,但环上的点就会循环调用,因此特殊处理一下。

在环上走,一定是沿顺时针或逆时针走到某个点 \(i\) 后停止或者往 \(i\)的 子树走,直接推就可以了。求出环上的 \(f\) 后,再用之前的方法算树的 \(f\) 就没了。

view code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,cnt,first[N];
struct node{
	int u,v,w,nxt;
}e[N<<1];
inline void add(int u,int v,int w){e[++cnt].v=v;e[cnt].w=w;e[cnt].nxt=first[u];first[u]=cnt;}
int fa[N],son[N];
double down[N],up[N],ans[N];
bool vis[N]; 
int rt,fr[N];
int stk[N],top,L[N],R[N],fw[N],pre[N],nxt[N]; 
inline void findloop(int u,int f){
	fr[u]=f;
	vis[u]=1;
	if(top) return ;
	for(int i=first[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==f) continue;
		if(vis[v]){
			rt=v;
			top=0;L[v]=R[u]=e[i].w;
			while(u!=v){
				stk[++top]=u;L[u]=R[fr[u]]=fw[u];
				vis[u]=1,u=fr[u];
			}
			vis[v]=1;stk[++top]=v;
			return ;
		} 
		fw[v]=e[i].w;findloop(v,u);
		if(top) return;
	}
}
inline void dfs_down(int u,int f){
	for(int i=first[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==f||vis[v]) continue;
		son[u]++;fa[v]=1;
		dfs_down(v,u);
		down[u]+=down[v]+e[i].w;
	}
	if(!son[u]) return ;
	down[u]=down[u]/son[u];
}
inline void dfs_up(int u,int f,int w){
	if(!f) up[u]=0;
	else{
		if(fa[f]+son[f]==1) up[u]=w;
		else up[u]=w+(up[f]*fa[f]+down[f]*son[f]-(down[u]+w))/(fa[f]+son[f]-1);	
	}
	for(int i=first[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==f||vis[v]) continue;
		dfs_up(v,u,e[i].w);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<=m;++i)
		scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w);
	if(m==n){
		findloop(1,0);
		memset(vis,0,sizeof(vis));
		for(int i=1;i<=top;++i) vis[stk[i]]=1,pre[i]=i==1?top:i-1,nxt[i]=i==top?1:i+1;
		for(int i=1;i<=n;++i) fa[i]=1;
		for(int i=1;i<=top;++i){
			int u=stk[i];fa[u]=2;
			dfs_down(u,0);
		}
		for(int i=1;i<=top;++i){
			int u=stk[i];
			double p=0.5;
			for(int j=pre[i];j!=i;j=pre[j]){
				int v=stk[j];
				if(pre[j]==i) up[u]+=p*(down[v]+L[v]);
				else up[u]+=p*((down[v]*son[v])/(son[v]+1)+L[v]);
				p*=1.0/(son[v]+1);
			}
			p=0.5;
			for(int j=nxt[i];j!=i;j=nxt[j]){
				int v=stk[j];
				if(nxt[j]==i) up[u]+=p*(down[v]+R[v]);
				else up[u]+=p*((down[v]*son[v])/(son[v]+1)+R[v]);
				p*=1.0/(son[v]+1);
			}
			for(int t=first[u];t;t=e[t].nxt) if(!vis[e[t].v]) dfs_up(e[t].v,u,e[t].w);
		}
	}
	else{
		dfs_down(1,0);
		dfs_up(1,0,0);
	}	
	double ret=0;	
	for(int u=1;u<=n;++u){
		ans[u]=(down[u]*son[u]+up[u]*fa[u])/(son[u]+fa[u]);
		ret+=ans[u];
	}
	printf("%.5lf\n",ret/n);
	return 0;
}

三、

有些时候 \(f\) 的递推求解会互相调用对方,可以列出方程后解方程得到递推式完成。

这个方程可能比较简单,可以直接解出,然后就变成普通的递推了。也可能需要使用高斯消元或模拟高斯消元来解。

模拟高斯消元,就是利用方程矩阵的特殊形式手动消元优化复杂度。

例:[六省联考2017]分手是祝愿

首先,一个局面的最优策略是从后往前扫,如果当前位置为 \(1\)就进行操作。

这是因为前面的位置的操作一定不会再影响到这个位置了,所以必须进行这一次操作。同时也可以看出,对于任何一个局面,它的操作序列是固定的。

根据上面的策略,我们可已经每一个局面转化为1个 \(01\) 串。其中第 \(i\) 位表示是否要对这一位进行操作。

那么这个局面的最优操作次数就是 \(1\)的个数。因此,我们只关心一个 \(01\) 串中有多少个 \(1\) 而不关心这个串具体情况。

\(f[i]\) 表示串中有 \(i\)\(1\) 时的期望要走多少次才能变为只有 \((i-1)\)\(1\) 的情况。

\(\frac in\) 的概率选到 \(1\) 成功转化,也有 \(\frac {n-i}n\)的概率变为 \((i+1)\)\(1\),还需要再进行 \(f[i+1]+f[i]\) 次操作

\[f[i]=\frac{i}{n}+\frac{n-i}{n}(1+f[i+1]+f[i]) \]

解方程得到 \(f[i]=\frac{(n-i)f[i+1]+n}{i}\)\(ans=\sum_{i=k+1}^{cnt}f[i]+k\),其中 \(cnt\)为初始局面最优操作次数。

view code
#include<bits/stdc++.h>
using namespace std;
const int mod=100003;
const int N=1e5+10;
int n,k,w[N],f[N],inv[N];
vector<int> d[N];
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;++i) scanf("%d",&w[i]);
	int ans=0;
	for(int i=1;i<=n;++i)
		for(int j=i<<1;j<=n;j+=i) d[j].push_back(i);
	for(int i=n;i>=1;--i){
		if(w[i]){
			ans++;
			w[i]^=1;
			for(int v:d[i]) w[v]^=1;
		}
	}
	if(ans>k){
		f[n+1]=0;
		int ret=k;
		inv[0]=inv[1]=1;
		for(int i=2;i<=n;++i) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
		for(int i=n;i>=k+1;--i){
			f[i]=1ll*(n+1ll*(n-i)*f[i+1]%mod)%mod*inv[i]%mod;
			if(i<=ans) ret+=f[i];
		}
		ans=ret;
	}
	for(int i=1;i<=n;++i) ans=1ll*ans*i%mod; 
	printf("%d\n",ans);
	return 0;
}

例:CF1349D

\(E_i\) 表示游戏结束时 \(i\) 拥有所有饼干的情况的期望步数。则 \(ans=\sum_{i=1}^{n}E_i\)

直接算 \(E_i\) 非常难算,考虑 \(F_i\) 表示一个新的仅在 \(i\) 拥有所有饼干时才结束的游戏的期望步数,\(P_i\) 表示游戏结束时 \(i\)拥有所有饼干的概率。\(C\) 表示 \(i\) 拥有所有饼干后期望经过多少步才能使 \(j\) 拥有所有饼干。

显然对于所有 \(i,j\) 这个步数相同。

那么不难从 \(F_i\) 得到 \(E_i\),考虑枚举游戏终止时拥有所有饼干的人,考虑枚举游戏终止时拥有所有饼干的人:

\[E_i=F_i-\sum_{j=1,j\not=i}^{n}(P_jC+E_j) \]

\[\sum_{i=1}^{n}E_i=F_i-C*\sum_{j=1,j\not=i}^{n}P_j=ans \]

将所有的 \(n\) 个柿子加起来得到:

\[ n*ans=\sum_{i=1}^{n}F_i-C(n-1)\sum_{i=1}^{n}P_i=\sum_{i=1}^{n}F_i-C(n-1) \]

再求解 \(F_i\)\(C\) 时,我们实际上只关心每个饼干在第 \(i\) 个人手中还是不在了,

因此设 \(f_i\) 表示这个人现在有 \(i\) 个饼干,期望经过多少步后才能拥有所有饼干。于是 \(F_i=f_{a_i},C=f_0\)

对于 \(f\) 的求解,边界条件为 \(f_m=0\),容易写出递推式:

\[ f_i=1+\frac{m-i}{m}(\frac{n-2}{n-1}f_i+\frac{1}{n-1}f_{i+1})+\frac im f_{i-1}(i>0) \]

\[f_i=1+\frac{n-2}{n-1}f_0+\frac{1}{n-1} f_1(i=0) \]

到这里我们的问题就转换为解方程了,直接高斯消元复杂度太高,考虑到这些方程左右两侧所有系数的和都是 \(1\)

因此如果令 \(g_i=f_i-f_{i+1}\),然后将 \(f_i\) 替换为 \(\sum_{j=i}^m g_j\),那么 \(g_{i+1},g_{i+2},\dots g_n\)都会被抵消掉。

于是方程就只剩 \(g_i\)\(g_{i-1}\)了,直接递推即可。

化简后得到方程为

\[ g_i=\frac{i(n-1)g_{i-1}+m(n-1)}{m-i}(i>0) \]

\[g_0=n-1 \]

代码可能比我的题解还短。

view code
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=3e5+10;
int n,a[N],m,g[N],f[N],inv[N];
inline int add(int x,int y){return (x+y>=mod)?x+y-mod:x+y;}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),m+=a[i];
	g[0]=n-1;
	inv[0]=inv[1]=1;for(int i=2;i<=max(m,n);++i) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=1;i<m;++i)
		g[i]=1ll*add(1ll*i*(n-1)%mod*g[i-1]%mod,1ll*m*(n-1)%mod)*inv[m-i]%mod;
	for(int i=m-1;i>=0;--i) f[i]=add(f[i+1],g[i]);
	int ans=0;
	for(int i=1;i<=n;++i) ans=add(ans,f[a[i]]);
	ans=(ans-1ll*f[0]*(n-1)%mod+mod)%mod;
	printf("%d\n",1ll*ans*inv[n]%mod);
	return 0;
}

四、

利用概率生成函数求解。

对于任意一个取值在非负整数集上的离散随机变量\(X\),其概率生成函数为:

\[ F(z)=\sum_{i=0}^{\infty}P(X=i)z^i \]

重要性质:
1.\(F(1)=1\)
2.\(E(X)=F'(1)\):\(F'(z)=\sum_{i=0}^{\infty}(i+1)P(X=i+1)z^i\),代入\(1\)后正好是期望的定义。
3.\(E(X^{\underline{k}})=F^{(k)}(1)\)
4.\(E(X^k)=\sum_{i=0}^{k}\left\{\begin{matrix}k\\i\end{matrix}\right\}F^{(i)}(1)\)

例:[ZJOI2013]抛硬币

定义 \(f_i\) 表示扔了 \(i\) 次后游戏结束的概率,\(g_i\) 表示扔了 \(i\) 次后游戏没有结束的概率。

\(f\)\(g\) 建立生成函数 \(F\)\(G\) ,此时\(F\)\(PGF\)满足上面的性质,但\(G\)并不是,答案是\(F'(1)\)

可以得到方程:\(F(x)+G(x)=xG(x)+1\)

考虑在一个未完成的序列后增加一个\(A\)(设长为\(L\)),那么游戏一定会结束,但有可能在第\(i\)个位置提前结束,

那么此时\(A[1-i]\)必须是一个\(border\)并且此时之后扔出的\(L-i\)次的影响应当消去,求出\(a_i\)表示\(A[1\dots i]\)是否是\(border\)

\(G(x)P(A)=\sum_{i=1}^{L}F(x)P(A[i+1\dots n])a_i\),其中\(P(S)\)\(S\)整个串连续出现的概率。

解方程就完了。

view code
这题要写高精度,太恶心了,所以code咕掉了

例:[SDOI2017]硬币游戏

设这\(n\)个串为\(A_1\dots A_n\)

我们依然使用套路,定义\(f_{i,j}\)表示第\(i\)个串在扔\(j\)次硬币后结束游戏的概率,以及\(g_i\)表示扔\(i\)次硬币游戏没有结束的概率。

以此建出生成函数\(F_i(x)\)\(G(x)\),那么答案就是\(F_i(1)\)

显然有一个方程:\(\sum_{i=1}^{n}F_i(1)=1\)

依然考虑在未完成的字符串后直接添加第\(i\)个字符串,但此时我们不只要考虑是否提前结束了游戏,还有考虑是哪个字符串结束的。

定义\(a_{i,j,k}\),为\(1\)当且仅当\(A_i[1\dots k]=A_j[n-k+1\dots n]\),否则为\(0\),这个显然可以用\(hash \mathcal O(n^3)\)求出。

得到方程如下:

\[\frac{G(x)x^m}{2^m}=\sum_{j=1}^{n}\sum_{k=1}^{m} a_{i,j,k}F_j(x)\frac{x^{m-k}}{2^{m-k}} \]

\(x=1\)代入,于是最终我们得到了\(n\)个关于\(F_i(1)\)\(G(1)\)的方程,加上一开始那个一共\(n+1\)个,可以使用高斯消元完成,复杂度\(\mathcal O(n^3)\)

view code
#include<bits/stdc++.h>
using namespace std;
const int N=310;
const int mod=1e9+7;
const double eps=1e-10;
char s[N][N];
int n,m;
int a[N][N][N],pw[N],id[N];
double c[N][N],p[N],ans[N];
int hsh[N][N];
inline int dec(int x,int y){return (x-y<0)?x-y+mod:x-y;}
inline int gethsh(int i,int l,int r){
	return dec(hsh[i][r],1ll*pw[r-l+1]*hsh[i][l-1]%mod);
}
inline void init(){
	pw[0]=1;p[0]=1;
	for(int i=1;i<=m;++i) pw[i]=(pw[i-1]<<1)%mod,p[i]=p[i-1]*2;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j) hsh[i][j]=((hsh[i][j-1]<<1)+(s[i][j]=='H'))%mod;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			for(int k=1;k<=m;++k)
				a[i][j][k]=gethsh(i,1,k)==gethsh(j,m-k+1,m);
}	
inline void Gauss(){
	for(int i=1;i<=n+1;++i){
		if(c[i][i]>-eps&&c[i][i]<eps){
			for(int j=i+1;j<=n+1;++j)
				if(c[j][i]<-eps||c[j][i]>eps){swap(id[i],id[j]),swap(c[i],c[j]);break;}
		}
		for(int j=n+2;j>=i;--j) c[i][j]/=c[i][i];
		for(int j=i+1;j<=n+1;++j)
			for(int k=n+2;k>=i;--k)
				c[j][k]-=c[i][k]*c[j][i];
	}
	for(int i=n+1;i>=1;--i){
		c[i][i]=c[i][n+2]/c[i][i];ans[id[i]]=c[i][i];
		for(int j=i-1;j>=1;--j) c[j][n+2]-=c[j][i]*c[i][i];
	}
	for(int i=1;i<=n;++i) printf("%.10lf\n",ans[i]);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%s",s[i]+1);
	init(); 
	for(int i=1;i<=n+1;++i) id[i]=i;
	for(int i=1;i<=n;++i){
		c[i][n+1]=-1;
		for(int j=1;j<=n;++j)
			for(int k=1;k<=m;++k)
				if(a[i][j][k]) c[i][j]+=p[k];		
	}
	for(int i=1;i<=n;++i) c[n+1][i]=1;c[n+1][n+2]=1;
	Gauss();
	return 0;
}
posted @ 2021-04-12 09:07  cjTQX  阅读(155)  评论(0编辑  收藏  举报