期望DP——解决从自身转移的情况

期望DP——解决从自身转移的情况

问题背景

可以进行若干次抽奖,每一次分别获得 \(0-k\) 个物品的概率 \(p_j\) 都是确定的,给定一个 \(X\) ,求抽到 \(X\) 个物品的期望抽奖次数。

如果定义 \(f_i\) 为获得 \(i\) 个物品的期望次数,那么这个转移方程也是十分显然:

\[f_i=1+\sum_{j=0}^kf_{i-j}\times p_j \]

出现的问题

从数学意义上思考,这个式子本身是没有问题的。

但是从程序的方面来思考,我们写一个 naive 的转移肯定是行不通的,因为这涉及到当 \(j=0\) 的时候 ,\(f_i\) 会从自身转移。

解决方法

可以通过解方程的方法来从数学的角度变形这个式子:

\[f_i=1+f_i\times p_0+\sum_{j=1}^kf_{i-j}\times p_j \]

再化简一下可以得到:

\[f_i=\frac{1+\sum_{j=1}^kf_{i-j}\times p_j }{1-p_0} \]

这个时候就可以发现转移方程里面不包含 \(f_i\) 本身了,可以直接转移。

例题

abc382_e

这个题并没有直接给出 \(p\) ,而是给出了 \(n\) 个相互独立的概率 ,所以开一个卡包获得 \(0-n\) 张卡的概率需要自己算。

这个题里面我们还需要人为处理一下 \(p\) ,这个也考虑从 DP 的角度来解决,定义 \(g_{i,j}\) 数组为考虑了前 \(i\) 张卡获得了 \(j\) 张卡的概率,这样的话转移还是就挺明显的 :

\[g_{i,j}=g_{i-1,j}\times (1-p_i)+g_{i-1,j-1}\times p_i \]

然后剩下的就直接套上面的流程即可。

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. 不投了。

对于情况 1 ,可以转化为子问题求解。

也就是

\[\frac{\sum_{j=i+1}^{i+D}dp_j}{D} \]

对于情况 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_i=\max(\frac{\sum_{j=i+1}^{i+D}dp_j}{D},ans_x) \]

其中 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

posted @ 2024-12-05 13:46  Hanggoash  阅读(2)  评论(0编辑  收藏  举报
动态线条
动态线条end