期望dp

  • 期望的基本性质
    • 离散型随机变量和连续型随机变量
      • 例题1:红包发红包
    • 期望的基本运算性质
      • 例题2:Little Pony and Expected Maximum
  • 期望的一些经典题型
    • 图上期望问题
      • 例题3:绿豆蛙的归宿
      • 例题4:游走
      • 例题5:线形生物
      • 例题6:Intergalaxy Trips
    • dp状态设计
      • 例题7:收集邮票
      • 例题8:Game Relics
      • 例题9:分手是祝愿
      • 例题10:亚瑟王
    • 树上期望问题
      • 例题11:仓鼠找sugar II
      • 例题12:概率充电器
      • 例题13:Shrinking Tree
    • 期望dp+优化
      • 例题14:Inversions After Shuffle
      • 例题15:小 Y 和恐怖的奴隶主

期望真的太神奇了…

期望的基本性质

首先,随机变量分为离散型随机变量和连续型随机变量两种,值域大小有限(取值个数有限)或为可列无穷大的随机变量称为离散型随机变量。

离散型随机变量和连续型随机变量

离散型随机变量的期望等于它的每一个取值乘该取值对应的概率的总和,而连续型随机变量需要以积分形式表示为: ∫−∞+∞xp(x)dx\int_{-\infty}^{+\infty}xp(x)dx∫−∞+∞​xp(x)dx ,其中 p(x)p(x)p(x) 指的是 xxx 取值的密度分布函数(通俗的来说大概就是 xxx 这个值出现的概率)。

例题1:红包发红包

(洛谷P5104)

这道题的题意非常简单明了,问题就在于这个 [0,w][0,w][0,w] 内随机取值是一个无穷大的值域,所以这题所问的是一个连续型随机变量的期望。
既然如此,考虑一下它的分布函数是什么。其实也很简单,即 p(x)=1w(0≤x≤w)p(x)=\frac{1}{w}\,\,(0\leq x \leq w)p(x)=w1​(0≤x≤w) ,代入公式可得期望为 ∫0wxdxw\int_{0}^{w}\frac{xdx}{w}∫0w​wxdx​ 。这个积分形式很简单,介于积分可以看做是求导的一个逆运算,所以可以直接得到答案是 w2\frac{w}{2}2w​ 。
现在求的这个是第一个人得到的钱的期望,第二个人得到的期望实质上就是把原来的 www 换成 (w−w2)(w-\frac{w}{2})(w−2w​) ,以此类推,第 kkk 个人抢到的钱的期望就是 w2k\frac{w}{2^k}2kw​ ,写个快速幂这题就解决了。代码略。

OI中一般不常考连续型随机变量的期望问题,所以这里也只是简单举一个例子尝尝鲜,真正关键的是离散型随机变量的期望问题(为便于表述,以下的“离散型随机变量的期望”简称为“期望”,“离散型随机变量”简称为“随机变量”,“xxx的期望”也可能称为“期望xxx”)。

期望的基本运算性质

一般以 。设期望的主要性质有四:
( ccc 为一个常数, xxx 和 yyy 是两个随机变量, E(x)E(x)E(x) 表示 xxx 的期望)

E(c)=cE(c) = cE(c)=c
E(cx)=cE(x)E(cx)=cE(x)E(cx)=cE(x)
xxx 和 yyy 相互独立时, E(xy)=E(x)E(y)E(xy)=E(x)E(y)E(xy)=E(x)E(y)
E(x+y)=E(x)+E(y)E(x+y)=E(x)+E(y)E(x+y)=E(x)+E(y)

最后一条被称为“期望的线性性”,是化一个看似不可解的期望问题为若干相对简单的子任务的工具,因此也是期望问题中最常见的转化问题求解的技巧。
除此之外,还有一条概率前缀和的性质:P(x=n)=P(x≤n)+P(x≤n−1)P(x=n)=P(x\leq n)+P(x\leq n-1)P(x=n)=P(x≤n)+P(x≤n−1) ,本质上是一个补集思想,这可以在一些求特定值例如最值的期望问题当中用到。

例题2:Little Pony and Expected Maximum

(CF453A)

尝尝鲜。
考虑枚举每一个最大值,计算它出现的概率。但是发现这个概率直接算不太好计算,因为不好排除扔出别的更大点数的情况,这就可以套用前面的思维,扔出的最大值为 kkk 的概率,就是扔出的点数不超过 kkk 去掉扔出的不超过 (k−1)(k-1)(k−1) 的概率,这两个就可以直接看作在对应的点数当中枚举了。答案就是∑i=1m(in−(i−1)n)×imn=∑i=1m((im)n−(i−1m)n)×i.\frac{\sum\limits_{i=1}^m(i^n-(i-1)^n)\times i}{m^n}=\sum\limits_{i=1}^m((\frac{i}{m})^n-(\frac{i-1}{m})^n)\times i.mni=1∑m​(in−(i−1)n)×i​=i=1∑m​((mi​)n−(mi−1​)n)×i.
代码略。

期望的一些经典题型

期望的题经典的问题并不多,整体上还是千变万化,还是像所有的dp一样没什么好套路,凭思维见招拆招。不过一些很经典的问题还是可以作为参考。

图上期望问题

要理解期望,我觉得还是得从图这种最直观的形式开始。经典分裂图论和树论了属于是
这里所要讨论的图分为两种,一种是DAG,一种是有环图(无自环)。这两者的区别就在于,前者一定是单向更新的,而后者不一定,所以可能涉及到用高斯消元解方程组(事实上大部分时候到底是状态设计有问题还是这题确实就是得高斯消元靠着看数据范围就能看出来)。

例题3:绿豆蛙的归宿

(洛谷P4316)

