概率期望小结
一个小技巧,求方差的期望时,有特殊的方法:
设 (n=|Omega|)
概率期望的核心知识点就在于求解概率/期望上,这里讲一些期望的常见求解方法:
一、
直接利用定义正向DP得到随机变量所有取值的出现概率,再利用定义计算期望
通常适用于随机的变量的取值有限的情况。
显然答案的最大值一定 (in[1,x]),于是我们可以转化为概率问题:
考虑求解 (P(ans<a)),也就是所有区间中都有 (<a) 的数的概率。
首先如果一个区间完全包含了另一个区间,那么只要满足后者,前者一定满足,于是可以去掉前者。
将剩下的区间按左端点从小到大排序,右端点一定单调递增。
此时再考虑任意一个点 (i),如果选择这个点的 (w_i<a),这样的选择发生的概率为 (p=dfrac{i-1}{x}) ,那么它会对所有覆盖它的区间打上标记,排序后这样的区间一定是连续的一段 ([l_i,r_i])。
设 (f_i) 表示选择了 (i) 这个点,并且第 (1-r_i) 个区间都已经被打上标记了。枚举上一个选择的点(j),转移即为:
思路就是上一个选择的 (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
",ans);
return 0;
}
二、
直接使用期望 (dp),设 (f_i) 表示当前状态是 (i),从当前状态到末状态这部分过程的答案期望。通常末状态的 (f) 为0,然后从末状态往初状态倒着递推。
考虑求出从每个点出发的路径边权和期望 (ans_i),然后得到答案。
对树上的情况,每个点出发的路径有两种,第一步先走到父亲,或者第一步走到某个儿子。分别用 (f_u) 和 (g_u) 来表示这两种情况的答案.
(g) 的递推显然:末状态就是叶子结点 (g_u=0)
其他点满足(g_u=frac {1}{|son_u|} sum_{vin son_u}g _v+w_{u,v})
设 (v)是 (u) 的父亲,那么
末状态为 (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
",ret/n);
return 0;
}
三、
有些时候 (f) 的递推求解会互相调用对方,可以列出方程后解方程得到递推式完成。
这个方程可能比较简单,可以直接解出,然后就变成普通的递推了。也可能需要使用高斯消元或模拟高斯消元来解。
模拟高斯消元,就是利用方程矩阵的特殊形式手动消元优化复杂度。
首先,一个局面的最优策略是从后往前扫,如果当前位置为 (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{(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
",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),考虑枚举游戏终止时拥有所有饼干的人,考虑枚举游戏终止时拥有所有饼干的人:
即
将所有的 (n) 个柿子加起来得到:
再求解 (F_i) 与 (C) 时,我们实际上只关心每个饼干在第 (i) 个人手中还是不在了,
因此设 (f_i) 表示这个人现在有 (i) 个饼干,期望经过多少步后才能拥有所有饼干。于是 (F_i=f_{a_i},C=f_0)。
对于 (f) 的求解,边界条件为 (f_m=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})了,直接递推即可。
化简后得到方程为
代码可能比我的题解还短。
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
",1ll*ans*inv[n]%mod);
return 0;
}
四、
利用概率生成函数求解。
对于任意一个取值在非负整数集上的离散随机变量(X),其概率生成函数为:
重要性质:
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{egin{matrix}k\iend{matrix}
ight}F^{(i)}(1))
定义 (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[1dots i])是否是(border)。
(G(x)P(A)=sum_{i=1}^{L}F(x)P(A[i+1dots n])a_i),其中(P(S))为(S)整个串连续出现的概率。
解方程就完了。
view code
这题要写高精度,太恶心了,所以code咕掉了
设这(n)个串为(A_1dots 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[1dots k]=A_j[n-k+1dots n]),否则为(0),这个显然可以用(hash mathcal O(n^3))求出。
得到方程如下:
将(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
",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;
}