概率期望基础

在概率论和统计学中,数学期望(mathematic expectation )(或均值,亦简称期望)是试验中每次可能结果的概率乘以其结果的总和,是最基本的数学特征之一。它反映随机变量平均取值的大小

期望具有线性性质,所以我们可以很方便的求解。

P4316 绿豆蛙的归宿

这题就是教你求期望,在 DAG 上,容易想到拓扑排序后跑 DP。

\(f_i\) 表示从起点到 \(i\) 点的期望,\(p_i\) 表示从起点到 \(i\) 点的概率。显然有状态转移方程

\[f_i=\frac{f_j+w_{i\to j}\cdot p_j}{out_j} \]

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1e5+10;
int n,m,tot,head[N],out[N],in[N];
struct edge{int v,next,w;}e[N<<1];
inline void add(int w,int v,int u){e[++tot]={v,head[u],w};head[u]=tot;out[u]++;in[v]++;}
double ans,f[N],p[N];
inline void topo(){
	std::queue<int> q;
	for(int i=1;i<=n;++i)if(!in[i])q.push(i);
	p[1]=1;
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=e[i].next){
			int v=e[i].v;
			f[v]+=(f[u]+e[i].w*p[u])/out[u];
			p[v]+=p[u]/out[u];
			if(!--in[v])q.push(v);
		}
	}
}
int main(){
// 	freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read(),m=read();
	for(int i=1;i<=m;++i){
		add(read(),read(),read());
	}
	topo();
	printf("%.2lf\n",f[n]);
}

P4206 [NOI2005] 聪聪与可可

提前处理好各个位置的猫的一二步走法,然后直接设 \(f_{i,j}\) 表示猫在 \(i\),老鼠在 \(j\) 的期望,有 \(f_{i,j}=\sum \frac{f_{x,y}}{du_j+1}+1\)\(y\) 表示老鼠的可能位置,\(x\) 表示老鼠在 \(y\) 时猫将到达的位置。
当猫能一步或两步到达时 \(f_{i,j}=1\),同点时 \(f_{i,j}=0\)

#include <bits/stdc++.h>//没写这道题,he的。
using namespace std;
const int N=2010;
const double eps=1e-8;
struct edge{
    int u,v;
}road[N];
int n,m,st,en;
int tot,first[N],nex[N],out[N];
int deep[N],vis[N],step[N][N];
double f[N][N];
queue <int> q;
void Add(int x,int y)
{
    nex[++tot]=first[x];
    first[x]=tot;
    road[tot].u=x;
    road[tot].v=y;
}
 
double DFS(int x,int y)     //记忆化搜索即可 
{
    if(x==y) return 0.0;        //已经抓到,期望为0 
    if(step[x][y]==y||step[step[x][y]][y]==y) return 1.0;   //下一步即可捉到,期望为1 
    if(!(fabs(f[x][y])<eps)) return f[x][y];     //已经算过 
    double sum=DFS(step[step[x][y]][y],y);	//原地不动 
    for(int i=first[y];i!=-1;i=nex[i])
        sum+=DFS(step[step[x][y]][y],road[i].v);	//枚举选择 
    return f[x][y]=sum/(out[y]+1.0)+1.0;        //可可可以不动,最后加1与扑克同理可证 
}
 
void Get_Step(int point)        //step[i][j]表示当可可在j位置聪聪在i位置时聪聪的选择 
{
    memset(vis,0,sizeof(vis));
    memset(deep,127,sizeof(deep));
    q.push(point);
    vis[point]=1;
    deep[point]=0;
    while(!q.empty()){ 
        int x=q.front();
        vis[x]=0; 
        for(int i=first[x];i!=-1;i=nex[i]){
            int to=road[i].v;
            if(!vis[to]&&deep[to]>deep[x]+1){	
                deep[to]=deep[x]+1;			//更新深度 
                step[to][point]=x;		//记录目标点 
                q.push(to);		//由于深度发生变化,重新入对 
                vis[to]=1;		//标记为已入队 
            }
            else{
                if(deep[to]==deep[x]+1)		
                    if(x<step[to][point]) step[to][point]=x;	//更新为下标更小的点 
            }
        }
        q.pop();
    }
}
 
int main()
{
    memset(first,-1,sizeof(first));
    scanf("%d%d",&n,&m);
    scanf("%d%d",&st,&en);
    int x,y;
    for(int i=1;i<=m;i++){
        scanf("%d%d",&x,&y);
        Add(x,y); Add(y,x); 
        out[x]++; out[y]++; //统计出度 
    }
    for(int i=1;i<=n;i++)    //枚举每个点,当成可可的位置进行BFS,求解step数组 
        Get_Step(i);
    printf("%.3lf",DFS(st,en));     //记忆化搜索答案易得 
    return 0;
} 

P1654 OSU!

\(f_i\) 表示到第 \(i\) 个位置时的期望,经过思考后发现因为取立方的缘故,不是很好转移。考虑将式子展开。