一道不能更经典的图上期望问题。
这道题首先保证了是一个连通DAG,好评。
记 degxdeg_xdegx​ 表示点 xxx 的出度数。
首先可以想到,假设存在 u−vu-vu−v ,那么绿豆蛙从 uuu 点走到 vvv 点的概率就是 1degu\frac{1}{deg_u}degu​1​ , fv=∑(fu+lenu,v)×1deguf_v=\sum \,(f_u+len_{u,v})\times \frac{1}{deg_u}fv​=∑(fu​+lenu,v​)×degu​1​ ,完事了!
然而并不如此。那么问题出在哪儿呢?
这里的关键在于,确实考虑了从 uuu 点走到 vvv 点的概率,也确实是在用 uuu 的期望在计算贡献,但是 u−vu-vu−v 的边长并不是期望意义下的长度,它应该乘以到达 uuu 这个点的概率才是有期望意义的。记 pxp_xpx​ 表示到 xxx 点的概率,有 pv=∑pudegup_v=\sum \frac{p_u}{deg_u}pv​=∑degu​pu​​ ,那么正确的式子就应该是 fv=∑(fu+lenu,v×pu)×1degu.f_v=\sum \,(f_u+len_{u,v}\times p_u)\times \frac{1}{deg_u}.fv​=∑(fu​+lenu,v​×pu​)×degu​1​.

现在换个方向思考,假设反着做这道题,记 fxf_xfx​ 表示从 xxx 点到终点还需要走的期望步数,转移方程会如何变化?
还是套用前面的状态,设 pxp_xpx​ 为从 xxx 点走到终点的概率,由于此题的约束,会发现 px=1p_x=1px​=1 ,于是就不用考虑它了,可得fu=∑(fv+lenu,v)×1degu.f_u=\sum \,(f_v+len_{u,v})\times \frac{1}{deg_u}.fu​=∑(fv​+lenu,v​)×degu​1​.

尽管此题有若干有利的约束,但是通过上面的讨论,我们还是能学到两点:一是期望的转移本质上是事件的期望乘以转移发生的概率贡献到目标事件的期望 ,简单来说,期望贡献的一切参量归根结底都应该是在期望意义下的;二是一些期望问题倒推可能更加简单。前者看似是一句废话,但是确实容易被忽略,应该格外重视。

以上两种都是topo+递推的做法,不妨再来考虑一下用线性性怎么解决这题。答案就等于经过每一条边的期望次数(注意并不是概率)乘以边的长度之和。经过每一条边的期望次数就是经过它的起点的期望次数除以出度数,而经过一个点的期望次数又等于经过它的入度的期望次数之和。
形式化地,设 gx,yg_{x,y}gx,y​ 表示经过边 x−yx-yx−y 的期望次数, hxh_xhx​ 表示经过点 xxx 的期望次数,那么就有 gu,v=∑hudegu,hv=∑gu,v,ans=∑gu,v×lenu,v.g_{u,v}=\sum \frac{h_u}{deg_u},\,h_v=\sum g_{u,v},\,ans=\sum g_u,v\times len_{u,v}.gu,v​=∑degu​hu​​,hv​=∑gu,v​,ans=∑gu​,v×lenu,v​. 不难发现,其实这样分析这道题更为清晰,也不那么太玄学,充分说明了线性性的意义。

代码很简单,略过。注意反图只是用来约束更新顺序的,所以建反图的时候不要把统计出度给反过来。

例题4:游走

(洛谷P3232)

无重边无自环,没有别的限制了。
这题要求设计一个边长的排列,使得经过的期望总边长最小。这算是一个小插曲,显然是期望经过次数越多的边长应该越小,于是问题就是求每一条边的期望经过次数。
延续上面的方法,由于这回是无向边,起点有两个,所以 gu,v=hudegu+hvdegv,hv=∑hudegu.g_{u,v}=\frac{h_u}{deg_u}+\frac{h_v}{deg_v},\,h_v=\sum \frac{h_u}{deg_u}.gu,v​=degu​hu​​+degv​hv​​,hv​=∑degu​hu​​. 由于转移关系成环,所以需要上高斯消元求解。注意一是不要把无意义的 hnh_nhn​ 放进去消元,二是不要忘了初始状态下 h1=1.h_1=1.h1​=1.

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 125001;
struct yjx{int nxt,to;
}e[N << 1];
int ecnt = -1,head[505],deg[505],u[N],v[N];
double a[505][505],f[N];
void save(int x,int y){e[++ecnt].nxt = head[x];e[ecnt].to = y;head[x] = ecnt;++deg[y];
}
void Gauss(int n){int i,j,k,temp;for(i = 1;i <= n;i++){temp = i;for(j = i + 1;j <= n;j++){if(fabs(a[j][i]) > fabs(a[temp][i])) temp = j;}if(temp != i){for(j = 1;j <= n + 1;j++) swap(a[i][j],a[temp][j]);}if(fabs(a[i][i]) < 1e-8) continue;for(j = 1;j <= n;j++){if(j != i){for(k = i + 1;k <= n + 1;k++){a[j][k] -= a[i][k] * a[j][i] / a[i][i];}}}}for(i = 1;i <= n;i++) a[i][n + 1] /= a[i][i];
}
int main(){int i,j,n,m,temp;double res = 0;scanf("%d %d",&n,&m);memset(head,-1,sizeof(head));for(i = 1;i <= m;i++){scanf("%d %d",&u[i],&v[i]);save(u[i],v[i]),save(v[i],u[i]);}a[1][n] = -1.0;for(i = 1;i < n;i++){a[i][i] = -1.0;for(j = head[i];~j;j = e[j].nxt){temp = e[j].to;if(temp == n) continue;a[i][temp] = 1.0 / deg[temp];}}Gauss(n - 1);for(i = 1;i <= m;i++){f[i] = a[u[i]][n] / deg[u[i]] + a[v[i]][n] / deg[v[i]];}sort(f + 1,f + m + 1);for(i = 1;i <= m;i++) res += (m - i + 1) * f[i];printf("%.3lf\n",res);return 0;
}

高斯消元即使在dp当中也是一种效率不高的算法,属于一种没法转移的无奈之举。因此即便转移是有环的,根据题目的许多性质,也不一定就必须要用高斯消元。

例题5:线形生物

(洛谷P6835)

