ybtAu「动态规划」第1章 期望概率 DP

这是 neatisaac 的金牌导航题解!

A. 【例题1】期望分数

\(X_i\) 表示到第 \(i\) 个位置的连续 x 串期望长度,\(f_i\) 表示到第 \(i\) 个位置的分数,\(p_i\) 为第 \(i\) 个位置的成功率,有:

\[\large E(f_i)=p_i(E(f_{i-1})+E(X_i^3-X_{i-1}^3))+(1-p_i)E(f_{i-1}) \\ =E(f_{i-1})+p_iE(X_i^3-x_{i-1}^3) \]

解释一下,如果成功则 x 串期望长度变长,分数有一定的增加,否则分数不变。
现在考虑如何求 \(E(X_i^3)\)
为什么不是 \(E(X_i)^3\) 呢?因为期望乘法公式 \(E(XY)=E(X)E(Y)\) 要求 \(X\)\(Y\) 相互独立,而显然 \(X_i\)\(X_i\) 并不是相互独立的。
考虑 x 串长度增加 \(1\)\(X^3\) 带来的影响:

\[\large (X+1)^3-X^3=3X^2+3X+1 \]

因此:

\[\large E(X_i^3)=p_i(E(X_{i-1}^3)+3E(X_{i-1}^2)+3E(X_{i-1})+1) \]

\(E(X^2)\) 也不能直接乘,需要单独求:

\[\large E(X_i^2)=p_i(E(X_{i-1}^2)+2E(X_{i-1})+1) \]

最后我们需要求 \(E(X)\)

\[\large E(X_i)=p_i(E(X_{i-1})+1) \]

于是问题得解。

#include <iostream>
#include <cstdio>
#define N 100005
int n;
double ex2[N],ex[N],f[N],p[N];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n;
	for(int i=1;i<=n;i++)
	{
		std::cin>>p[i];
		ex[i]=p[i]*(ex[i-1]+1);
		ex2[i]=p[i]*(ex2[i-1]+2*ex[i-1]+1);
		f[i]=f[i-1]+p[i]*(3*ex2[i-1]+3*ex[i-1]+1);
	}
	printf("%.1lf",f[n]);
}

B. 【例题2】选书问题

大力推式子。
\(len_i\) 表示第 \(i\) 个人的列表长度。对于第 \(i\) 个人的第 \(j\) 本书,我们选它的概率为:

\[\large P_{i,j}=\sum_{k=0}^{+\infty}(1-p)^{j-1+klen_i}p=\frac{p(1-p)^{j-1}}{1-(1-p)^{len_i}} \]

\(id_{i,j}\) 表示第 \(i\) 个人第 \(j\) 本书的编号,于是我们要求:

\[\large \sum_{i=1}^{n}\sum_{j=1}^{i-1}\sum_{x=1}^{len_i}\sum_{y=1}^{len_j}P_{i,x}P_{j,y}[id_{i,x}<id_{j,y}] \\ =\sum_{i=1}^{n}\sum_{x=1}^{len_i}P_{i,x}\sum_{j=1,id_{j,y}>id_{i,x}}^{i-1}P_{j,y} \]

发现最后那一项可以用树状数组来维护。
于是有如下做法:从小到大枚举每一个人,再枚举每一本书,在树状数组中查询 \(P\) 的后缀和计入答案,最后把这个点的 \(P\) 插入树状数组。注意对每个人要把他的书从小到大排序,以避免同一个人的不同书的 \(P\) 计入答案。
注意开 long double。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <algorithm>
#define N 500005
int n,m;
std::vector<int> a[N];
long double p,ans,len[N];
struct BIT
{
	long double f[N],r;
	void c(int x,long double t) {for(;x<=n;x+=x&-x) f[x]+=t;}
	long double q(int x) {for(r=0;x;x-=x&-x) r+=f[x];return r;}
} tr;
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	double t;
	std::cin>>n>>m>>t;
	p=t;
	for(int i=1;i<=n;i++) len[i]=1;
	for(int i=1,u,v;i<=m;i++)
	{
		std::cin>>v>>u;
		len[v]*=1-p,a[v].push_back(u);
	}
	for(int i=1;i<=n;i++)
	{
		std::sort(a[i].begin(),a[i].end());
		long double tmp=1;
		for(int j=0;j<a[i].size();j++)
		{
			long double tp=p*tmp/(1-len[i]);
			tmp*=1-p;
			ans+=tp*(tr.q(n)-tr.q(a[i][j]));
			tr.c(a[i][j],tp);
		}
	}
	t=roundl(ans*100)/100;
	printf("%.2lf",t);
}