\((x+1)^3=x^3+3\cdot x^2+3\cdot x+1\) 不难发现只多了 \(3\cdot x^3+3\cdot x+1\) ,考虑将它们分别维护出来。

\(x1_i\) 表示 \(x\),设 \(x2_i\) 表示 \(x^2\),有
$x1_i=(x1_{i-1}+1)\cdot p_i $
\(x2_i=(x2_{i-1}+2\cdot x1_{i-1}+1)\cdot p_i\)
\(f_i=f_{i-1}+[3\cdot(x1_{i-1}+x2_{i-1})+1]\cdot p_i\)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 111111
#define int long long
using namespace std;
int n;
double p[maxn];
double x1[maxn],x2[maxn],ans[maxn];
signed main()
{
  scanf("%lld",&n);
  for(int i=1;i<=n;i++)
      scanf("%lf",&p[i]);
  for(int i=1;i<=n;i++){
      x1[i]=(x1[i-1]+1)*p[i];
      x2[i]=(x2[i-1]+2*x1[i-1]+1)*p[i];
      ans[i]=ans[i-1]+(3*(x1[i-1]+x2[i-1])+1)*p[i];
  }
  printf("%.1lf",ans[n]);
  return 0;
}

时间复杂度 \(O(n)\),观察上面的式子,发现它还可以拿矩阵快速幂来优化,故时间复杂度可以降到 \(O(n\log n)\)

本人的做法通过打表找到了一定的规律,发现每次贡献都会多一个系数为 \(6\) 的贡献,实际上原理一样,故只贴代码,不多赘述了。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1e5+10;
int n;
double p[N],f[N],zc,last,y;
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%lf",p+i);
	f[1]=1*p[1];last=1;zc=0;y=0;
	for(int i=2;i<=n;++i){
		last*=p[i-1];y*=p[i-1];y+=6*p[i-1]*(1-p[i-2]);
		zc*=p[i-1],zc+=y;
		f[i]=f[i-1]*(1-p[i])+p[i]*(f[i-1]+(1-p[i-1])+zc+last);
		last=(1-p[i-1])+zc+last;
	}
	printf("%.1lf",f[n]);
}

关于此题将 \(3\) 次方推广到 \(k\) 次方,要用到一些多项式的知识,感兴趣可以去看这里的一些讨论,此题还有一些多倍经验,如P1365 WJMZBMR打osu! / Easy Let's Play Osu!,做法大同小异,不多赘述。

BZOJ1419 Red is good

注意这里的最优策略指的是不知道牌的情况下平均全局最优策略,并不是每种情况拿最多。
\(f_{i,j}\) 表示要选 \(i\) 个红,\(j\) 个黑的时的最优期望,
显然有 \(f_{i,j}=\max \{0,(f_{i-1,j}+1)\cdot \frac{i}{i+j}+(f_{i,j-1}-1)\cdot \frac{j}{i+j}\}\)
当期望小于 \(0\) 时及时停止。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=5e3+10;
int a,b;
double f[N][N];
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	a=read(),b=read();
	int now=0;
	for(int i=0;i<=a;++i,now^=1,f[now][0]=i)
		for(int j=1;j<=b;++j)
			f[now][j]=std::max(0*1.0,(f[now^1][j]+1)*i/(i+j)+(f[now][j-1]-1)*j/(i+j));
	now^=1;
	printf("%.6lf\n",f[now][b]-5.0/1e7);
}

BZOJ3029 守卫者的挑战

数据看起来是跑不了 \(O(n^3)\) 的算法的,但是稍微观察不难发现对于所有的 \(k\geq n\) 都属于同一类数。
\(f_{i,j,k}\) 表示到了第 \(i\) 个挑战,赢了 \(j\) 次,当前背包容量为 \(k\) 时的概率。有

\[\begin{equation*} \begin{split} f_{i+1,j+1,k+a_{i+1}}=\sum_{k=-n}^n f_{i,j,k}\cdot p_{i+1}\\ f_{i+1,j,k}=\sum_{k=-n}^n f_{i,j,k}\cdot(1-p_{i+1}) \end{split} \end{equation*} \]

直接刷表就行了。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=205;
int n,l,K,a[N];
double p[N],f[N][N][N*2];
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read();l=read();K=std::min(read(),n);
	for(int i=1;i<=n;++i)p[i]=read()/100.0;
	for(int i=1;i<=n;++i)a[i]=read();
	f[0][0][K+n]=1;
	for(int i=0;i<n;++i){
		for(int j=0;j<=i;++j){
			for(int k=0;k<=n*2;++k){
				if(!f[i][j][k])continue;
				if(k+a[i+1]>=n*2){
					f[i+1][j+1][n*2]+=f[i][j][k]*p[i+1];
					f[i+1][j][k]+=f[i][j][k]*(1-p[i+1]);
				}else{
					f[i+1][j+1][k+a[i+1]]+=f[i][j][k]*p[i+1];
					f[i+1][j][k]+=f[i][j][k]*(1-p[i+1]);
				}
			}
		}
	}
	double ans=0;
	for(int i=l;i<=n;++i){
		for(int j=n;j<=2*n;++j)ans+=f[n][i][j];
	}
	printf("%.6lf\n",ans);
}