这道题如果还上高斯消元就不太合适了 ,因为数据范围
仍然是套线性性,答案等于经过链上每一条边的期望次数之和,那么 gu,u+1=1degu+1degu∑(gv,u+1+1)g_{u,u+1}=\frac{1}{deg_u}+\frac{1}{deg_u}\sum\,(g_{v,u+1}+1)gu,u+1​=degu​1​+degu​1​∑(gv,u+1​+1) ,考虑到只有链和返祖边,因此 v−(u+1)v-(u+1)v−(u+1) 也是若干链上的边组成的,所以这里就可以记一个前缀和 sumsumsum ,然后化简一波:

gu,u+1=1degu+1degu∑(gv,u+1+1)=1degu+1degu∑(sumu−sumv−1+1)=1degu+1degu∑(gu,u+1+sumu−1−sumv−1+1)=1degu+degu−1degu(gu,u+1+1)+1degu∑(sumu−1−sumv−1+1)∴gu,u+1=degu+1degu∑(sumu−1−sumv−1+1).\begin{aligned} g_{u,u+1}&=\frac{1}{deg_u}+\frac{1}{deg_u}\sum\,(g_{v,u+1}+1)\\ &=\frac{1}{deg_u}+\frac{1}{deg_u}\sum\,(sum_{u}-sum_{v-1}+1)\\ &=\frac{1}{deg_u}+\frac{1}{deg_u}\sum\,(g_{u,u+1}+sum_{u-1}-sum_{v-1}+1)\\ &=\frac{1}{deg_u}+\frac{deg_{u}-1}{deg_u}(g_{u,u+1}+1)+\frac{1}{deg_u}\sum\,(sum_{u-1}-sum_{v-1}+1)\\ \\ \therefore g_{u,u+1}&=deg_u+\frac{1}{deg_u}\sum\,(sum_{u-1}-sum_{v-1}+1). \end{aligned} gu,u+1​∴gu,u+1​​=degu​1​+degu​1​∑(gv,u+1​+1)=degu​1​+degu​1​∑(sumu​−sumv−1​+1)=degu​1​+degu​1​∑(gu,u+1​+sumu−1​−sumv−1​+1)=degu​1​+degu​degu​−1​(gu,u+1​+1)+degu​1​∑(sumu−1​−sumv−1​+1)=degu​+degu​1​∑(sumu−1​−sumv−1​+1).​

最终答案即为 sumnsum_nsumn​ ,代码略。

例题6:Intergalaxy Trips

(CF605E)

这题就复杂一些了。
先来考虑这个“最优策略”。很显然,我一定会在连通的点当中选到终点期望天数最小的点去走,因此需要倒推。
有了这个思路,设 fxf_xfx​ 表示从 xxx 走到 nnn 的期望天数,对于一条边 u−vu-vu−v ,它会被选中当且仅当 vvv 与 uuu 连通,而到 nnn 的期望天数比 vvv 小的点都不与 uuu 连通,即:
fu=∑v=1nfv×pu,v×∏wfw<fv(1−pu,w)+1f_u=\sum\limits_{v=1}^{n} f_v\times p_{u,v}\times\prod\limits_{w}^{f_w<f_v}(1-p_{u,w})+1fu​=v=1∑n​fv​×pu,v​×w∏fw​<fv​​(1−pu,w​)+1
然而这个转移式是有瑕疵的,因为光考虑了怎么去转移,而没有考虑转移发生的概率(也就是前面说的缺少一个期望/概率意义) 。所以还应该给 fvf_vfv​ 乘以一个期望天数,即 11−∏x=1n(1−pv,x).\frac{1}{1-\prod\limits_{x=1}^{n}(1-p_{v,x})}.1−x=1∏n​(1−pv,x​)1​.

这里所得的期望是概率的倒数,至于为什么是这样的,下面会有详细解释。

转移的时候使用类似于Dijkstra的方法就可以了,每次找最小未更新的来更新。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 1001;
int n;
double e[N][N],f[N],g[N],mx;
bool vis[N];
int main(){int i,j;scanf("%d",&n);for(i = 1;i <= n;i++){for(j = 1;j <= n;j++){scanf("%lf",&e[i][j]);e[i][j] *= 0.01;}} if(n == 1){puts("0.0000000000");return 0;}f[n] = 0;vis[n] = 1;for(i = 1;i < n;i++){f[i] = 1;g[i] = 1.0 - e[i][n];}//预处理for(i = 1;i <= n;i++){mx = 1e18;int pos = 0;for(j = 1;j <= n;j++){if(!vis[j] && f[j] / (1.0 - g[j]) < mx){mx = f[j] / (1.0 - g[j]),pos = j;}}//找当前未更新的期望天数最小的点vis[pos] = 1;if(pos == 1){printf("%.10lf\n",f[1] / (1.0 - g[1]));return 0;}for(j = 1;j <= n;j++){f[j] += f[pos] / (1.0 - g[pos]) * e[j][pos] * g[j],g[j] *= (1.0 - e[j][pos]);}//把这个点的价值和约束一次性都刷完}return 0;
}

总结一下,通过图上期望问题,直观地了解了期望是如何计算的,不仅在实践当中验证了线性性的强大,而且也尝试了通过倒推来避开一些繁复的概率计算。期望神秘的面纱也算是开始被揭开了 (自行脑补语气) 。

dp状态设计

经过图上期望,很容易感觉到,期望的线性性是解决相当大部分期望问题的关键一步。更绝对地,解决大部分期望问题的步骤大致可以抽象如下:分段,设计转移方程—套线性性—根据转移方程求所需的参量设计新转移方程—套线性性—…—得到解。对于所有dp,怎么设计转移方程永远是最关键的一环。而通过一些比较奇妙的或是有代表性的题目,可以尽量多地总结出一些经验。

例题7:收集邮票

(洛谷P4550)