C. 【例题3】乘坐电梯

\(f_{i,j}\) 表示到了第 \(i\) 秒,已经上了 \(j\) 个人的概率。
于是有:

\[\large f_{i+1,j+1}\leftarrow p*f_{i,j} \\ f_{i+1,j}\leftarrow (1-p)*f_{i,j} \]

注意边界:\(\large f_{i+1,n}\leftarrow f_{i,n}\)
做完了。

#include <iostream>
#define N 2005
int n,t;
double p,f[N][N],ans;
int main()
{
	std::cin>>n>>p>>t;
	f[0][0]=1;
	for(int i=0;i<t;i++)
	{
		f[i+1][n]+=f[i][n];
		for(int j=0;j<n;j++)
		{
			f[i+1][j+1]+=p*f[i][j];
			f[i+1][j]+=(1-p)*f[i][j];
		}
	}
	for(int i=0;i<=n;i++) ans+=f[t][i]*i;
	std::cout<<ans;
}

D. 【例题4】图上游走

显然,我们要让期望走过次数更多的边编号更小,于是我们求每条边的期望走过次数。
由于边数可能达到 \(O(n^2)\) 级别,所以直接求边的期望走过次数是行不通的。于是我们求每个点的期望走过次数。
\(P_u\) 表示节点 \(u\) 的期望走过次数,\(deg_u\) 表示节点 \(u\) 的度数,于是有:

\[\large E(u)=\sum_{v\rightarrow u,v\neq n}\frac{E(v)}{deg_u}(u\neq 1) \\ \large E(u)=\sum_{v\rightarrow u,v\neq n}\frac{E(v)}{deg_u}+1(u=1) \]