BZOJ2720 列队春游

不要定式思维,不要一眼 DP,考虑数学,然后比较平凡。
直接考虑每个人对答案的贡献,枚举人,枚举这个人看到的视野,然后做完了。时间复杂度 \(O(n^3)\),然后稍微处理一下一些东西可以 \(O(n^2)\),推会式子可以做到 \(O(n)\),懒得写了。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=305;
int n,a[N],temp[N],_len,big[N],sm[N];
double ans=0;
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read();
	for(int i=1;i<=n;++i)a[i]=temp[i]=read();
	std::stable_sort(temp+1,temp+n+1);_len=std::unique(temp+1,temp+n+1)-temp-1;
	for(int i=1;i<=n;++i)a[i]=std::lower_bound(temp+1,temp+_len+1,a[i])-temp;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			if(a[j]>=a[i])big[i]++;else sm[i]++;
		}
		big[i]--;
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			double p=1;int zc=sm[i];
			for(int k=1;k<=j-1;++k){
				ans+=k*p*big[i]/(n-k);
				p*=zc*1.0/(n-k);
				if(zc)zc--;
			}
			ans+=j*p;
		}
	}
	printf("%.2lf\n",ans/n);
}

BZOJ2969 矩形粉刷

跟上面一样吧,直接考虑每个格子的贡献,找出有多少种方案它会有贡献,画画图发现就是一个简单的容斥,平凡题。

#include<bits/stdc++.h>
#define int long long
typedef long double Ld;
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1e3+10;
int k,w,h,tot,a[N][N];
double ans=0;
signed main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	k=read(),w=read(),h=read();
	for(int i=1;i<=w;++i){
		for(int j=1;j<=h;++j){

			a[i][j]=i*j*(w-i+1)*(h-j+1)+i*(h-j+1)*j*(w-i+1)-i*(w-i+1)-j*(h-j+1)+1;
			a[i][j]=a[i][j]*2-1;
			a[i][j]=w*h*w*h-a[i][j];
		}
	}
	for(int i=1;i<=w;++i){
		for(int j=1;j<=h;++j){
			double p=1;
			for(int it=1;it<=k;++it){
				p*=1.0*a[i][j]/w/w/h/h;
			}
			ans+=1.0*(1-p);
		}
	}
	printf("%.0lf\n",ans);
}

P2059 [JLOI2013] 卡牌游戏

看着就是一个记忆化搜索(没写聪聪可可还是要在这题写记搜),直接设 \(f(tot,s)\) 表示省 \(tot\) 个人,第 \(s\) 个人为庄家时,这 \(tot\) 个人中没人获胜的概率。
显然有 \(f(tot,s)=\sum_{k=1}^m \frac{f(tot-1,new_s)}{m}\),比较平凡,注意然后按顺时针从庄家位置数第X个人将被处决即退出游戏,庄家算数的第一个。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=55;
int n,m,x[N];
double f[N][N][N];
bool vis[N][N];
inline void search(int tot,int s){
	vis[tot][s]=1;
	if(tot==1){f[tot][s][s]=1.0;return;}
	for(int i=1;i<=m;++i){
		int a=tot-1,c=((s+x[i]-1)%tot==0?tot:(s+x[i]-1)%tot);
		int _new=(c%a==0?a:c%a);
		if(!vis[a][_new]){search(a,_new);}
		for(int i=1;i<c;++i){f[tot][s][i]+=1.0/m*f[a][_new][i];}
		for(int i=c+1;i<=tot;++i){f[tot][s][i]+=1.0/m*f[a][_new][i-1];}
	}
}
int main(){
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read(),m=read();
	for(int i=1;i<=m;++i)x[i]=read();
	search(n,1);
	for(int i=1;i<=n-1;++i)printf("%.2lf%% ",f[n][1][i]*100);
	printf("%.2lf%%",f[n][1][n]*100);
}

P1850 [NOIP2016 提高组] 换教室

先拿 Floyd 处理好全源最短路,然后直接设 \(f_{i,j,0/1}\) 表示到第 \(i\) 节课,申请了 \(j\) 次,这一次申请与否的最小代价期望。分类讨论上一次在哪,显然有

\[\begin{equation*} \begin{split} f_{i,j,0}=\min&(f_{i-1,j,0+e_{c_{i-1}\to c_i}},(f_{i-1,j,1}+e_{c_{i-1}\to c_i})\cdot (1-p_{i-1})+(f_{i-1,j,1}+e_{d_{i-1}\to c_i})\cdot p_{i-1})\\ f_{i,j,1}=\min(\\&(f_{i-1,j-1,0}+e_{c_{i-1}\to c_i})\cdot (1-p_i)+(f_{i-1,j-1,0}+e_{c_{i-1}\to d_i})\cdot p_i,\\&(f_{i-1,j-1,1}+e_{c_i-1\to c_i})\cdot(1-p_i)\cdot (1-p_{i-1}))+\\& (f_{i-1,j-1,1}+e_{c_{i-1}\to d_i})\cdot p_i\cdot (1-p_{i-1})+\\& (f_{i-1,j-1,1}+e_{d_{i-1}\to c_i})\cdot (1-p_i)\cdot p_{i-1}+\\& (f_{i-1,j-1,1}+e_{d_{i-1}\to d_i})\cdot p_i\cdot p_{i-1}\\&) \end{split} \end{equation*} \]