一道也是经典的不能更经典的题。
设 fxf_xfx​ 表示已经取了 xxx 张邮票,要取完 nnn 种邮票还需要的期望花费(如果没有前面的部分,这一状态设计看起来会多少有点不自然)。假设再取一次,那么情况显然分两种,一种是取到了新的,一种是没取到新的。注意到取这一次的花费和一共取了多少次有关,所以再设 gxg_xgx​ 表示已经取了 xxx 张邮票,要取完 nnn 种邮票还需要的期望次数。分析它的转移方程的方法同理,更简单,就是 gi=in×(gi+1)+n−in×(gi+1+1)g_i=\frac{i}{n}\times (g_i+1)+\frac{n-i}{n}\times (g_{i+1}+1)gi​=ni​×(gi​+1)+nn−i​×(gi+1​+1) ,那么就有 fi=in×(gi+1+fi)+n−in×(gi+1+1+fi+1).f_i=\frac{i}{n}\times (g_i+1+f_i)+\frac{n-i}{n}\times (g_{i+1}+1+f_{i+1}).fi​=ni​×(gi​+1+fi​)+nn−i​×(gi+1​+1+fi+1​). 化简一下就可以了。
和上面的绿豆蛙的问题类似,如果采取正推,不能像这样来写转移方程。不妨把这种转移关系看成这样一张图:

相比于前面的绿豆蛙的那一题,其实这里正推在数字上更为直观。想象一下,如果形式一致,那么大概形如 gi=i−1n×(gi+1)+n−i+1n×(gi−1+1)g_i=\frac{i-1}{n}\times (g_i+1)+\frac{n-i+1}{n}\times (g_{i-1}+1)gi​=ni−1​×(gi​+1)+nn−i+1​×(gi−1​+1) ,但是仔细观察就发现这样转移显然是没道理的,从 (i−1)(i-1)(i−1) 张抽到自己并不能更新 fif_ifi​ ,所以这个转移方程也是没意义的,必须算出抽到新的一张邮票的期望次数才能更新。
那么怎么计算呢?不同于之前,自环的存在导致这是一个无限的和,枚举买的次数,即 ∑kk×(i−1n)k−1×n−i+1n\sum\limits_k k\times(\frac{i-1}{n})^{k-1}\times\frac{n-i+1}{n}k∑​k×(ni−1​)k−1×nn−i+1​ ,经过一系列化简(这个属于数列知识,不展开解释了)得到 nn−i+1\frac{n}{n-i+1}n−i+1n​ ,于是有 gi=gi−1+nn−i+1g_i=g_{i-1}+\frac{n}{n-i+1}gi​=gi−1​+n−i+1n​ , fif_ifi​ 同理。

上面的这一系列推导的结果十分有特点:期望等于概率的倒数。更加严谨的定义是:一件事发生的期望次数等于它发生的概率的倒数。 这个结论是来自于上面的推导,所以这个结论成立,一是算的必须是期望次数,二是可以等概率发生无限次。

代码略。

例题8:Game Relics

(CF1267G)

接着上一题的思路,现在已经有 iii 个圣物的情况下抽到一个新的圣物的期望花费是 (nn−i+1)×x2(\frac{n}{n-i}+1)\times\frac{x}{2}(n−in​+1)×2x​ ,买到一个新的圣物的期望花费是 1n−i×∑jcj\frac{1}{n-i} \times\sum\limits_jc_jn−i1​×j∑​cj​ ,这里如何决策与购买次数和总花费都有关系,所以设 fi,jf_{i,j}fi,j​ 表示购买了 iii 个圣物花费为 jjj 的概率。这个概率不好转移,而方案数用背包就算出来了,所以算个方案数最后除以 CniC_n^iCni​ 就可以了。这个时候再用线性性,期望最小花费是各个状态下期望最小花费的总和,此题就做出来了。

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 101;
const int M = 1e4 + 1;
int n,m,c[N];
double res,w,fac[N],f[N][M];
#define C(n,m) (fac[n] / fac[m] / fac[(n) - (m)])
int main(){int i,j,k;scanf("%d %lf",&n,&w);for(i = 1;i <= n;i++){scanf("%d",&c[i]);m += c[i];}fac[0] = 1;for(i = 1;i <= n;i++) fac[i] = 1.0 * i * fac[i - 1];f[0][0] = 1.0;for(i = 1;i <= n;i++){for(j = n;j >= 1;j--){for(k = m;k >= c[i];k--){f[j][k] += f[j - 1][k - c[i]];}}}for(i = 0;i < n;i++){for(j = 0;j <= m;j++){if(!f[i][j]) continue;//减少常数res += f[i][j] / (1.0 * C(n,i)) * min(1.0 * (m - j) / (1.0 * (n - i)),(1.0 * n / (1.0 * (n - i)) + 1) * w / 2);}}printf("%.10lf\n",res);return 0;
}

例题9:分手是祝愿

(洛谷P3750)

这道题就比较玄了。
首先,如果存在一种方案能够把当前局面变成全灭,那么这个操作的步骤是可以随便换的(开关灯本质上是异或运算,而异或运算具有交换律),所以一个开关只有操作或不操作两种可能。不难想到一种暴力的方法:从大到小扫一遍,有亮的就关掉。
这种方法是最优的吗?
显然,每一种操作方案只能解决唯一的一种局面。那么是否存在多种操作方案对应同一种局面呢?不存在,因为操作方案和局面都是 2n2^n2n 种,假设存在的话,就会出现某种局面无解,而这明显不可能。因此我们上面提到的那种暴力做法就是最优的。
按这种方法模拟一遍,算出需要关多少个开关。根据线性性,全灭的期望操作次数等于每一步的期望操作次数。这道题特殊的地方在于,关错了会导致需要关的开关变多,所以就不能直接设距离全灭还有多少步。不妨设 fif_ifi​ 表示从 iii 个需要关的开关当中减少一个的期望操作次数,那么转移方程就是 fi=in+n−in×(fi+1+fi+1)f_i=\frac{i}{n}+\frac{n-i}{n}\times(f_{i+1}+f_i+1)fi​=ni​+nn−i​×(fi+1​+fi​+1) ,注意别忘了关错了的情况下这一次开关本身。化简上式即可递推,最终答案就是 ∑i=k+1totfi.\sum\limits_{i=k+1}^{tot}f_i.i=k+1∑tot​fi​.
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 1e5 + 3;
const int N = 1e5 + 1;
int n,k,st[N],cnt;
long long f[N];
long long ksm(long long a,int b){long long ret = 1;while(b){if(b & 1) ret = ret * a % mod;a = a * a % mod;b >>= 1;}return ret;
}
int main(){int i,j;long long res = 0;scanf("%d %d",&n,&k);for(i = 1;i <= n;i++) scanf("%d",&st[i]);for(i = n;i >= 1;i--){if(st[i]){++cnt;st[i] ^= 1;for(j = 1;j * j <= i;j++){if(i % j == 0){st[j] ^= 1;if(j * j != i) st[i / j] ^= 1;}}}}if(cnt <= k) res = cnt;else{res = k;f[n] = 1;for(i = n - 1;i >= 1;i--){f[i] = ((n - i) * f[i + 1] % mod + n) % mod * ksm(i,mod - 2) % mod;}for(i = cnt;i > k;i--){res = (res + f[i]) % mod;}}for(i = 1;i <= n;i++){res = res * i % mod;}printf("%lld\n",res);return 0;
}

例题10:亚瑟王

(洛谷P3239)

根据线性性,应该计算出每一张牌打出的概率,而这种概率是和轮数有关的,具体的计算方法也和前面的购买邮票有些相似。设 fi,jf_{i,j}fi,j​ 表示前 iii 张牌打了 jjj 张的概率,由于每一张牌打出都要消耗掉一轮,所以讨论第 iii 张是否被打出,可得 fi,j=fi−1,j×(1−pi)r−j+fi−1,j−1×(1−(1−pi)r−j+1)f_{i,j}=f_{i-1,j}\times(1-p_i)^{r-j}+f_{i-1,j-1}\times(1-(1-p_i)^{r-j+1})fi,j​=fi−1,j​×(1−pi​)r−j+fi−1,j−1​×(1−(1−pi​)r−j+1) ,要注意由于 jjj 的不同导致指数的不同。
有了这个就好办了。设 gig_igi​ 表示第 iii 张牌被打出的概率,首先 g1=(1−(1−p1)r)g_1=(1-(1-p_1)^r)g1​=(1−(1−p1​)r) ;对于剩下的,由于 fff 计算的是考虑结束的状态,所以应该从 fi−1,jf_{i-1,j}fi−1,j​ 的状态推出 gi=∑j=0rfi−1,j×(1−(1−pi)r−j).g_i=\sum\limits_{j=0}^{r} f_{i-1,j}\times(1-(1-p_i)^{r-j}).gi​=j=0∑r​fi−1,j​×(1−(1−pi​)r−j). 大功告成。

总结一下,这道题当中的线性性主要体现在第一步上,而后续的计算每一个部分仍然是要从前面的状态上推导而来的,不能完全把问题彻底割裂开来。这也算是组合数学+期望dp的一个简单版本。

代码略。

总结一下,上面的5道题都在讨论如何设计dp状态的问题。例题7算是一个开端,把图上问题当中讨论正推逆推的方法推广到了线性递推问题上,而例题8,9从两个方面介绍了怎么在这个基础问题上再应用线性性解题的方法,以及在动态规划当中仍然去考虑“局部最优”的情况。例题10引入了一点排列组合问题,扩展了线性性应用后一步当中的转移复杂度。经过这些问题,不仅是把期望问题抽象化了,也强化了应用线性性的灵活度。

树上期望问题

由于树的结构比一般图更特殊,树的性质也更多,所以期望dp发挥的空间也大了,自然问题的难度也高了很多。

例题11:仓鼠找sugar II

(洛谷P3412)

一道我所认为的树上期望经典入门题。
应用线性性,考虑如何计算一条边被走过的期望步数。根据前面游走的经验,起点不同被走过的期望次数也是不同的,由于树的结构是可以单向化的,不妨设 fxf_xfx​ 为经过 x→fa(x)x\rightarrow fa(x)x→fa(x) 的期望步数, gxg_xgx​ 为经过 fa(x)→xfa(x)\rightarrow xfa(x)→x 的期望步数。这里又要借鉴前面例题9的经验,计算 fxf_xfx​ 时,起点是 xxx ,那么情况有二:一是直接走到 fa(x)fa(x)fa(x) ,二是走到子节点,回来,走到 fa(x).fa(x).fa(x). 因此转移方程是:

fu=1degu+1degu×∑v∈son(u)(fv+fu+1)=degu+∑v∈son(u)fvf_u=\frac{1}{deg_u}+\frac{1}{deg_u}\times\sum\limits_{v\in son(u)}(f_v+f_u+1)=deg_u+\sum\limits_{v\in son(u)}f_vfu​=degu​1​+degu​1​×v∈son(u)∑​(fv​+fu​+1)=degu​+v∈son(u)∑​fv​
化简方法可以直接参考前面的例题5,不再赘述。

现在用类似的方法考虑 gxg_xgx​ ,情况有三:直接走到 xxx ;走到 fa(fa(x))fa(fa(x))fa(fa(x)) 然后回头;走到 fa(x)fa(x)fa(x) 的其他子节点然后回来。那么转移方程如下:
gu=1degfau+1degfau×(gfau+gu+1)+1degfau×∑v∈son(fa(u)),v≠u(fv+gu+1)g_u=\frac{1}{deg_{fa_u}}+\frac{1}{deg_{fa_u}}\times(g_{fa_u}+g_u+1)+\frac{1}{deg_{fa_u}}\times\sum\limits_{v\in son(fa(u)),v\neq u} (f_v+g_u+1)gu​=degfau​​1​+degfau​​1​×(gfau​​+gu​+1)+degfau​​1​×v∈son(fa(u)),v​=u∑​(fv​+gu​+1)
这个就复杂一些了,具体写一下它的化简方法:(以下推导过程中 fa(u)fa(u)fa(u) 简记为 fafafa)

gu=1degfa+1degfa×(gfa+gu+1)+1degfa×∑v∈son(fa),v≠u(fv+gu+1)=1+degfa−1degfa×gu+1degfa×gfa+1degfa∑v∈son(fa)fv−fu=1+degfa−1degfa×gu+1degfa×gfa+1degfa(ffa−degfa−fu)=degfa−1degfa×gu+1degfa×gfa+1degfa(ffa−fu)∴gu=gfa+ffa−fu.\begin{aligned} g_u&=\frac{1}{deg_{fa}}+\frac{1}{deg_{fa}}\times(g_{fa}+g_u+1)+\frac{1}{deg_{fa}}\times\sum\limits_{v\in son(fa),v\neq u} (f_v+g_u+1)\\ &=1+\frac{deg_{fa}-1}{deg_{fa}}\times g_u+\frac{1}{deg_{fa}}\times g_{fa}+\frac{1}{deg_{fa}}\sum\limits_{v\in son(fa)}f_v-f_u\\ &=1+\frac{deg_{fa}-1}{deg_{fa}}\times g_u+\frac{1}{deg_{fa}}\times g_{fa}+\frac{1}{deg_{fa}}(f_{fa}-deg_{fa}-f_u)\\ &=\frac{deg_{fa}-1}{deg_{fa}}\times g_u+\frac{1}{deg_{fa}}\times g_{fa}+\frac{1}{deg_{fa}}(f_{fa}-f_u)\\ \therefore g_u&=g_{fa}+f_{fa}-f_u. \end{aligned} gu​∴gu​​=degfa​1​+degfa​1​×(gfa​+gu​+1)+degfa​1​×v∈son(fa),v​=u∑​(fv​+gu​+1)=1+degfa​degfa​−1​×gu​+degfa​1​×gfa​+degfa​1​v∈son(fa)∑​fv​−fu​=1+degfa​degfa​−1​×gu​+degfa​1​×gfa​+degfa​1​(ffa​−degfa​−fu​)=degfa​degfa​−1​×gu​+degfa​1​×gfa​+degfa​1​(ffa​−fu​)=gfa​+ffa​−fu​.​

算完这两个函数之后 x−fa(x)x-fa(x)x−fa(x) 的贡献就等于这条边两侧点对数乘以 (fx+gx)(f_x+g_x)(fx​+gx​) ,总和除以 n2n^2n2 就是答案了。
核心代码如下:

void dfs1(int now,int fa){int i,temp;f[now] = d[now];siz[now] = 1;for(i = head[now];~i;i = e[i].nxt){temp = e[i].to;if(temp == fa) continue;dfs1(temp,now);f[now] = (f[now] + f[temp]) % mod;siz[now] += siz[temp];}
}
void dfs2(int now,int fa){int i,temp;if(now ^ 1) g[now] = (g[fa] + f[fa] - f[now] + mod) % mod;res = (res + siz[now] * (n - siz[now]) % mod * (f[now] + g[now]) % mod) % mod;for(i = head[now];~i;i = e[i].nxt){temp = e[i].to;if(temp == fa) continue;dfs2(temp,now);}
}

这里需要注意的是,尽管根节点没有父亲,仍然要如常计算它的 fff 函数值用于转移。

例题12:概率充电器

(洛谷P4284)

由于每一个元件通电的贡献都是1,所以这题实际上就是求每一个元件通电的概率的和。
元件通电有三种方式:自身通电,被子节点通电,被父节点通电。
设最终元件 iii 被通电的概率是 fi.f_i.fi​. 考虑把前两种放一起计算,自身通不了电就让子节点给自己通电,初始化 fu=puf_u=p_ufu​=pu​ ,从子节点转移 fu=∑v∈son(u)(1−fu)×fv×eu,v.f_u=\sum\limits_{v\in son(u)}(1-f_u)\times f_v\times e_{u,v}.fu​=v∈son(u)∑​(1−fu​)×fv​×eu,v​.
然后是被父节点通电的情况,模仿上面的正向再扫一遍就行了。
但这样实际是错的,因为可能出现一个点给父节点通电之后自己又被父节点给通电了的情况,这时候的概率是重复的,所以要求被父节点通电时,这个父节点通电的概率必须是没有算被该子节点时的概率。
具体来说,对于 uuu 的一个子节点 vvv ,不妨假设它是最后一个更新 uuu 的,有 fu=fu′+(1−fu′)×fv×eu,vf_u=f'_u+(1-f'_u)\times f_v\times e_{u,v}fu​=fu′​+(1−fu′​)×fv​×eu,v​ ,此时的 fu′f'_ufu′​ 就是排除了 vvv 的贡献时 uuu 通电的概率,用它更新 fvf_vfv​ ,可得 fv=fv+(1−fv)×fu′×eu,vf_v=f_v+(1-f_v)\times f'_u\times e_{u,v}fv​=fv​+(1−fv​)×fu′​×eu,v​ ,而 fu′f'_ufu′​ 只需要移项就可以表示了。
核心代码如下:

void dfs1(int now,int fa){int i,temp;for(i = head[now];~i;i = e[i].nxt){temp = e[i].to;if(temp == fa) continue;dfs1(temp,now);f[now] += (1 - f[now]) * f[temp] * e[i].c;}
}
void dfs2(int now,int fa){int i,temp;for(i = head[now];~i;i = e[i].nxt){temp = e[i].to;if(temp == fa) continue;if(fabs(1.0 - f[temp] * e[i].c) > eps){double w = (f[now] - f[temp] * e[i].c) / (1.0 - f[temp] * e[i].c);f[temp] += (1 - f[temp]) * w * e[i].c;}dfs2(temp,now);}
}

例题13:Shrinking Tree

一个看着非常离谱的题。
为了能够求解,最终想要剩下哪一个点就是哪一个点作为根。
一种合并的方案可以看作是一种边的排列,只需要除以 (n−1)!(n-1)!(n−1)! 就能得到想要的情况出现的概率。现在考虑怎么从这个排列入手计算。对于一个点,要想保留它的编号,需要讨论它的子树内边排列的顺序。具体来说,设 fu,if_{u,i}fu,i​ 表示对于 uuu 子树内的这些边,后 iii 条合并时需要确定编号的总概率,对于每一个子节点 vvv ,再单独设一个意义相同的数组 gig_igi​ 用来帮助计算这个子树内产生的贡献。
现在对于一对父子 u−vu-vu−v ,对于一个特定的 iii ,再枚举这条边在这个排列里的倒数位置 jjj 。如果 j≤ij\leq ij≤i ,说明在这条边被收起之前编号已经确定,合并的时候必须要选 uuu ;否则可以随便选(这也可以是看作什么时候根节点被合并到当前的节点)。因此有如下的形式:

for(k = head[now];~k;k = e[k].nxt){temp = e[k].to;if(temp == fa) continue;dfs(temp,now);memset(g,0,sizeof(g)); for(i = 0;i <= siz[temp];i++){for(j = 1;j <= siz[temp];j++){if(j <= i) g[i] += f[temp][j - 1] / 2;else g[i] += f[temp][i];}}//...
}

现在考虑怎么用 ggg 进行更新。枚举之前的子树里选择了 iii 条边的编号,在当前子树里面选择了 jjj 条边的编号,更新时需要保持各自内部顺序不变的同时,随机合并的和被选定的仍然是相互分开的,于是有 fu,i+j=fu,i+j+fu,i×Ci+ji×Csizu−i+sizv−jsizu−if_{u,i+j}=f_{u,i+j}+f_{u,i}\times C_{i+j}^i\times C_{siz_u-i+siz_v-j}^{siz_u-i}fu,i+j​=fu,i+j​+fu,i​×Ci+ji​×Csizu​−i+sizv​−jsizu​−i​ ( sizusiz_usizu​ 指的是当前的子树大小而不是总的子树大小),为了防止重复需要再开一个数组维护转移。
这一部分核心代码如下:

memset(h,0,sizeof(h));
for(i = siz[now] - 1;i >= 0;i--){for(j = siz[temp];j >= 0;j--){h[i + j] += f[now][i] * g[j] * C[i + j][i] * C[siz[now] - i - 1 + siz[temp] - j][siz[now] - i - 1];//这里之所以-1是因为初始化siz[now]=1}
}
siz[now] += siz[temp];
for(i = 0;i < siz[now];i++) f[now][i] = h[i];

总结一下,例题11和12表现了树上期望dp的一个难点(或者说是树形dp自身的一种特性),即从两个方向更新,前者侧重于如何完成计算,后者侧重于如何避免算重。例题13则说明,如果问题很复杂,要完成定向再去进行对应的计算,而且如果一个子树内的情况很复杂,可能需要先把当前节点放进子节点操作之后再合并(换句话说,就是把子节点和父节点合并、子节点之间的合并分成两个独立的步骤)。

期望dp+优化

这部分的难点其实就跟期望dp本身没有很大的关系了,算是收集补完。

例题14:Inversions After Shuffle

(CF749E)

显然,由于给定的是一个排列,所以每一对数不是逆序对就是正序对。
关于修改区间求逆序对的问题早已不陌生了。这题所谓的随机重排可以直接看作是对于每一对被选中的数,都有一半的概率被交换先后顺序,有一半的概率不被交换先后顺序,也就是说,每一对被选中的正序对可以产生12\frac{1}{2}21​ 的贡献,逆序对可以产生−12-\frac{1}{2}−21​ 的贡献,由于区间修改只影响完全在区间内的逆序对个数,问题就变成了求这对数被选中的概率。一共有 n(n+1)2\frac{n(n+1)}{2}2n(n+1)​ 个可选区间(千万不要组合数学上头) ,选中点对 (i,j)(i,j)(i,j) 就相当于从 [j,n][j,n][j,n] 当中选右端点,从 [1,i][1,i][1,i] 当中选左端点。于是最终的期望就是 i(n−j+1)n(n+1)\frac{i(n-j+1)}{n(n+1)}n(n+1)i(n−j+1)​ ,只需要加上正负就行了。套个树状数组即可通过。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 1e5 + 1;
double res,sum;
int n,a[N];
struct yjx{double tre[N];#define lowbit(x) (x & (-x))void modify(int x,double c){for(;x <= n;x += lowbit(x)){tre[x] += c;}}double query(int x){double ret = 0;for(;x;x -= lowbit(x)){ret += tre[x]; }return ret;}
}bit1,bit2;
int main(){int i;scanf("%d",&n);for(i = 1;i <= n;i++){scanf("%d",&a[i]);sum += bit1.query(n) - bit1.query(a[i]);res -= (n - i + 1) * bit2.query(a[i]);res += (n - i + 1) * (bit2.query(n) - bit2.query(a[i]));bit1.modify(a[i],1.0);bit2.modify(a[i],1.0 * i);}res /= (1.0 * n * (n + 1));printf("%.11lf\n",sum - res); return 0;
}

例题15:小 Y 和恐怖的奴隶主

(洛谷P4007)

由于随从血量不超过3,所以可以直接设 fi,a,b,cf_{i,a,b,c}fi,a,b,c​ 表示攻击了 iii 次后,剩下 a/b/ca/b/ca/b/c 个有1/2/3点血量的随从的概率。 这个转移方式比较简单。
现在考虑怎么算期望伤害,根据线性性,总的期望伤害就是每一个 fi,a,b,cf_{i,a,b,c}fi,a,b,c​ (单次攻击)乘以对应的概率 1a+b+c+1\frac{1}{a+b+c+1}a+b+c+11​ ,考虑到这个数据范围,加上此题的系数与函数值无关,所以可以用矩阵加速。在 m=3m=3m=3 的情况下,可能的状态一共只有166种(加上一个状态计算boss的血量),所以复杂度是 O(1663logn)O(166^3logn)O(1663logn) ,又由于多组测试数据,可以直接像快速幂一样预处理出所有 2i2^i2i 的情况,时间复杂度变为 O(1663logn+T1662logn).O(166^3logn+T166^2logn).O(1663logn+T1662logn).

核心代码如下:

signed main(){for(i = 0;i <= p;i++){if(m > 1) u = p - i; else u = 0;for(j = 0;j <= u;j++){if(m > 2) v = p - i - j;else v = 0;for(k = 0;k <= v;k++){id[i][j][k] = ++cnt;}}}++cnt;A[0].mat[cnt][cnt] = 1;A[0].l = A[0].r = cnt;for(i = 0;i <= p;i++){if(m > 1) u = p - i;else u = 0;for(j = 0;j <= u;j++){if(m > 2) v = p - i - j;else v = 0;for(k = 0;k <= v;k++){int x = id[i][j][k],op = (i + j + k < p);long long inv = Inv[i + j + k + 1];A[0].mat[x][id[i - 1][j][k]] = 1ll * i * inv % mod;if(m == 2){if(j) A[0].mat[x][id[i + 1][j - 1 + op][k]] = 1ll * j * inv % mod;}else if(m == 3){if(j) A[0].mat[x][id[i + 1][j - 1][k + op]] = 1ll * j * inv % mod;if(k) A[0].mat[x][id[i][j + 1][k - 1 + op]] = 1ll * k * inv % mod;}A[0].mat[x][x] = A[0].mat[x][cnt] = inv;}}}mi[0] = 1;for(i = 1;i <= 60;i++){A[i] = A[i - 1] * A[i - 1];mi[i] = mi[i - 1] * 2;}while(tt--){scanf("%lld",&n);for(i = 1;i <= cnt;i++) res[i] = 0;res[id[m == 1][m == 2][m == 3]] = 1;for(i = 0;i <= 60;i++){if(n & mi[i]) calc(i);}printf("%lld\n",res[cnt]);}return 0;
}

完结撒邮票
之前还有两个坑,一个SA,一个网络流(下),希望至少能把SA填完吧233

期望/概率dp 学习报告相关推荐

  1. LightOJ - 1038 Race to 1 Again 基础期望概率 dp

    传送门 刚刚学习期望&概率 我们设数X的期望改变次数为P[X] 如果要求X的期望,很容易想到找x的因子; 可以得到下式  ,cnt为X因子个数,ai为X的因子 可以这么理解,当因子ai为1时, ...

  2. bzoj3470 Freda's Walk (期望概率DP)

    bzoj3470 Freda's Walk 原题地址:http://www.lydsy.com/JudgeOnline/problem.php?id=3470 题意: 有向无环图,求从点0出发,走到一 ...

  3. 【BZOJ 4832】 [Lydsy2017年4月月赛] 抵制克苏恩 期望概率dp

    打记录的题打多了,忘了用开维记录信息了......我们用f[i][j][l][k]表示已经完成了i次攻击,随从3血剩j个,2血剩l个,1血剩k个,这样我们求出每个状态的概率,从而求出他们对答案的贡献并 ...

  4. 【LOJ6178】 「美团 CodeM 初赛 Round B」景区路线规划 期望概率DP

    题目链接 LOJ 题解 考虑进行Dp,我们设 f[i][j] f [ i ] [ j ] f[i][j]表示到达节点 i i i消耗了j" role="presentation&q ...

  5. luoguP4206 [NOI2005]聪聪与可可 期望概率DP

    首先,分析一下这个猫和鼠 猫每局都可以追老鼠一步或者两步,但是除了最后的一步,肯定走两步快些.... 既然猫走的步数总是比老鼠多,那么它们的距离在逐渐缩小(如果这题只能走一步反而不能做了...) 猫不 ...

  6. [USACO Hol10] 臭气弹 图上期望概率dp 高斯

    记住一开始和后来的经过是两个事件因此概率可以大于一 #include<cstdio> #include<iostream> #include<cstdlib> #i ...

  7. BZOJ4008. [HNOI2015]亚瑟王 期望概率dp

    看到这道题想什么? 一个好转移的状态由于T最多444所以把每个点控制在O(400000)以内,所以对于n和r最多乘一次因此猜f[n][r],f[r][n],首先一轮一轮的搞不好转移,那么先想一想f[n ...

  8. UvaLive6441(期望概率dp)

    1.涉及负数时同时维护最大和最小,互相转移. 2.考场上最大最小混搭转移WA,赛后发现如果是小的搭小的,大的搭大的就可过,类似这种: db a = (C[i] - W[i]) * dp1[i - 1] ...

  9. 【原创】概率DP总结 by kuangbin

    概率DP主要用于求解期望.概率等题目. 转移方程有时候比较灵活. 一般求概率是正推,求期望是逆推.通过题目可以体会到这点. 首先先推荐几篇参考的论文: <信息学竞赛中概率问题求解初探> & ...

最新文章

  1. 职场中如何与别人高效沟通?
  2. Asp.net导出Excel
  3. Android socket 编程 实现消息推送(一)
  4. 职业学校教的计算机技术,浅谈对职业学校计算机专业数据库教学
  5. 学习笔记--Dubbo
  6. python3 在线工具_Curl转python在线工具
  7. python爬取苏州天气并用excel来保存
  8. 【Java从0到架构师】SpringMVC - 异常处理_拦截器
  9. jQuery Howto: 如何快速创建一个AJAX的加载的图片效果
  10. autohotkey-大漠插件
  11. 【Python笔记】第5章 if语句
  12. 奇技淫巧之dummy网卡
  13. python数据可视化之美源码_Python数据可视化之美-专业图
  14. 同级最强!天玑8200实测成绩放出,iQOO Neo7 SE神机配神U
  15. 不出千元!打造耐用、高效SCSI硬盘系统(转)
  16. 315Mhz RF射频解码 串口输出方案(支持2262/1527多种编码方式)
  17. matlab 生成zc序列,利用zc序列进行简单的帧同步
  18. 解决ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broke
  19. 第一篇博文——与诸位共勉
  20. 工坊实验室 | CALCULATE 的嵌套使用

热门文章

  1. python文件的两种类型是什么意思_Python文件处理里encoding和encode有事区别,bytes类型是什么意思?...
  2. vs+cmake完美编译RTS游戏,类似魔兽争霸源码
  3. 锐捷客户端-您不在许可范围中,请确认您的权限
  4. 基于Grafana的Web监控报警
  5. 科普“知识共享”严重缺失,国内亟待补课
  6. 【NOIP2012】国王游戏
  7. appium java常用函数_AppiumLibrary常用关键字
  8. 聚焦东风汽车,解锁企业上云的正确姿势
  9. Pandas与SQL比较
  10. 单模光纤与多模光纤的区别