\(u=1\)\(+1\) 是因为 \(1\) 是我们的起点,无论如何都是要走一次的。
\(v\neq n\) 是因为走到 \(n\) 就不能再走回来了。
如果这个图是个 DAG,这题就做完了,可惜不是。这是一个无向连通图,这意味着会有环,这时我们就得不到一个令人满意的递推顺序了(记忆化搜索也不行)。于是我们使用高斯消元。
由于这一章并不是高斯消元,所以这里不展开讲高斯消元。
求解完每个点的期望经过次数之后,就可以求每条边的期望经过次数了。注意特判边的端点为 \(n\) 的情况

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#define N 505
std::pair<int,int> e[N*N];
int n,m,hed[N*N],tal[N*N],nxt[N*N],cnte;
double a[N][N],f[N*N],ans,deg[N];
void adde(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void gauss()
{
	int r;
	for(int i=0;i<n;i++)
	{
		r=i;
		for(int j=i+1;j<n;j++) if(fabs(a[j][i])>fabs(a[r][i])) r=j;
		if(r!=i) for(int j=0;j<=n;j++) std::swap(a[r][j],a[i][j]);
		for(int k=i+1;k<n;k++)
		{
			double g=a[k][i]/a[i][i];
			for(int j=i;j<=n;j++) a[k][j]-=g*a[i][j];
		}
	}
	for(int i=n-1;i>=0;i--)
	{
		for(int j=i+1;j<n;j++)
			a[i][n]-=a[j][n]*a[i][j];
		a[i][n]/=a[i][i];
	}
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m;
	for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,adde(u,v),adde(v,u),deg[u]++,deg[v]++,e[i]={u,v};
	for(int i=1;i<=n;i++)
	{
		for(int j=hed[i];j;j=nxt[j]) if(tal[j]!=n)
			a[i-1][tal[j]-1]=1.0/deg[tal[j]];
		a[i-1][i-1]=-1;
	}
	a[0][n]=-1;
	gauss();
	for(int i=1;i<=m;i++)
	{
		int u=e[i].first,v=e[i].second;
		f[i]=a[u-1][n]/deg[u]+a[v-1][n]/deg[v];
		if(u==n) f[i]-=a[u-1][n]/deg[u];
		if(v==n) f[i]-=a[v-1][n]/deg[v];
		f[i]=-f[i];
	}
	std::sort(f+1,f+m+1);
	for(int i=1;i<=m;i++) ans+=-f[i]*i;
	printf("%.3lf",ans);
}

E. 彩色圆环

需要一个很逆天的状态设计。
我们令 \(f_{i,0}\) 表示一个长 \(i\) 的段,第一个位置的颜色跟第二个位置不一样,且跟最后一个位置一样(0)或不一样(1)的期望美观程度,令 \(P_i\) 表示连续 \(i\) 个同色的概率。
于是有:

\[\large f_{i,0}=\sum_{j=1}^{i-1}P_jj(\frac{m-2}{m}f_{i-j,0}+\frac{m-1}{m}f_{i-j,1}) \\ f_{i,1}=\sum_{j=1}^{i-1}P_jj(\frac{1}{m}f_{i-j,0}) \]

但是我们还没有做完。要求环上美观程度的期望。
实际上,此“第一个位置”并不一定是第一个位置,而是一段连续相同颜色段压缩后的结果。
于是可以枚举 \(i\)。对于每一个 \(i\),环被分为两部分:“第一个位置”,以及一段可以直接用 \(f_{n-i+1,0}\) 来表示其贡献的部分。
对于“第一个位置”,它的贡献就是 \(P_ii\)
于是答案为:

\[\large \sum_{i=1}^{n}iP_ii\cdot f_{n-i+1,0} \]

由于我们的 \(f_{i,j}\) 并没有考虑 \(i=0\) 的情况,即整个环都是同一种颜色,而这种情况是要计入答案的,所以最终答案还需要加上 \(P_nn\)

为什么是这样呢?只能说:

悠然心会,妙处难与君说。

其实 neatisaac 也不知道

#include <cstdio>
#define N 205
int n,m;
double f[N][2],p[N],ans;
int main()
{
	scanf("%d%d",&n,&m);
	p[1]=1;
	for(int i=2;i<=n;i++) p[i]=p[i-1]*1.0/m;
	f[1][1]=1;
	for(int i=1;i<=n;i++) for(int j=1;j<i;j++)
	{
		f[i][0]+=p[j]*j*(f[i-j][0]*(m-2)*1.0/m+f[i-j][1]*(m-1)*1.0/m);
		f[i][1]+=p[j]*j*f[i-j][0]*1.0/m;
	}
	ans=n*p[n];
	for(int i=1;i<n;i++) ans+=i*i*f[n-i+1][0]*p[i];
	printf("%.5lf",ans);
}

F. 关灯游戏

由于按一个开关并不会对比它编号大的灯造成影响,因此最优策略是唯一的:从大到小关灯。
对开始的局面进行求解,如果操作次数 \(cnt\) 小于 \(k\),那么就直接输出 \(cnt\)
否则:
\(f_i\) 表示所需操作次数为 \(i\)\(i-1\) 经历的期望操作次数,有:

\[\large f_i=\frac{i}{n}+\frac{n-i}{n}(1+f_{i+1}+f_i) \]

该式的前半部分好理解,对于后半部分,我们按了一个不该按的键,期望操作 \(1\) 次;所需操作次数变为 \(i+1\),需要按回来,期望操作 \(f_{i+1}\) 次;按回来了还得继续操作,期望操作次数 \(f_i\) 次。
对上式化简得:

\[\large f_i=\frac{(n-i)f_{i+1}+n}{i} \]

答案即为 \(\large k+\sum_{k+1}^{cnt}f_i\)
于是做完了。

upd:
关于起始状态,考虑如果所需操作次数为 \(n\),那么无论怎么操作都不会使所需操作次数增加,因此 \(f_n=1\)。代码中令 \(f_{n+1}=0\) 便于统一计算。
本题的状态设计也很逆天。

#include <iostream>
#define mod 100003
#define int long long
#define N 100005
int n,k,a[N],inv[N],f[N];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>k;
	inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
	for(int i=1;i<=n;i++) std::cin>>a[i];
	int cnt=0;
	for(int i=n;i>=1;i--) if(a[i])
	{
		cnt++;
		for(int j=1;j*j<=i;j++) if(i%j==0)
		{
			a[j]^=1;
			if(j*j!=i) a[i/j]^=1;
		}
	}
	if(cnt<=k)
	{
		int tmp=cnt;
		for(int i=1;i<=n;i++) tmp=tmp*i%mod;
		return std::cout<<tmp,0;
	}
	f[n+1]=0;
	for(int i=n;i>k;i--)
		f[i]=inv[i]*(n+(n-i)*f[i+1]%mod)%mod;
	int ans=0;
	for(int i=k+1;i<=cnt;i++) (ans+=f[i])%=mod;
	(ans+=k)%=mod;
	for(int i=1;i<=n;i++) ans=ans*i%mod;
	std::cout<<ans;
}