直接转移即可。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=2e3+10;
int n,m,v,E,c[N],d[N],e[305][305];
double k[N],f[N][N][2];
inline void floyd(){
	for(int i=1;i<=v;++i)e[i][i]=0;
	for(int k=1;k<=v;++k)
		for(int i=1;i<=v;++i)
			for(int j=1;j<=v;++j)
				e[i][j]=std::min(e[i][j],e[i][k]+e[k][j]);
}
inline void work(){
	memset(f,0x43,sizeof(f));
	f[1][0][0]=f[1][1][1]=0;
	for(int i=2;i<=n;++i){
		for(int j=0;j<=m&&j<=i;++j){
			if(j<=i-1){
				f[i][j][0]=std::min(f[i][j][0],f[i-1][j][0]+e[c[i-1]][c[i]]);
				f[i][j][0]=std::min(f[i][j][0],(f[i-1][j][1]+e[c[i-1]][c[i]])*(1.0-k[i-1])+(f[i-1][j][1]+e[d[i-1]][c[i]])*k[i-1]);
			}
			if(!j)continue;
			double nw=(f[i-1][j-1][0]+e[c[i-1]][c[i]])*(1.0-k[i])+(f[i-1][j-1][0]+e[c[i-1]][d[i]])*k[i];
			f[i][j][1]=std::min(f[i][j][1],nw);
			nw=(f[i-1][j-1][1]+e[c[i-1]][c[i]])*(1.0-k[i])*(1.0-k[i-1])+
				(f[i-1][j-1][1]+e[c[i-1]][d[i]])*(1.0-k[i-1])*(k[i])+
				(f[i-1][j-1][1]+e[d[i-1]][c[i]])*(1.0-k[i])*(k[i-1])+
				(f[i-1][j-1][1]+e[d[i-1]][d[i]])*k[i]*k[i-1];
			f[i][j][1]=std::min(f[i][j][1],nw);
		}
	}
}
int main(){
// 	freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	scanf("%d%d%d%d",&n,&m,&v,&E);
	for(int i=1;i<=n;++i)scanf("%d",c+i);
	for(int i=1;i<=n;++i)scanf("%d",d+i);
	for(int i=1;i<=n;++i)scanf("%lf",k+i);
	memset(e,0x3f,sizeof(e));
	for(int i=1;i<=E;++i){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		e[u][v]=e[v][u]=std::min(e[u][v],w);
	}
	floyd();work();
	double ans=1e9;
	for(int i=0;i<=m;++i)ans=std::min({ans,f[n][i][0],f[n][i][1]});
	printf("%.2lf\n",ans);
}

P2473 [SCOI2008] 奖励关

male,从这道题开始才应该写题解,前面全是水题,好像就 red is good 值得写写,那我补了个几把的博客。
看到数据范围,考虑状压,但是我们不知道当前的物品是否应该拿(它对后面有影响),导致从这个思路出发很难转移,考虑逆推,设 \(f_{i,s}\) 表示选完了 \(i\)\(k\) 这些次,且 \(i\) 次之前的物品状态为 \(s\) 时的期望。
当宝物能取时,有 \(f_{i,s}+=\max(f_{i+1,s},f_{i+1,s|(1<<k-1)}+p_k)\cdot \frac{1}{n}\)
不能取就直接从不去转移即可。
关于此题的逆推,如果正推的话,我们甚至都不知道每次的状态能否到达,并且还需要处理概率问题,比较棘手。
因为一个状态到后面状态的可能性是均分的,但是由从其他状态导入后一个状态时,由于题目条件的约束,其他状态的各个概率并不是均分的。而且这题的条件约束是个乱七八糟的集合,毫无规律,所以正推不了
但如果是逆推的话,保证了转移时的概率和为 \(1\),还可以做到最有决策。遇到期望DP,往往考虑逆推是一个不错的选择。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1<<16;
int n,k,p[N],st[20],maxn,_p[20];
double f[105][N];
int main(){
// 	freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	k=read(),n=read();
	maxn=(1<<n)-1;
	for(int i=1;i<=n;++i){
		p[i]=read();int zc=read();
		while(zc){st[i]|=(1<<zc-1);zc=read();}
	}
	for(int i=k;i;--i){
		for(int s=0;s<=maxn;++s){
			for(int k=1;k<=n;++k){
				if((s&st[k])==st[k]){
					f[i][s]+=std::max(f[i+1][s],f[i+1][s|(1<<k-1)]+p[k])*1.0/n;
				}else f[i][s]+=f[i+1][s]*1.0/n;
			}
		}
	}
	printf("%.6lf\n",f[1][0]);
}

