本文学习自 Sengxian 学长的博客php
以前也在CF上写了一些几率DP的题并作过总结html
建议阅读完本文再去接着阅读这篇文章:Herenode
单纯只用到几率的题并非不少,从现有的 OI/ACM 比赛中来看,大多数题目须要几率与指望结合起来(指望就是用几率定义的),因此本文主要讲述指望 DP。git
指望 DP 有一些固定的方法,这里分多种方法来说述。学习
题意:code
给定一个起点为 \(1\),终点为 \(n\) 的有向无环图。到达每个顶点时,若是有 \(K\) 条离开该点的道路,能够选择任意一条道路离开该点,而且走向每条路的几率为 \(\frac 1 K\)。问你从 \(1\) 出发走到 \(n\) 的路径指望总长度是多少。htm
这道题的终点很明确,那就是走到 \(n\) 即中止。对于指望 DP,咱们通常采用逆序的方式来定义状态,即考虑从当前状态到达终点的指望代价。由于在大多数状况下,终点不惟一,而起点是惟一的。
咱们定义 \(dp(i)\)为从 \(i\) 出发走到终点 \(n\) 的路径指望总长度,根据全指望公式,获得(设 \(G_i\)为从 \(i\) 的边的集合):blog
由于这是一个有向无环图,每一个点须要其能到达的点的状态,故咱们采用拓扑序的逆序进行计算便可。ip
【AC Code】
const int N = 100000, M = 2 * N; int n, m; struct node { int v, w;}; vector<node>e[N]; int d[N]; // 出度 double f[N]; // dp double dfs(int u) { if (f[u] >= 0)return f[u]; f[u] = 0; for (auto [v, w] : e[u]) { // tuple 需开启 C++17 f[u] += (w + dfs(v)) / d[u]; } return f[u]; } int main() { cin.tie(nullptr)->sync_with_stdio(false); memset(f, -1, sizeof(f)); cin >> n >> m; for (int i = 0; i < m; ++i) { int u, v, w; cin >> u >> v >> w; e[u].push_back(node{v, w}); d[u]++;//出度++ } cout << fixed << setprecision(2) << dfs(1); }
根据指望的线性性质,\(E[aX +bY]=aE[X] + bE[Y]\)。因此另一种求指望的方式是分别求出每一种代价产生的指望贡献,而后相加获得答案。在本题中,路径指望总长度等于每条边产生的指望贡献之和。而每条边产生又等于通过这条边的指望次数乘这条边的代价。因此,咱们只须要算每条边的指望通过次数便可。
边 \((u,v,w)\) 的指望通过次数是很差直接算的,但若是咱们能算得点 \(u\) 的指望通过次数为 \(dp(u,v)\),那么边 \((u,v,w)\) 的指望通过次数是 \(dp(u)*\frac1{|G_u|}\) ,对答案的贡献就是 \(w*dp(u)*\frac1{|G_u|}\)
如何计算点 \(u\) 的指望通过次数 \(dp(u)\)呢?咱们依然考虑 DP 的方式,首先有 \(dp(u) = 1\),转移采起刷表的方式:
在用边 \(e\) 刷表的同时,边 \(e\) 的贡献就能够计算了,十分简洁。由于这种方法计算答案十分的便捷,并且适用范围广,因此这种『利用指望的线性性质,单独计算贡献的方法』是咱们计算指望首选的方法。
【AC Code】这里贴 Sengxian 学长的代码
typedef long long ll; inline int readInt() { static int n, ch; n = 0, ch = getchar(); while (!isdigit(ch)) ch = getchar(); while (isdigit(ch)) n = n * 10 + ch - '0', ch = getchar(); return n; } const int MAX_N = 100000 + 3, MAX_M = MAX_N * 2; struct edge { edge *next; int to, cost; edge(edge *next = NULL, int to = 0, int cost = 0): next(next), to(to), cost(cost) {} } pool[MAX_M], *pit = pool, *first[MAX_N]; int n, m, deg[MAX_N], outDeg[MAX_N]; double f[MAX_N]; void solve() { static int q[MAX_N]; int l = 0, r = 0; q[r++] = 0; double ans = 0; f[0] = 1.0; while (r - l >= 1) { int u = q[l++]; for (edge *e = first[u]; e; e = e->next) { f[e->to] += f[u] / outDeg[u]; ans += f[u] * e->cost / outDeg[u]; if (--deg[e->to] == 0) q[r++] = e->to; } } printf("%.2f\n", ans); } int main() { n = readInt(), m = readInt(); for (int i = 0, u, v, w; i < m; ++i) { u = readInt() - 1, v = readInt() - 1, w = readInt(); first[u] = new (pit++) edge(first[u], v, w); deg[v]++, outDeg[u]++; } solve(); }
接着咱们考虑 Codeforces 518D 这道题,以便体会方法二的好处。
题意:有 \(n\) 我的排成一列,每秒中队伍最前面的人有 \(p\) 的几率走上电梯(一旦走上就不会下电梯),或者有 \(1-p\) 的几率不动。问你 \(T\) 秒事后,在电梯上的人的指望。
在本题这样一个状况中,方法一是用不了的,由于咱们的结束状态不明确。
若是 \(X\) 是离散的随机变量,输出值为 \(x_1,x_2,...\),输出值相应的几率为 \(p_1,p_2,...\),那么指望值是一个无限数列的和(若是不收敛,那么指望不存在):
在本题中,若是设 \(dp(i,j)\) 为 \(i\) 秒事后,电梯上有 \(j\) 我的的几率,那么答案是:
因此咱们只须要求 \(dp(i, j)\) 就能够了,初始值 \(dp(0, 0) = 1\) 就能够了,仍然是采用刷表法:
【AC Code】
const int N = 2e3 + 10; double p, dp[N][N]; int main() { cin.tie(nullptr)->sync_with_stdio(false); int n, t; cin >> n >> p >> t; dp[0][0] = 1; for (int i = 0; i < t; ++i) { dp[i + 1][n] += dp[i][n]; for (int j = 0; j < n; ++j) if (dp[i][j] > 1e-10) { dp[i + 1][j + 1] += dp[i][j] * p; dp[i + 1][j] += dp[i][j] * (1 - p); } } double ans = 0; for (int i = 0; i <= n; ++i) ans += i * dp[t][i]; cout << fixed << setprecision(6) << ans; }
那么以前提到的适用范围广的方法二,是否能在这里用呢?答案是确定的。
延续方法三的 DP,咱们不妨将状态之间的转移抽象成边,只不过只有 \(dp(i, j)\) 到 \(dp(i + 1, j + 1)\) 的边才有为 \(1\) 的边权,其他都为 \(0\)。由于这个 DP 涵盖了全部可能出现的状况,因此咱们仍然能够利用指望的线性性质,在刷表的过程当中进行计算答案。
本题中,没有直观的边的概念,可是咱们能够将状态之间的转移抽象成边,因为 \(dp(i, j)\)到 \(dp(i + 1, j + 1)\) 这一个转移是对答案有 \(1\) 的贡献的,因此咱们将它们之间的边权赋为 \(1\)。
这一题将方法二抽象化了,实际上大多数题并不是是直观的,而是这种抽象的形式。
const int N = 2e3 + 10; double p, dp[N][N]; int main() { cin.tie(nullptr)->sync_with_stdio(false); int n, t; cin >> n >> p >> t; dp[0][0] = 1; double ans = 0; for (int i = 0; i < t; ++i) { dp[i + 1][n] += dp[i][n]; for (int j = 0; j < n; ++j) if (dp[i][j] > 1e-10) { dp[i + 1][j + 1] += dp[i][j] * p; dp[i + 1][j] += dp[i][j] * (1 - p); ans += dp[i][j] * p; } } cout << fixed << setprecision(6) << ans; }
题意:给定一个序列,一些位置未肯定(是 \(\texttt{o}\) 与 \(\texttt{x}\) 的概率各占 \(\%50%\))。对于一个 \(\texttt{ox}\) 序列,连续 \(x\) 长度的 \(\texttt{o}\) 会获得 \(x^2\) 的收益,请问最终获得的序列的指望收益是多少?
这个题若是一段一段的处理,实际上并非很好作。咱们观察到 \((x + 1) ^ 2 - x ^ 2 = 2x + 1\),那么根据指望的线性性质,咱们能够单独算每个字符的贡献。咱们设 \(dp_i\) 为考虑前 ii 个字符的指望得分,\(l_i\) 为以 \(i\) 为结尾的 comb 的指望长度,\(Comb_i\) 为第 \(i\)个字符,那么有 3 种状况:
对于前两种状况,实际上是很是直观的,对于第三种状况,其实是求了一个平均长度。例如 ?oo
,两种状况的长度 \(l_i\) 分别为 \([0,1,2]\) 和 \([1,2,3]\) ,可是求了平均以后,长度 \(l_i\) 变成了 \([0.5,1.5,2.5]\) ,这样因为咱们的贡献是一个关于长度的一次多项式 \((2x + 1)\) ,因此长度平均以后,贡献也至关于求了一个平均,天然可以求得正确的得分指望。
【AC Code】
const int N = 3e5 + 10; double dp[N], Comb[N]; int main() { cin.tie(nullptr)->sync_with_stdio(false); int n; string s; cin >> n >> s; for (int i = 0; i < n; ++i) { if (s[i] == 'o') { dp[i] = dp[i - 1] + Comb[i - 1] * 2 + 1; Comb[i] = Comb[i - 1] + 1; } else if (s[i] == 'x') { dp[i] = dp[i - 1]; Comb[i] = 0; } else { dp[i] = dp[i - 1] + (Comb[i - 1] * 2 + 1) / 2; Comb[i] = (Comb[i - 1] + 1) / 2; } } cout << setprecision(4) << fixed << dp[n - 1]; }
思考:若是长度为 \(a\) 的 comb 的贡献为 \(a^3\) 时该如何解决?
Tips:因为 \((a + 1)^3 - a^3 = 3a^3 + 3a + 1\) ,因此咱们要维护 \(a^2\) 和 \(a\) 的指望,注意 \(E_{a^2} \not= E^2_a\),因此维护 \(a^2\) 的指望是必要的。
题意:给定一个序列,每一个位置 \(o\) 的概率为 \(p_i\) ,为 \(x\) 的概率为 \(1-p_i\) 。对于一个 \(\texttt{ox}\) 序列,连续 \(x\) 长度的 \(\texttt{o}\) 会获得 \(x^3\) 的收益,请问最终获得的 \(ox\) 序列的指望收益是多少?
延续例三的思路,咱们仍是分别求每个位置的贡献。根据 \((a + 1)^3 - a^3 = 3a^3 + 3a + 1\),咱们只须要维护 \(l(i)\)为以 \(i\) 为结尾的 comb 的指望长度,\(l_2(i)\)为以 \(i\) 为结尾的 comb 的指望长度的平方。注意 \(E[a^2] \not =E^2[a]\),因此维护 \(a^2\) 的指望是必要的。
int main() { cin.tie(nullptr)->sync_with_stdio(false); int n; double p, l1 = 0, l2 = 0, ans = 0; cin >> n; for (int i = 0; i < n; ++i) { cin >> p; ans += (3 * l2 + 3 * l1 + 1) * p; l2 = (l2 + 2 * l1 + 1) * p; l1 = (l1 + 1) * p; } cout << fixed << setprecision(1) << ans; }
指望 DP 通常来讲有它固定的模式,一种模式是直接 DP,定义状态为到终点指望,采用逆序计算获得答案。一种模式是利用指望的线性性质,对贡献分别计算,这种模式通常要求咱们求出每种代价的指望使用次数,而每种代价每每体如今 DP 的转移之中。最后的两个例题是典型的分离变量,用指望的线性性质计算答案的例子,若是状态过于巨大,那么就得考虑分离随机变量了。
本总结只是解释了几率与指望 DP 的冰山一角,它能够变化无穷,但那些实际上并不仅属于几率与指望 DP,真正核心的内容,仍是逃不出咱们几种方法。
想要深刻了解一些几率的DP的请阅读这篇文章:Here