G. 守卫挑战

neatisaac 想出了状态但是不会转移了
题意即求 \(\sum a_i\ge0\) 的概率。
\(f_{i,j,k}\) 表示到了第 \(i\) 项,赢了 \(j\) 场,\(\sum a_i=k\) 的概率。
于是有转移:

\[\large f_{i+1,j,k}\leftarrow(1-p_i)f_{i,j,k} \\ f_{i+1,j+1,\max(n,k+a_i)}\leftarrow p_if_{i,j,k} \]

为什么不是 \(k+a_i\)?当 \(k\ge n\) 时,\(k=+\infin\)\(k=n\) 没有区别。
答案即:

\[\large \sum_{i=l}^n\sum_{j=0}^nf_{n,i,j} \]

注意由于 \(k\) 可能小于零,所以要整体加 \(n\)
由于需要使用滚动数组,而第一个转移式能起到覆盖 \(f\) 数组的作用,所以不把两个转移式在一次循环中一起跑。

#include <iostream>
#include <cstdio>
#define N 205

int n,l,K,a[N];
double p[N],ans,f[2][N][N<<1];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>l>>K;
	if(K>n) K=n;
	for(int i=1;i<=n;i++) std::cin>>p[i],p[i]/=100;
	for(int i=1;i<=n;i++) std::cin>>a[i];
	f[0][0][n+K]=1;
	int t=1;
	for(int i=1;i<=n;i++,t^=1)
	{
		for(int j=0;j<=n;j++) for(int k=0;k<=(n<<1);k++)
			f[t][j][k]=(1-p[i])*f[t^1][j][k];
		for(int j=0;j<n;j++) for(int k=0;k<=(n<<1);k++)
			f[t][j+1][std::min(k+a[i],n<<1)]+=p[i]*f[t^1][j][k];
	}
	for(int j=l;j<=n;j++) for(int k=n;k<=(n<<1);k++)
		ans+=f[t^1][j][k];
	printf("%.6lf",ans);
}

\[\Huge End \]

posted @ 2025-04-30 23:38  整齐的艾萨克  阅读(26)  评论(0)    收藏  举报