P4284 [SHOI2014] 概率充电器

很难想到的换根DP神仙题(其实是一个树形DP up and down 的trick)。
考虑每一个点怎样来电

  • 自己来电了
  • 儿子给它电了
  • 父亲给它电了

因为儿子父亲都可以给电,很那难来转移,首先考虑暴力,设 \(f_i\) 表示一个点在前两个情况下来电的概率,不难发现如果我们钦定一个根的话,这个根没有父亲,所以只根据前两种情况我们可以求出根的正确答案,设初始状态 \(f_i=q_i\) 有状态转移方程 \(f_i+=f_i+f_{son}\cdot p_e-f_i\cdot f_{son}\cdot p_e\),(有一个小容斥),时间复杂度 \(O(n^2)\),只能拿到30分,考虑如何通过 \(f_i\) 处理出来真正的答案。
一个点真正的答案为三个情况来电的概率和,对于一个儿子,我们可以处理出来它的父亲不从这个儿子来电的概率为 \(pa\),设 \(zc=f_{son}\cdot p_e\),观察上面的式子,有 \(f_{fa}=pa+zc-pa\cdot zc\),所以 \(pa=\frac{f_{fa}-w}{1-w}\),所以儿子的真正答案为 \(f_{son}+pa\cdot p_e-f_{son}\cdot pa\cdot p_e\)。跑一遍换根即可。
大体来讲就是我们需要处理出来父亲不需要这个儿子时来电的概率,然后依靠这个来处理出父亲给这个儿子供电的概率,从而处理出来儿子的真正答案。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=5e5+10;
int n,tot,head[N];
double p[N],f[N];
struct EDGE{int v,next;double p;}e[N<<1];
inline void add(int u,int v,double p){e[++tot]={v,head[u],p/100},head[u]=tot;}
inline void dfs1(int x,int fa){
	for(int i=head[x];i;i=e[i].next){
		int y=e[i].v;
		if(y!=fa){
			dfs1(y,x);
			double zc=f[y]*e[i].p;
			f[x]=f[x]+zc-f[x]*zc;
		}
	}
}
inline void dfs2(int x,int fa){
	for(int i=head[x];i;i=e[i].next){
		int y=e[i].v;
		if(y!=fa){
			double zc=f[y]*e[i].p;
			if(zc!=1){
				double pa=e[i].p*(f[x]-zc)/(1-zc);
				f[y]=f[y]+pa-f[y]*pa;
			}
			dfs2(y,x);
		}
	}
}
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	n=read();
	for(int i=1;i<n;++i){
		int u,v;double _p;
		u=read(),v=read(),_p=read();
		add(u,v,_p);add(v,u,_p);
	}
	for(int i=1;i<=n;++i)p[i]=read(),p[i]/=100,f[i]=p[i];
	double ans=0;
	dfs1(1,0);dfs2(1,0);
	for(int i=1;i<=n;++i)ans+=f[i];
	printf("%.6lf\n",ans);
}

感觉这题的转化确实比较牛。

P3232 [HNOI2013] 游走

边数太多,不好求,所以求点,设每个点的的期望经过次数为 \(f_i\),显然有 \(f_i=\sum_{k\in i} \frac{f_k}{d_k}(k\in i)\),特别的,对于节点 \(1\),有 \(f_1-1=\sum_{k\in i} \frac{f_k}{d_k}\),这个式子显然有后效性,直接拿高斯消元解就行,解完之后对于每条边就是 \(e_{i\to j}=\frac{f_i}{d_i}+\frac{f_j}{d_j}\),排完序后就完了,时间复杂度 \(O(n^3)\)

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=505;
int n,m,out[N];
double a[N][N],x[N],v[N*N];
std::vector<int> e[N];
std::pair<int,int> p[N*N];
inline double Abs(double x){return x<0?-x:x;}
inline bool ep(double x,double y){return Abs(x-y)<1e-9;}
inline void GUASS(){
	int line=1;
	for(int k=1;k<n;++k){
		int maxi=line;
		for(int i=line+1;i<n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
		if(ep(a[maxi][k],0))continue;
		for(int i=1;i<=n;++i)std::swap(a[line][i],a[maxi][i]);
		for(int i=1;i<n;++i){
			if(i==line)continue;
			double mul=a[i][k]/a[line][k];
			for(int j=k;j<=n;++j)a[i][j]-=a[line][j]*mul;
		}
		++line;
	}
	for(int i=1;i<n;++i)x[i]=a[i][n]/a[i][i];
}
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read(),m=read();
	for(int i=1;i<=m;++i){
		int u=read(),v=read();
		out[u]++,out[v]++;
		p[i]={u,v};
		e[u].push_back(v),e[v].push_back(u);
	}
	for(int i=1;i<n;++i){
		if(i==1)a[i][n]=-1;
		a[i][i]=-1;
		for(int x:e[i]){
			if(x!=n)a[i][x]=1.0/out[x];
		}
	}
	GUASS();
	for(int i=1;i<=m;++i){
		int u=p[i].first,w=p[i].second;
		if(u!=n)v[i]+=x[u]/out[u];
		if(w!=n)v[i]+=x[w]/out[w];
	}
	std::stable_sort(v+1,v+m+1);
	double ans=0;
	for(int i=1;i<=m;++i){
		ans+=v[i]*(m-i+1);
	}
	printf("%.3lf\n",ans);
}

