期望DP从入门到出门
开完会后决定重写一遍课件
持续更新
数学定义:
期望的公式:
我们先来明确两个概念
随机变量:
我们扔一颗骰子,设 \(x\) 为抛出的点数,显而易见 \(x\) 的取值有六个,分别是 1,2,3,4,5,6。
但是每次扔出的点数是随机的、不可预计的,即 \(x\) 的取值有不确定性,像 \(x\) 这种变量就称作随机变量。
离散型随机变量:
随机变量分为两种:离散型和连续型。
离散型:该变量的取值个数是有限的,可以一个一个列出的,就比如投骰子和扔硬币。
连续型:该变量的取值个数无限,数不尽的,比如随机变量 \(x\) 的全部取值为全体实数,那么 \(x\) 就为连续型变量。
对于一组离散型随机变量,它的所有取值乘上取值所对应概率的总和,就是数学期望。
期望的性质:
设 \(X,Y\) 为随机变量,\(a,b,c\) 为常数
不知道什么性质:\(E(c)=c\)
显而易见,根据期望的公式,\(E(c)=c×P(c=x)=c\)
可乘性:当 \(X\) 与 \(Y\) 相互独立时,\(E(XY)=E(X)×E(Y)\)
证明:
当 \(X\) 与 \(Y\) 相互独立时,\(P(XY)=P(X)×P(Y)\)。
可加性:\(E(X+Y)=E(X)+E(Y)\)
证明:
根据期望的定义来计算:
根据前两条可以推出:\(E(aX)=aE(X)\)
再结合第三条可以进一步得出:\(E(aX+b)=aE(X)+b\)。
以上为期望的基础知识,大家还是掌握好一点,毕竟后面的题推式子的时候你可能什么都不理解(
期望DP:
梯丹
当然不可能全讲,只能挑会讲的几道讲,剩下的就当成作业罢。
前面几题严格来说并不算 \(dp\),只能算简单递推,就作为引入罢。
o s u 三 件 套
这三题思路大体一致连名字都差不多,所以放在一块讲。
题意应该很好理解,解法也很好想,为了方便讲,我们先把第一个中的 0 1 ?
分别转化成成功率为 0 1 0.5
(?
表示 0
和 1
的出现概率相等,也就是 0.5
的胜率)。
我们用 \(len1\) 来表示打到当前操作 combo
长度的期望,\(len2\) 表示长度平方的期望,\(f\) 为期望得分,\(p\) 为成功概率。
先看前两道题,它们的得分贡献为平方,比较好维护,如果当前成功,那么:
因为 \(f\) 是长度的平方,\(len1+1\) 的话 \(sum\) 就要加上 \(2×len+1\)。
如果失败了,那么 \(len1\) 要清零,\(sum\) 不变。
综合一下,可以推出式子:
然后再看最后一题。从平方变成了立方,不过也好维护:
SP1026 FAVDICE - Favorite Dice
设 \(f_i\) 为已经掷到过 \(i\) 个不同的面,使 \(n\) 个面全部被掷到的期望投掷次数。
显然的,有 \(\frac{i}{n}\) 的概率掷到已经掷到过的面,有 \(\frac{n-i}{n}\) 的概率掷到未被掷到的面。
可以推出式子:
移个项:
最后直接递推就行
#include<bits/stdc++.h>
using namespace std;
double ans,n;
int T;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%lf",&n);
for(int i=1;i<=n;++i) ans+=(n*1.0/(i));
printf("%.2lf\n",ans);
ans=0;
}
return 0;
}
P3802 小魔女帕琪
稍微思考一下可以得出,任意连续 \(7\) 位构成终极魔法的概率是一样的。那么问题就转换成了求长度为 \(7\) 的序列中触发的期望,最后再 \(×(n-6)\),就可以算出最终的期望。
先求形如 \(1,2,3,4,5,6,7\) 的触发概率
设 \(N=a_1+a_2+...+a_7\)
很好得出:
考虑到顺序没有影响,所以有 \(7!\) 种排列,所以:
可以得出最终答案:
#include<bits/stdc++.h>
using namespace std;
double a[8],n;
int main()
{
for(int i=1;i<=7;++i) scanf("%lf",&a[i]),n+=a[i];
printf("%.3lf",5040.0*a[1]/n*a[2]/(n-1.0)*a[3]/(n-2.0)*a[4]/(n-3.0)*a[5]/(n-4.0)*a[6]/(n-5.0)*a[7]);
return 0;
}
Ilya and Escalator
虽然有点恶意评分的意思,但也可以来水一道蓝题锻炼一下推式子的能力。
用二维的状态来解决这个问题,设 \(f_{i,j}\) 为时间为 \(j\) 秒时电梯里有 \(i\) 人的概率。
有 \(p\) 的概率人数和秒数均增加,有 \(1-p\) 的概率只有秒数增加,于是可以推出式子:
最后特判一下边界条件:当 \(j=n\) 时电梯人满了,所以一定可以从 \(f_{i,j-1}\) 转移而来:
因为要求的是期望人数,所以 \(ans=\sum_{i=1}^{n} f_{i,j}×i\)
#include<bits/stdc++.h>
using namespace std;
double n,t,p,dp[2010][2010],ans;
int main ()
{
cin>>n>>p>>t;
dp[0][0]=1;
for(int i=1;i<=t;++i)
for(int j=0;j<=n;++j)
{
if(j) dp[i][j]+=dp[i-1][j-1]*p;
if(j!=n) dp[i][j]+=dp[i-1][j]*(1-p);
else dp[i][j]+=dp[i-1][j];
}
for(int i=1;i<=n;++i) ans+=dp[(int)t][i]*(i*1.0);
printf("%.6lf",ans);
return 0;
}
Bag of mice
典?
oi-wiki
用的就是这道题
先贴一份代码在这里,题解先咕着
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int w,b;
double f[N][N];
int main()
{
io>>w>>b;
for(int i=1;i<=w;++i)
f[i][0]=1.0,f[i][1]=1.0*i/(i+1);
if(!b||b==1)return printf("%.9lf\n",f[w][b]),0;
for(int i=1;i<=w;++i)
for(int j=2;j<=b;++j)
{
f[i][j]=1.0*i/(i+j);
f[i][j]+=1.0*j/(i+j)*(j-1)/(i+j-1)*i/(i+j-2)*f[i-1][j-2];
if(j^2)f[i][j]+=1.0*j/(i+j)*(j-1)/(i+j-1)*(j-2)/(i+j-2)*f[i][j-3];
}
printf("%.9lf\n",f[w][b]);
return 0;
}
先考虑边界
P6835 [Cnoi2020] 线形生物
好题,可以加强对期望线性性质的理解。
设 \(f_i\) 为从当前节点 \(i\) 到 \(i+1\) 所需的期望步数。
根据上面讲的可加性,可以得出 \(E_{x\to y} = \sum_{i=x}^{y-1} E_{i\to i+1}=\sum_{i=x}^{y-1} f_i\)
所以我们只需要单独算出每个点的 \(f\) 值,再求和即可。
现在问题就转化为了求 \(f_i\) ,设 \(cnt\) 为 \(i\) 返祖边的数量。
假如在当前点选择了返祖边,那么需要再从 \(v\) 走回 \(u\),即 \(\sum_{i=v}^{u-1} f_i\),显而易见可以用前缀和维护,最后还要加上走了返祖边的贡献 \(1\),简化为:\(sum_i-sum_{j-1}+1\)。
现在所有返祖边的贡献都可以求出了,同时还需要加上直接往后走的情况,所以方程为:
复杂度为 \(O(n+m)\),不简化会慢一点。
#include<bits/stdc++.h>
using namespace std;
long long mod=998244353;
long long qp(long long a,long long b,long long ans=1)
{
for(;b;b>>=1,a=(a*a)%mod)ans=ans*(b&1?a:1)%mod;
return ans;
}
int id,n,m,c,ans;
vector<int>s[1000100];
long long dp[1000100],sum[1000100];
int main ()
{
cin>>id>>n>>m;
for(int i=1;i<=m;++i)
{
int u,v;
cin>>u>>v;
s[u].push_back(v);
}
for(int i=1;i<=n;++i)
{
long long len=s[i].size(),cnt=0;
for(auto j:s[i]) cnt+=sum[j-1];
dp[i]=((len*sum[i-1]-cnt+len+1)%mod+mod)%mod;
sum[i]=(sum[i-1]+dp[i])%mod;
}
cout<<sum[n];
return 0;
}
P3750 [六省联考 2017] 分手是祝愿
好典
仔细想想不难发现:对于每个灯,只有它后面的灯才能影响到它,换句话说:对于一个点,在后面的点操作完后,如果这个灯还亮着,这个点就必须按。所以我们可以从后面扫一遍,把必须要按的灯记录下来,共 \(cnt\) 个,特别的,如果 \(cnt\le k\),直接输出 \(cnt\) 就行。并且对于每个不必要的点,如果按到了,就必须要按回去来消除影响。
设 \(f[i]\) 为由剩余 \(i\) 个必须要按的灯,变为 \(i-1\) 个必须要按的灯所需次数的期望。然后就是熟悉的分情况:
-
按到必要的灯,因为必要的灯已经确定了,所以顺序没有影响,概率为 \(\frac{i}{n}\)
-
按到不必要的灯(或者是后面已经灭了的灯),概率为 \(\frac{n-i}{n}\) ,此时必要灯的数量变为 \(i+1\) ,所以还需 \(f[i+1]+f[i]\) 次操作才能变为 \(i-1\) 个。
简化一下:
最终求出答案(不要忘了 \(+k\) ):
#include<bits/stdc++.h>
using namespace std;
long long mod=100003;
int n,k,cnt;
long long c=1,f[100001];
int a[100001];
long long qp(long long a,long long b,long long ans=1){for(;b;b>>=1,a=(a*a)%mod) if(b&1) ans=(ans*a)%mod;return ans;}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)
{
c=(c*i)%mod;
scanf("%d",&a[i]);
}
for(int i=n;i>=1;--i)
{
if(!a[i]) continue;
for(int j=1;j*j<=i;++j)
{
if(i%j==0) a[i/j]^=1,a[j]^=1;
if(j*j==i) a[j]^=1;
}
cnt++;
}
if(cnt<=k) return printf("%lld",c*cnt%mod),0;
f[n]=1;
for(int i=n-1;i;--i) f[i]=(long long)(n-i)*(f[i+1]+1)%mod*qp(i,mod-2)%mod+1;
long long ans=k;
for(int i=k+1;i<=cnt;++i) ans=(ans+f[i])%mod;
printf("%lld",ans*c%mod);
return 0;
}