動態規劃之經典數學指望和機率DP

原由:在一場訓練賽上。有這麼一題沒作出來。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

遞推方法

使用逆推。

狀態轉移方程:

\[s = X + Y + Z\\ dp(i,j,k) = \frac{i}{s}*(dp(i + 1,j,k) + 1) + \frac{j}{s}*dp(i,j + 1,k) + 1) \\+ \frac{j}{s}*dp(i,j,k + 1) + 1)) \]

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 求機率

這類題目採用順推,也就是從初始狀態推向結果。同通常的 DP 相似的,難點依然是對狀態轉移方程的刻畫,只是這類題目通過了機率論知識的包裝。

"例題 Codeforces 148 D Bag of mice"

​ 題目大意:袋子裏有 w 只白鼠和 b 只黑鼠,公主和龍輪流從袋子裏抓老鼠。誰先抓到白色老鼠誰就贏,若是袋子裏沒有老鼠了而且沒有誰抓到白色老鼠,那麼算龍贏。公主每次抓一隻老鼠,龍每次抓完一隻老鼠以後會有一隻老鼠跑出來。每次抓的老鼠和跑出來的老鼠都是隨機的。公主先抓。問公主贏的機率。

\(f_{i,j}\) 爲輪到公主時袋子裏有 \(i\) 只白鼠, \(j\) 只黑鼠,公主贏的機率。初始化邊界, \(f_{0,j}=0\) 由於沒有白鼠了算龍贏, \(f_{i,0}=1\) 由於抓一隻就是白鼠,公主贏。
考慮 \(f_{i,j}\) 的轉移:

  • 公主抓到一隻白鼠,公主贏了。機率爲 \(\frac{i}{i+j}\)
  • 公主抓到一隻黑鼠,龍抓到一隻白鼠,龍贏了。機率爲 \(\frac{j}{i+j}\cdot \frac{i}{i+j-1}\)
  • 公主抓到一隻黑鼠,龍抓到一隻黑鼠,跑出來一隻黑鼠,轉移到 \(f_{i,j-3}\) 。機率爲 \(\frac{j}{i+j}\cdot\frac{j-1}{i+j-1}\cdot\frac{j-2}{i+j-2}\)
  • 公主抓到一隻黑鼠,龍抓到一隻黑鼠,跑出來一隻白鼠,轉移到 \(f_{i-1,j-2}\) 。機率爲 \(\frac{j}{i+j}\cdot\frac{j-1}{i+j-1}\cdot\frac{i}{i+j-2}\)

考慮公主贏的機率,第二種狀況不參與計算。而且要保證後兩種狀況合法,因此還要判斷 \(i,j\) 的大小,知足第三種狀況至少要有 3 只黑鼠,知足第四種狀況要有 1 只白鼠和 2 只黑鼠。

AC 代碼(建議獨立思考之後再閱讀)
// Author : RioTian
// Time : 20/12/10
#include 
   
   
            
   

  
   
  using 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; } 

  

相關習題

DP 求指望

" 例題 POJ2096 Collecting Bugs"

​ 題目大意:一個軟件有 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}\) 的狀態轉移:

  • \(f_{i,j}\) ,發現一個 bug 屬於已經發現的 \(i\) 種 bug 分類, \(j\) 個子系統,機率爲 \(p_1=\frac{i}{n}\cdot\frac{j}{s}\)
  • \(f_{i,j+1}\) ,發現一個 bug 屬於已經發現的 \(i\) 種 bug 分類,不屬於已經發現的子系統,機率爲 \(p_2=\frac{i}{n}\cdot(1-\frac{j}{s})\)
  • \(f_{i+1,j}\) ,發現一個 bug 不屬於已經發現 bug 分類,屬於 \(j\) 個子系統,機率爲 \(p_3=(1-\frac{i}{n})\cdot\frac{j}{s}\)
  • \(f_{i+1,j+1}\) ,發現一個 bug 不屬於已經發現 bug 分類,不屬於已經發現的子系統,機率爲 \(p_4=(1-\frac{i}{n})\cdot(1-\frac{j}{s})\)

再根據指望的線性性質,就能夠獲得狀態轉移方程:

\[\begin{aligned} f_{i,j} &= p_1\cdot f_{i,j}+p_2\cdot f_{i,j+1}+p_3\cdot f_{i+1,j}+p_4\cdot f_{i+1,j+1} + 1\\ &=(p_2\cdot f_{i,j+1}+p_3\cdot f_{i+1,j}+p_4\cdot f_{i+1,j+1}+1)/(1-p_1) \end{aligned} \]

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]);
    }
}

"例題 「NOIP2016」換教室"

​ 題目大意:牛牛要上 \(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}\) 的狀態轉移:

  • 若是這一階段不換,即 \(f_{i,j,0}\) 。多是由上一次不換的狀態轉移來的,那麼就是 \(f_{i-1,j,0}+w_{c_{i-1},c_{i}}\) , 也有多是由上一次交換的狀態轉移來的,這裏結合條件機率和全機率的知識分析能夠獲得 \(f_{i-1,j,1}+w_{d_{i-1},c_{i}}\cdot p_{i-1}+w_{c_{i-1},c_{i}}\cdot (1-p_{i-1})\) ,狀態轉移方程就有

\[\begin{aligned} f_{i,j,0}=min(f_{i-1,j,0}+w_{c_{i-1},c_{i}},f_{i-1,j,1}+w_{d_{i-1},c_{i}}\cdot p_{i-1}+w_{c_{i-1},c_{i}}\cdot (1-p_{i-1})) \end{aligned} \]

  • 若是這一階段交換,即 \(f_{i,j,1}\) 。相似地,可能由上一次不換的狀態轉移來,也可能由上一次交換的狀態轉移來。那麼遇到不換的就乘上 \((1-p_i)\) ,遇到交換的就乘上 \(p_i\) ,將全部會出現的狀況都枚舉一遍出進行計算就行了。這裏再也不贅述各類轉移狀況,相信經過上一種階段例子,這裏的狀態轉移應該可以很容易寫出來。

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 求指望,老是離不開機率知識和列出、化簡計算公式的步驟,在寫狀態轉移方程時須要思考的細節也相似。

習題

有後效性 DP

例題 "CodeForces 24 D Broken robot"

​ 題目大意:給出一個 \(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}=\frac{1}{3}\cdot(f_{i+1,1}+f_{i,2}+f_{i,1})+1\)
  • \(f_{i,j}=\frac{1}{4}\cdot(f_{i,j}+f_{i,j-1}+f_{i,j+1}+f_{i+1,j})+1\)
  • \(f_{i,m}=\frac{1}{3}\cdot(f_{i,m}+f_{i,m-1}+f_{i+1,m})+1\)

在行之間因爲只能向下移動,是知足無後效性的。在列之間能夠左右移動,在移動過程當中可能產生環,不知足無後效性。
將方程變換後能夠獲得:

  • \(2f_{i,1}-f_{i,2}=3+f_{i+1,1}\)
  • \(3f_{i,j}-f_{i,j-1}-f_{i,j+1}=4+f_{i+1,j}\)
  • \(2f_{i,m}-f_{i,m-1}=3+f_{i+1,m}\)

因爲是逆序的遞推,因此每個 \(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;
}

習題

參考文獻

kuangbin 機率 DP 總結

ACM裏的指望和機率問題 從入門到精通

論文學習:

《信息學競賽中機率問題求解初探》

《淺析競賽中一類數學指望問題的解決方法》

《有關機率和指望問題的研究 》

關於一些練習題:

機率dp總結

相關文章
相關標籤/搜索