P3211 [HNOI2011] XOR和路径

这题跟上一题虽然比较像,但不可定式思维。
直接考虑的话发现无法进行异或计算,不如直接考虑最终答案的每一位为 \(1\) 的概率,按位考虑每个节点到终点的概率 \(f_i\),设边权的这一位为 \(w\),分为两种情况

\[\begin{split} &f_i=\sum_{k\in i\&w_{i\to k}=0} f_k\cdot \frac{1}{d_i}\\ &f_i=\sum_{k\in i\&w_{i\to k}=1} (1-f_k)\cdot \frac{1}{d_i} \end{split} \]

随便导下式子然后直接拿高斯消元做就行,最后答案就是每位是 \(1\) 的贡献乘上概率就行。

#include<bits/stdc++.h>
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=105;
int n,m,head[N],tot,out[N];
double a[N][N],x[N],w[N*N*2];
struct EDGE{int u,v,next,w;}e[N*N*2];
inline void add(int u,int v,int w){e[++tot]={u,v,head[u],w};head[u]=tot;}
namespace GUASS{
	inline double Abs(double x){return x<0?-x:x;}
	inline bool ep(double x,double y){return Abs(x-y)<1e-9;}
	inline void sol(){
		int line=1;
		for(int k=1;k<n;++k){
			int maxi=line;
			for(int i=line+1;i<n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
			if(ep(a[maxi][k],0))continue;
			for(int i=1;i<=n;++i)std::swap(a[line][i],a[maxi][i]);
			for(int i=1;i<n;++i){
				if(i==line)continue;
				double mul=a[i][k]/a[line][k];
				for(int j=k;j<=n;++j)a[i][j]-=a[line][j]*mul;
			}
			++line;
		}
		for(int i=1;i<n;++i)x[i]=a[i][n]/a[i][i];
	}
}
inline void clear(){
	for(int i=1;i<=n;++i){
		x[i]=0;
		for(int j=1;j<=n;++j)a[i][j]=0;
	}
}
int main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read(),m=read();double ans=0;
	for(int i=1;i<=m;++i){
		int u=read(),v=read(),w=read();
		add(u,v,w);if(u!=v)add(v,u,w);
		out[u]++;if(u!=v)out[v]++;
	}
	for(int j=1;j<=30;++j){
		clear();
		for(int i=1;i<n;++i){
			for(int it=head[i];it;it=e[it].next){
				int v=e[it].v;
				if(e[it].w&(1<<j-1)){
					if(v!=n){
						a[i][v]+=-1.0/out[i];
						a[i][n]+=-1.0/out[i];
					}else a[i][n]+=-1.0/out[i];
				}
				else{
					if(v!=n)a[i][v]+=1.0/out[i];
				}
			}
			a[i][i]+=-1;
		}
		GUASS::sol();
		ans+=pow(2,j-1)*x[1];
	}
	printf("%.3lf\n",ans);
}

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

这篇题解比较详细,想解释如何去想到这样做。
首先考虑暴力,一个状态为一个节点,将他们都连接起来,直接拿高斯消元做,时间复杂度 \(O(2^{3n})\),这是不能接受的,不难发现这些节点只有 \(n\) 类,第 \(i\) 类表示需要 \(i\) 次操作可以全部关灯,第 \(i\) 类进行一次操作有 \(\frac{i}{n}\) 的概率向更少操作次数的状态转移,同理,有 \(\frac{n-i}{n}\) 的概率向更多次数的状态转移。对于 \(f_i\) 显然有 \(f_i=f_{i-1}\cdot \frac{n-i+1}{n}+f_{i+1}\cdot \frac{i+1}{n}\),此时进行高斯消元,时间复杂度为 \(O(n^3)\)
仔细观察这个式子,发现对于每个 \(f_i\) 的方程,只有 \(f_{i-1},f_i,f_{i+1}\) 的系数不为 \(0\),考虑这样一个矩阵。

\[\begin{bmatrix} -1&\frac{2}{n}&0&0&0\cdots&0\\ \frac{n-1}{n}&-1&\frac{3}{n}&0&0\cdots&0\\ 0&\frac{n-2}{n}&-1&\frac{4}{n}&0\cdots&0\\ 0&0&\frac{n-3}{n}&-1&\frac{5}{n}\cdots&0\\ \cdots&\cdots&\cdots&\cdots&\cdots&\cdots& \end{bmatrix} \]

