原由:在一场训练赛上。有这么一题没作出来。php
题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=6829html
题目大意:有三我的,他们分别有\(X,Y,Z\)块钱(\(1<=X,Y,Z<=1e6\)),钱数最多的(若是不止一个那么随机等几率的选一个)随机等可能的选另外一我的送他一块钱。直到三我的钱数相同为止。输出送钱轮数的指望,若是根本停不下来,输出-1。ios
根据题目的意思,其实就是每次向包里随机加入一枚钱币,直到包里某种钱币数量达到 100。本题的核心是如何计算指望。本题属于标准的动态规划求指望问题。直接套用模板便可。c++
一道”简单“几率DP题,没怎么了解几率DP致使作不出算法
当理解基础的知识之后发现的确比较简单数组
DP 数组定义学习
定义 \(DP[i][j][k]\),表示有 i 枚金币, j 枚银币, k 枚铜币的指望。优化
初值spa
全部的指望都为零。.net
递推方法
使用逆推。
状态转移方程:
AC代码:
// Author : RioTian // Time : 20/12/09 #include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e2 + 10; double dp[N][N][N]; int main() { // freopen("in.txt", "r", stdin); ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int a, b, c; cin >> a >> b >> c; for (int i = 99; i >= a; i--) for (int j = 99; j >= b; j--) for (int k = 99; k >= c; k--) { // 令 t = x + y + z,减小代码量 double t = i + j + k; dp[i][j][k] = i / t * (dp[i + 1][j][k] + 1) + j / t * (dp[i][j + 1][k] + 1) + k / t * (dp[i][j][k + 1] + 1); } // 关于 C++ 的输出控制能够在个人之前博客找到 cout << fixed << setprecision(9) << dp[a][b][c] << endl; }
时间和空间复杂度:
\(O(n^3)\)
固然在训练赛的题解我也提到也能够用蒙特卡洛方法模拟在\(O(n)\)解决,这里就再也不解释了,有兴趣的话能够去题解报告的那篇博客查阅
作完上面那道几率DP题,算是入了门,但仅仅入门不够的须要增强,因此开始学习各位大神的博客。
下面先说一下9974dalao的总结:
不少几率题总逃不开用dp转移。
指望题老是倒着推过来的,几率是正着推的,多作题就会理解其中的缘由
有些指望题要用到有关 几率 或 指望的常见\(\color{red}公式或思想\)
遇到dp转移方程(组)中有环的,多半逃不出\(\color{red}高斯消元\)(手动 和 写代码 两种)
这套题中还有道树上的dp转移,还用dfs对方程迭代解方程, 真是大开眼界了
固然还有与各类算法结合的题,关键仍是要\(\color{red}学会分析\)
当公式或计算时有除法时, 特别要注意\(\color{red}分母是否为零\)
下面几个题型来自Oi wiki 和 kuangbin 的总结
这类题目采用顺推,也就是从初始状态推向结果。同通常的 DP 相似的,难点依然是对状态转移方程的刻画,只是这类题目通过了几率论知识的包装。
题目大意:袋子里有 w 只白鼠和 b 只黑鼠,公主和龙轮流从袋子里抓老鼠。谁先抓到白色老鼠谁就赢,若是袋子里没有老鼠了而且没有谁抓到白色老鼠,那么算龙赢。公主每次抓一只老鼠,龙每次抓完一只老鼠以后会有一只老鼠跑出来。每次抓的老鼠和跑出来的老鼠都是随机的。公主先抓。问公主赢的几率。
设 \(f_{i,j}\) 为轮到公主时袋子里有 \(i\) 只白鼠, \(j\) 只黑鼠,公主赢的几率。初始化边界, \(f_{0,j}=0\) 由于没有白鼠了算龙赢, \(f_{i,0}=1\) 由于抓一只就是白鼠,公主赢。
考虑 \(f_{i,j}\) 的转移:
考虑公主赢的几率,第二种状况不参与计算。而且要保证后两种状况合法,因此还要判断 \(i,j\) 的大小,知足第三种状况至少要有 3 只黑鼠,知足第四种状况要有 1 只白鼠和 2 只黑鼠。
// Author : RioTian // Time : 20/12/10 #includeusing namespace std; typedef long long ll; const int N = 1e3 + 10; int w, b; double dp[N][N]; int main() { // freopen("in.txt", "r", stdin); ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); cin >> w >> b; memset(dp, 0.0, sizeof dp); for (int i = 1; i <= w; ++i) dp[i][0] = 1; for (int j = 1; j <= b; ++j) dp[0][j] = 0; for (int i = 1; i <= w; ++i) for (int j = 1; j <= b; ++j) { dp[i][j] += (double)i / (i + j); if (j >= 3) dp[i][j] += (double)j / (i + j) * (j - 1) / (i + j - 1) * (j - 2) / (i + j - 2) * dp[i][j - 3]; if (i >= 1 && j >= 2) dp[i][j] += (double)j / (i + j) * (j - 1) / (i + j - 1) * i / (i + j - 2) * dp[i - 1][j - 2]; } cout << fixed << setprecision(9) << dp[w][b] << endl; }
题目大意:一个软件有 s 个子系统,会产生 n 种 bug。某人一天发现一个 bug,这个 bug 属于某种 bug 分类,也属于某个子系统。每一个 bug 属于某个子系统的几率是 \(\frac{1}{s}\) ,属于某种 bug 分类的几率是 \(\frac{1}{n}\) 。求发现 n 种 bug,且 s 个子系统都找到 bug 的指望天数。
令 \(f_{i,j}\) 为已经找到 \(i\) 种 bug 分类, \(j\) 个子系统的 bug,达到目标状态的指望天数。这里的目标状态是找到 \(n\) 种 bug 分类, \(j\) 个子系统的 bug。那么就有 \(f_{n,s}=0\) ,由于已经达到了目标状态,不须要用更多的天数去发现 bug 了,因而就以目标状态为起点开始递推,答案是 \(f_{0,0}\) 。
考虑 \(f_{i,j}\) 的状态转移:
再根据指望的线性性质,就能够获得状态转移方程:
AC 代码
// Author : RioTian // Time : 20/12/10 #include <cstdio> #include <cstring> #include <iostream> using namespace std; const int N = 1e3 + 10; int n, s; double dp[N][N]; int main() { while (cin >> n >> s) { dp[n][s] = 0; for (int i = n; i >= 0; i--) for (int j = s; j >= 0; j--) { if (i == n && j == s) //跳过初始值 continue; double p1, p2, p3, p4; p1 = (double)i * j / (n * s); // dp[i][j]; p2 = (double)(n - i) * j / (n * s); // dp[i+1][j]; p3 = (double)i * (s - j) / (n * s); // dp[i][j+1]; p4 = (double)(n - i) * (s - j) / (n * s); dp[i][j] = (1 + p2 * dp[i + 1][j] + p3 * dp[i][j + 1] + p4 * dp[i + 1][j + 1]) / (1 - p1); } printf("%.4f\n", dp[0][0]); } }
题目大意:牛牛要上 \(n\) 个时间段的课,第 \(i\) 个时间段在 \(c_i\) 号教室,能够申请换到 \(d_i\) 号教室,申请成功的几率为 \(p_i\) ,至多能够申请 \(m\) 节课进行交换。第 \(i\) 个时间段的课上完后要走到第 \(i+1\) 个时间段的教室,给出一张图 \(v\) 个教室 \(e\) 条路,移动会消耗体力,申请哪几门课程可使他因在教室间移动耗费的体力值的总和的指望值最小,也就是求出最小的指望路程和。
对于这个无向连通图,先用 Floyd 求出最短路,为后续的状态转移带来便利。以移动一步为一个阶段(从第 \(i\) 个时间段到达第 \(i+1\) 个时间段就是移动了一步),那么每一步就有 \(p_i\) 的几率到 \(d_i\) ,不过在全部的 \(d_i\) 中只能选 \(m\) 个,有 \(1-p_i\) 的几率到 \(c_i\) ,求出在 \(n\) 个阶段走完后的最小指望路程和。
定义 \(f_{i,j,0/1}\) 为在第 \(i\) 个时间段,连同这一个时间段已经用了 \(j\) 次换教室的机会,在这个时间段换(1)或者不换(0)教室的最小指望路程和,那么答案就是 \(max \{f_{n,i,0},f_{n,i,1}\} ,i\in[0,m]\) 。注意边界 \(f_{1,0,0}=f_{1,1,1}=0\) 。
考虑 \(f_{i,j,0/1}\) 的状态转移:
AC代码
#include <bits/stdc++.h> using namespace std; const int maxn = 2010; int n, m, v, e; int f[maxn][maxn], c[maxn], d[maxn]; double dp[maxn][maxn][2], p[maxn]; int main() { scanf("%d %d %d %d", &n, &m, &v, &e); for (int i = 1; i <= n; i++) scanf("%d", &c[i]); for (int i = 1; i <= n; i++) scanf("%d", &d[i]); for (int i = 1; i <= n; i++) scanf("%lf", &p[i]); for (int i = 1; i <= v; i++) for (int j = 1; j < i; j++) f[i][j] = f[j][i] = 1e9; int u, V, w; for (int i = 1; i <= e; i++) { scanf("%d %d %d", &u, &V, &w); f[u][V] = f[V][u] = min(w, f[u][V]); } for (int k = 1; k <= v; k++) for (int i = 1; i <= v; i++) for (int j = 1; j < i; j++) if (f[i][k] + f[k][j] < f[i][j]) f[i][j] = f[j][i] = f[i][k] + f[k][j]; for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) dp[i][j][0] = dp[i][j][1] = 1e9; dp[1][0][0] = dp[1][1][1] = 0; for (int i = 2; i <= n; i++) for (int j = 0; j <= min(i, m); j++) { dp[i][j][0] = min(dp[i - 1][j][0] + f[c[i - 1]][c[i]], dp[i - 1][j][1] + f[c[i - 1]][c[i]] * (1 - p[i - 1]) + f[d[i - 1]][c[i]] * p[i - 1]); if (j != 0) { dp[i][j][1] = min(dp[i - 1][j - 1][0] + f[c[i - 1]][d[i]] * p[i] + f[c[i - 1]][c[i]] * (1 - p[i]), dp[i - 1][j - 1][1] + f[c[i - 1]][c[i]] * (1 - p[i - 1]) * (1 - p[i]) + f[c[i - 1]][d[i]] * (1 - p[i - 1]) * p[i] + f[d[i - 1]][c[i]] * (1 - p[i]) * p[i - 1] + f[d[i - 1]][d[i]] * p[i - 1] * p[i]); } } double ans = 1e9; for (int i = 0; i <= m; i++) ans = min(dp[n][i][0], min(dp[n][i][1], ans)); printf("%.2lf", ans); return 0; }
比较这两个问题能够发现,DP 求指望题目在对具体是求一个值或是最优化问题上会对方程获得转移方式有一些影响,但不管是 DP 求几率仍是 DP 求指望,老是离不开几率知识和列出、化简计算公式的步骤,在写状态转移方程时须要思考的细节也相似。
题目大意:给出一个 \(n*m\) 的矩阵区域,一个机器人初始在第 \(x\) 行第 \(y\) 列,每一步机器人会等几率地选择停在原地,左移一步,右移一步,下移一步,若是机器人在边界则不会往区域外移动,问机器人到达最后一行的指望步数。
在 \(m=1\) 时每次有 \(\frac{1}{2}\) 的几率不动,有 \(\frac{1}{2}\) 的几率向下移动一格,答案为 \(2\cdot (n-x)\) 。
设 \(f_{i,j}\) 为机器人机器人从第 i 行第 j 列出发到达第 \(n\) 行的指望步数,最终状态为 \(f_{n,j}=0\) 。
因为机器人会等几率地选择停在原地,左移一步,右移一步,下移一步,考虑 \(f_{i,j}\) 的状态转移:
在行之间因为只能向下移动,是知足无后效性的。在列之间能够左右移动,在移动过程当中可能产生环,不知足无后效性。
将方程变换后能够获得:
因为是逆序的递推,因此每个 \(f_{i+1,j}\) 是已知的。
因为有 \(m\) 列,因此右边至关因而一个 \(m\) 行的列向量,那么左边就是 \(m\) 行 \(m\) 列的矩阵。使用增广矩阵,就变成了 m 行 m+1 列的矩阵,而后进行 高斯消元 便可解出答案。
AC代码:这个有点绕,代码博主没有本身写,copy下dalao的代码了(侵权删)。
#include <bits/stdc++.h> using namespace std; const int maxn = 1e3 + 10; double a[maxn][maxn], f[maxn]; int n, m; void solve(int x) { memset(a, 0, sizeof a); for (int i = 1; i <= m; i++) { if (i == 1) { a[i][i] = 2; a[i][i + 1] = -1; a[i][m + 1] = 3 + f[i]; continue; } else if (i == m) { a[i][i] = 2; a[i][i - 1] = -1; a[i][m + 1] = 3 + f[i]; continue; } a[i][i] = 3; a[i][i + 1] = -1; a[i][i - 1] = -1; a[i][m + 1] = 4 + f[i]; } for (int i = 1; i < m; i++) { double p = a[i + 1][i] / a[i][i]; a[i + 1][i] = 0; a[i + 1][i + 1] -= a[i][i + 1] * p; a[i + 1][m + 1] -= a[i][m + 1] * p; } f[m] = a[m][m + 1] / a[m][m]; for (int i = m - 1; i >= 1; i--) f[i] = (a[i][m + 1] - f[i + 1] * a[i][i + 1]) / a[i][i]; } int main() { scanf("%d %d", &n, &m); int st, ed; scanf("%d %d", &st, &ed); if (m == 1) { printf("%.10f\n", 2.0 * (n - st)); return 0; } for (int i = n - 1; i >= st; i--) { solve(i); } printf("%.10f\n", f[ed]); return 0; }
论文学习:
关于一些练习题: