期望DP——解决从自身转移的情况
期望DP——解决从自身转移的情况
问题背景
可以进行若干次抽奖,每一次分别获得 \(0-k\) 个物品的概率 \(p_j\) 都是确定的,给定一个 \(X\) ,求抽到 \(X\) 个物品的期望抽奖次数。
如果定义 \(f_i\) 为获得 \(i\) 个物品的期望次数,那么这个转移方程也是十分显然:
出现的问题
从数学意义上思考,这个式子本身是没有问题的。
但是从程序的方面来思考,我们写一个 naive 的转移肯定是行不通的,因为这涉及到当 \(j=0\) 的时候 ,\(f_i\) 会从自身转移。
解决方法
可以通过解方程的方法来从数学的角度变形这个式子:
再化简一下可以得到:
这个时候就可以发现转移方程里面不包含 \(f_i\) 本身了,可以直接转移。
例题
abc382_e
这个题并没有直接给出 \(p\) ,而是给出了 \(n\) 个相互独立的概率 ,所以开一个卡包获得 \(0-n\) 张卡的概率需要自己算。
这个题里面我们还需要人为处理一下 \(p\) ,这个也考虑从 DP 的角度来解决,定义 \(g_{i,j}\) 数组为考虑了前 \(i\) 张卡获得了 \(j\) 张卡的概率,这样的话转移还是就挺明显的 :
然后剩下的就直接套上面的流程即可。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=5010;
double f[N],g[N][N];
int n,x;
double p[N];
int main()
{
cin>>n>>x;
for(int i=1;i<=n;++i)scanf("%lf",&p[i]),p[i]/=100.0;
g[0][0]=1;
for(int i=1;i<=n;++i)
{
g[i][0]=g[i-1][0]*(1-p[i]);
for(int j=1;j<=i;++j)
g[i][j]=g[i-1][j-1]*p[i]+g[i-1][j]*(1-p[i]);
}
f[0]=0;
for(int i=1;i<=x;++i)
{
f[i]=1;
for(int j=max(0,i-n);j<i;++j)
{
f[i]+=f[j]*g[n][i-j];
}
f[i]/=(1-g[n][0]);
}
printf("%.16lf",f[x]);
return 0;
}
abc314_e
类似于完全背包,有 \(n\) 个不同的卡包,每一个都可以选择无限次
Code
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=200;
int c[N],num[N];
int s[N][N];
double p[N][10010];
double f[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;++i)
{
cin>>c[i]>>num[i];
double plus=1.0/(num[i]*1.0);
for(int j=1;j<=num[i];++j)cin>>s[i][j],p[i][s[i][j]]+=plus;
}
for(int i=1;i<=m;++i)f[i]=1e18;
f[0]=0;
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
double tmp=c[j];
for(int k=1;k<=m;++k)
tmp+=f[max(i-k,0)]*p[j][k];
/*关于此处“无效状态”的思考:如果说 k 的遍历已经完成,但是都从无效状态转移,也就是tmp仍然等于 c[j] ,
这意味着当前考虑的组内,所有可能的取值都比 i 大,满足了我们 "f[i]为至少达到i的分数所需要的代价" 的定义,
而这个代价自然就是 C[j]。
此外,不免会思考:多出来的那一部分怎么办?
比如说 玩意我算出来的 f[1] 实际上是获得了 2 分的代价呢?
那么会发现计算 f[2] 的过程中肯定会考虑到这一个答案,而这时 f[2] 就变成了 "刚刚好" 的类型,再进行后续的转移。
总之,这个dp数组(f数组)的定义 本身就是“至少”而不是“刚刚好”。
*/
tmp/=1-p[j][0];
f[i]=min(f[i],tmp);
}
}
printf("%.10lf",f[m]);
return 0;
}
abc342_f
题意
这个题严格意义上来讲其实并没有涉及到从自身转移的情况,但是属于是比较难的一道期望DP,而且还是比较典型。
题意就是你可以先抛若干次骰子,每次都可以等概率地获得 \(1-D\) 的点数,记最后你获得的点数为 $ x$ 。紧接着另外一个人也丢若干骰子,他的投掷过程和你的完全独立,直到他获得至少 \(L\) 分之前,他都不会停止,记最后他获得的分数是 \(y\) 。
给定一个分数上界 \(N\) 。
如果最后你的点数 \(x\) 大于 \(N\) , 那么你就输了。
否则:
- 如果 \(y>N\) 或者 \(x>y\) ,就可以获胜。
- 否则就失败。
现在询问按照最优策略行事,最后获胜的概率多少?
分析
这是一个类博弈问题,我们尝试定义 \(dp_i\) 为:从总计得到 \(i\) 点数之后进行操作获得的最大获胜概率,那么这时候就有两种决策:
- 再投一次骰子。
- 不投了。
对于情况 1 ,可以转化为子问题求解。
也就是
对于情况 2 ,这时候我们就要对于确定的 \(x\) ,来看看 \(y\) 的概率分布,从而确定获胜情况。
由于两个过程是相互独立的,所以我们可以预处理出 \(y\) 最后停在任意一点的概率,实际问题的意义上考虑,\(y\) 最后所在的位置肯定只能是 \([L,L+D-1]\) 的区间内,我们定义 \(g\) 数组为所求的数组,发现 \(g\) 数组的每一个 \(g_i\) 会以 \(\frac{1}{D}\) 的等概率转移给他之后 \([1,D]\) 的 \(g_j\) ,那么这个区间加的过程实际上可以通过差分、树状数组、线段树的方式来优化,这里选择的是差分。
由于我们正序枚举 \(y\) 可能的位置,所以只会对后面的产生加减,那么一边枚举一边对差分数组进行前缀和 ,可以同时得到当前位置的概率。并且我们在 \(i\ge L\) 的时候就会停止前进,那么只需要枚举 \([0,L-1]\) 的 \(i\) 即可。
考虑对于一个给定的 \(x\) ,去计算 \(y\) 对应的能使之获胜的概率,显然就是 \(ans_x=1-\sum_{i=L}^Ng_i+\sum_{i=L}^{x-1}g_i\) ,如果 \(L>x-1\) 的话那么最后一项对应是 \(0\) 。这个显然可以通过前缀和进行优化。
最后的转移方程就是
其中 DP 的区间和可以通过双指针的思想O(1)求出。
Code
#include<bits/stdc++.h>
using namespace std;
int n,l,d;
const int N=2e5+10;
double dif[N<<1],g[N<<1],sg[N<<1];
inline void pre()
{
cin>>n>>l>>d;
g[0]=1.0;
dif[1]=1.0/d,dif[1+d]=-dif[1];
for(int i=1;i<=l-1;++i)
{
dif[i]+=dif[i-1],g[i]=dif[i];
dif[i+1]+=g[i]/d,dif[i+d+1]-=g[i]/d;
}
for(int i=l;i<=N;++i)dif[i]+=dif[i-1],g[i]=dif[i],sg[i]=sg[i-1]+g[i];
}
double dp[N<<1];
double calc_Y(int x)
{
double ans=1-sg[n];
if(x)ans+=sg[x-1];
return ans;
}
int main()
{
pre();
double sum=0;
for(int i=n;i>=0;--i)
{
dp[i]=max(sum/d,calc_Y(i));
sum+=dp[i],sum-=dp[i+d];
}
printf("%.15lf",dp[0]);
return 0;
}
abc333_f
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/18580461