最后的 \(-1\) 根据需要插在起点处,将这些式子加起来显然有 \(f_1\cdot\frac{1}{n}=-1\),然后逐一代入解就行了。
对于 \(k\),直接删去前 \(k\) 行就行,同理。
显然最后有 \(ans=\sum_{i=k}^n f_i\)

#include<bits/stdc++.h>
#define int long long
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1e5+10,mod=1e5+3;;
bool a[N];
int n,x,k,ans=0,jc[N],e[N],v[N];
struct EQ{int x1,x2,x3,k1,k2,k3,c;}eq[N];
inline int qpow(int a,int b){
	if(a==0)return 0;
	int zc=a;
	int res=1;
	for(;b;b>>=1,a=a*a%mod)if(b&1)res=res*a%mod;
	return res;
}
inline int ny(int x){return qpow(x,mod-2);}
inline int C(int x,int y){return jc[x]*ny(jc[y])%mod*ny(jc[x-y])%mod;}
inline int mo(int x){if(x>=0&&x<mod)return x;return (x%mod+mod)%mod;}
inline void work(){
		e[1]=n;
		for(int i=2;i<=n;++i){
			e[i]=mo(C(n,i-1)*n-e[i-1]);
		}
		for(int i=k+2;i<=n;++i){
			if(i>k+1)eq[i]={i-1,i,i+1,mo(e[i]*ny(e[i-1]+e[i])),mo(-1),mo(e[i+1]*ny(e[i+1]+e[i+2]))};
		}
		int s=k+1;
		eq[x].c=mo(-1);ans=k;
		eq[s]={0,s,s+1,0,mo(-1),mo(e[s+1]*ny(e[s+1]+e[s+2]))};
		v[s]=mo(-1*ny(eq[s].k2+eq[s+1].k1));ans=mo(ans+v[s]);
		for(int i=s;i<n;++i){
			eq[i].c=mo(eq[i].c-eq[i].k1*v[i-1]-eq[i].k2*v[i]);
			v[i+1]=mo(eq[i].c*ny(eq[i].k3));
			ans=mo(ans+v[i+1]);
		}
}
signed main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read(),k=read();jc[0]=1;
	for(int i=1;i<=n;++i)a[i]=read(),jc[i]=jc[i-1]*i%mod;
	for(int i=n;i;--i){
		if(a[i]){
			x++;
			for(int j=1;j*j<=i;++j){
				if(i%j==0){
					a[j]^=1;
					if(j*j!=i)a[i/j]^=1;
				}
			}
		}
	}
	if(x<=k)ans=x;else work();
	std::cout<<ans*jc[n]%mod<<'\n';
}

P3706 [SDOI2017] 硬币游戏

神仙题,不会。
有一个显然的暴力,直接建出AC自动机跑随机游走,时间复杂度 \(O(n^3m^3)\),能拿 50 分。
显然每个同学的串出现概率是一样的,为什么获胜概率不一样呢,有这样的情况,在接上这个同学的串的时候,有一位同学把他截胡了。
具体来说,我没把一个没有人获胜的串(任意长度)看做 S,它出现的概率和为 \(w\),每个人获胜的概率为 \(f_i\),看起来有 \(f_i=\frac{w}{2^m}\),但是这个式子并不成立,原因在于会有别的同学将它截胡,就是非法串的后缀加上第 \(i\) 个同学的串的前缀组成了另一个同学的串。有 \(f_i+\sum_{j=1}^n\sum_{k=1\&pre_{i,m-k}=st_{j,k}}^{m-1}f_j\cdot\frac{1}{2^{m-k}}=\frac{w}{2^m}\),自己也可能将自己截胡,\(f_j\cdot\frac{1}{2^{m-k}}\) 表示截胡的同学获胜后继续接上 \(i\) 同学前 \(m-k\) 个数的概率,随便整一个字符串匹配,然后直接上高斯消元就行。

#include<bits/stdc++.h>
#define int long long
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=305,base=131,mod1=998244353,mod2=1e9+7;
int n,m,P1[N],P2[N];
double f[N],a[N][N],_p[N];
std::pair<int,int>has[N][N];
char s[N];
inline double Abs(double x){return x<0?-x:x;}
inline void hs(int i,int j){has[i][j]={((has[i][j-1].first*base+s[j]-'A')%mod1+mod1)%mod1,((has[i][j-1].second*base+s[j]-'A')%mod2+mod2)%mod2};}
inline std::pair<int,int> Hs(int i,int l,int r){return {((has[i][r].first-has[i][l-1].first*P1[r-l+1])%mod1+mod1)%mod1,((has[i][r].second-has[i][l-1].second*P2[r-l+1])%mod2+mod2)%mod2};}
inline void GUASS(){
	int line=1;
	for(int k=1;k<=n;++k){
		int maxi=line;
		for(int i=line+1;i<=n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
		for(int i=1;i<=n+1;++i)std::swap(a[line][i],a[maxi][i]);
		for(int i=1;i<=n;++i){
			if(i==line)continue;
			double mul=a[i][k]/a[line][k];
			for(int j=k;j<=n+1;++j)a[i][j]-=a[line][j]*mul;
		}
		line++;
	}
	for(int i=1;i<=n;++i)f[i]=a[i][n+1]/a[i][i];
}
signed main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m;
	P1[0]=1;for(int i=1;i<=m;++i)P1[i]=P1[i-1]*base%mod1;
	P2[0]=1;for(int i=1;i<=m;++i)P2[i]=P2[i-1]*base%mod2;
	_p[0]=1;for(int i=1;i<=m;++i)_p[i]=_p[i-1]*1.0/2.0;
	for(int i=1;i<=n;++i){
		std::cin>>s+1;
		for(int j=1;j<=m;++j)hs(i,j);
	}
	for(int i=1;i<=n;++i){
		a[i][1]=-_p[m];
		for(int j=1;j<=n;++j){
			for(int k=1;k<m;++k){
				if(Hs(i,1,k)==Hs(j,m-k+1,m)){
					a[i][j+1]+=_p[m-k];
				}
			}
		}
	}
	n++;
	for(int i=1;i<=n;++i)a[i][i+1]+=1;
	for(int i=2;i<=n;++i)a[n][i]=1;a[n][n+1]=1;
	GUASS();
	for(int i=2;i<=n;++i)printf("%.10lf\n",f[i]);
}

钦定一个根后,对于从 \(i\)\(j\) 的期望步数,有 \(f_{i\to j}=\frac{1}{d_i}+\sum_{k\in i\& k!=j}\frac{1+f_{k\to i}+f_{i\to j}}{d_i}\),化简一下有 \(f_{i\to j}=d_i+\sum_{k\in i\&k!=j}f_{k\to i}\),显然一个叶子到它的父亲的期望步数为 \(1\),所以进行一次 dfs 可以从下往上算出每个点去父亲的期望步数,同理,再次依据上面的式子进行一遍 dfs 可以求出每个点到每个与它相邻的点的期望步数,最后处理一下树上前缀和直接做就行了。
感觉这题没啥精度,全是整数。

#include<bits/stdc++.h>
#define int long long 
inline int read(){
	char ch=getchar();int x=0,f=1;
	for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=1e5+10,mod=1e9+7;
int n,m,f[N],d[N],up[N],down[N],st[N][20],dfn[N],tot;
std::vector<int> e[N],v[N];
inline int mo(int x){if(x>=0&&x<mod)return x;if(x<0)return (x%mod+mod)%mod;return x%mod;}
inline int qpow(int a,int b){
	int res=1;
	for(;b;b>>=1,a=mo(a*a))if(b&1)res=mo(res*a);
	return res;
}
inline void dfs(int x,int fa){
	dfn[x]=++tot;st[tot][0]=fa;
	for(int y:e[x]){
		if(y!=fa){dfs(y,x);f[x]=mo(f[x]+f[y]);}
	}
	f[x]=mo(f[x]+d[x]);
}
inline void dfs1(int x,int fa,int last){
	int sum=d[x];
	for(int y:e[x]){
		if(y!=fa)sum=mo(sum+f[y]);
		else sum=mo(sum+v[fa][last]);
	}
	for(int w=0;w<e[x].size();++w){
		int y=e[x][w];
		if(y==fa){v[x].push_back(f[x]);}
		else v[x].push_back(mo(sum-f[y])),dfs1(y,x,w);
	}
}
inline void dfs2(int x,int fa){
	for(int w=0;w<e[x].size();++w){
		int y=e[x][w],val=v[x][w];
		if(y!=fa){
			up[y]=f[y]+up[x];
			down[y]=val+down[x];
			dfs2(y,x);
		}
	}
}
inline int get(int x,int y){return dfn[x]<dfn[y]?x:y;}
inline int lca(int u,int v){
	if(u==v)return u;
	if((u=dfn[u])>(v=dfn[v]))std::swap(u,v);
	int k=std::__lg(v-u++);
	return get(st[u][k],st[v-(1<<k)+1][k]);
}
inline int ask(int u,int v){
	int LCA=lca(u,v);
	return up[u]-up[LCA]+down[v]-down[LCA];
}
signed main(){
	// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
	std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
	n=read();m=read();
	for(int i=1;i<n;++i){
		int u=read(),v=read();e[u].push_back(v),e[v].push_back(u);
		d[u]++,d[v]++;
	}
	dfs(1,0);dfs1(1,0,0);dfs2(1,0);
	for(int k=1;k<=std::__lg(n);++k)
		for(int i=1;i+(1<<k)-1<=n;++i)
			st[i][k]=get(st[i][k-1],st[i+(1<<k-1)][k-1]);
	for(int i=1;i<=m;++i){
		int u=read(),v=read();
		std::cout<<mo(ask(u,v))<<'\n';
	}
}
posted @ 2024-05-24 15:04  Ishar-zdl  阅读(184)  评论(9)    收藏  举报