數學期望在演算法中的應用

Macw發表於2024-11-27

數學期望在演算法中的應用

數學期望是機率論和統計學中的一個核心概念,主要用於描述所有資料的平均值或者是中心趨勢。在計算機演算法競賽中,期望演算法屬於一箇中高等難度的演算法,在程式設計中發揮著至關重要的作用。在近些年的 CSP/ USACO 等國際知名演算法競賽中,期望和期望動態規劃等演算法常常被作為考試題目。因此,本文將詳細講述數學期望在演算法中的應用。

為了降低本文的閱讀門檻,本文會提供諸多例子來幫助讀者來理解期望的定義和期望的實際應用。在文章的最後,我也會向大家提供相關的演算法練習題。

隨機變數的基本概念

在瞭解期望之前,務必要了解一下 隨機變數 Random Variable 的定義。

隨機變數是一個 函式,它將隨機實驗的每一個可能結果對映到一個數值。隨機變數可以看作是隨機現象的數學表達。

E.g. 1 投擲硬幣

以投擲一枚硬幣舉例子:

  • 有兩個實驗結果:正面 (\(\mathtt{H}\))、反面 (\(\mathtt{T}\))。
  • 隨機變數 \(X\):定義為正面記作 \(1\),反面記作 \(0\)

E.g. 2 投擲骰子

在投擲骰子時:

  • 有六個實驗結果:\(1, 2, 3, 4, 5, 6\)
  • 隨機變數 \(Y\):定義為投擲出的點數的值。

數學期望的基本概念

教科書上對於 期望 Expectation 的定義如下:

數學期望,簡稱期望,是對隨機變數取值的加權平均,其權重為對應取值的機率。

一個更直觀的說法是:期望值代表了大量重複實驗中,隨機變數取值的平均水平,是對隨機現象的一種 集中趨勢 的描述。

E.g. 3 考試評分

假設一門考試有五道選擇題,得分規則如下:

  1. 選擇正確答案獲得 \(4\) 分。
  2. 選擇錯誤答案扣除 \(1\) 分。
  3. 未作答不增加也不扣除分數。

如果考生隨機選擇答案,每道題的機率為:

  1. 做對這道題的機率為:\(\dfrac{1}{4}\)
  2. 做錯這道題的機率為:\(\dfrac{3}{4}\)

那麼這場考試每道題的期望得分為:

\[E(X) = 4\times \dfrac{1}{4} + (-1) \times \dfrac{3}{4} = 1- 0.75 = 0.25 \]

這意味著,隨機作答的長期平均得分是每道題 \(0.25\) 分。

E.g. 4 機率遊戲 \(\mathrm{I}\)

有一個擲骰子的遊戲,規則是如果投擲出點數 \(6\)\(10\) 分,投擲出其餘點數扣 \(2\) 分。那麼:

  • 投擲出 \(6\) 的機率是:\(\dfrac{1}{6}\)
  • 投擲出其他點數的機率是:\(\dfrac{5}{6}\)

期望得分為:

\[E(X) = 10\times\dfrac{1}{6} + (-2)\times \dfrac{5}{6} = \dfrac{10}{6} - \dfrac{10}{6} = 0 \]

因此從長遠來看(假如一直玩這個遊戲的話),這個遊戲是公平的,沒有任何的得分優勢。

透過這兩個例子應該就能夠很容易地理解期望在數學中的定義和作用了。

期望的線性疊加和獨立性

期望是可以疊加的,假設有兩個事件(兩個事件可以是相互獨立的,也可以是相互依賴的),兩個事件的期望分別為 \(E(\Alpha)\)\(E(\Beta)\),那麼這兩個事件的整體期望 \(E(\Alpha + \Beta)\) 可以直接被拆解成 \(E(\Alpha) + E(\Beta)\)

這說明 期望的計算可以逐項分解並加權,無論這些隨機變數是否獨立。

E.g. 5 機率遊戲 \(\mathrm{II}\)

有一個機率遊戲,分為兩輪:

  1. 第一輪:玩家投擲一個六面骰子,得分為骰子的點數。
  2. 第二輪:玩家擲兩個六面骰子,得分為兩個點數之和。

目標:要求計算玩家的總期望得分。

對於這種題目,我們就可以利用期望的線性疊加來完成。以下是一些變數的定義:

  1. 第一輪得分:隨機變數 \(X_1\),取值為 \(1, 2, 3, 4, 5, 6\)
  2. 第二輪得分:隨機變數 \(X_2\),為兩個骰子的點數之和,取值為 \(2, 3, \cdots, 11, 12\)
  3. 總得分的隨機變數 \(S = X_1 + X_2\)

根據期望的線性疊加性質:

\[E(S) = E(X_1) + E(X_2) \]

我們可以分別計算兩個隨機變數的期望,並將結果相加就可以計算出整一個機率遊戲的期望得分了。

經過計算(本文不再詳細舉例相同的期望得分計算過程,具體可以自己手動推導),兩輪遊戲的期望得分分別為:

  1. 第一輪:\(E(X_1) = \dfrac{21}{6} = 3.5\)
  2. 第二輪:\(E(X_2) = \dfrac{252}{36} = 7\)

那麼總期望得分就是:

\[E(S) = E(X_1) + E(X_2) = 3.5 + 7 = 10.5 \]

也就是說,如果玩家無限地玩這個遊戲,平均下來每一輪的得分大約為 \(10.5\)

期望的基本演算法題

期望在計算機的應用也非常的廣泛,這裡提供幾個實際的演算法題目來幫助讀者加深對期望的理解。

E.g. 6 隨機交換序列

題目描述

給定一個長度為 \(n\) 的序列,每個元素為 \(a_1, a_2, \cdots, a_{-1}, a_{n}\)。每一步操作為選擇兩個不同位置的 \(i\)\(j\),滿足 \(1 \le i, j\le n, i \neq j\),並交換 \(a_i\)\(a_j\) 的值。假設進行無限次隨機交換操作後,求每一個位置上的最終數字的期望值。

解題思路

在進行了無限次隨機交換次數後,序列將趨於均勻的隨機排列。由於所有的排列的機率都是相同的,那麼每個位置上的元素的期望值應為序列的平均值。

數學證明

設序列的總和為 \(S = \Sigma_{i=1}^{n}a_i\),平均值為 \(\mu = \dfrac{S}{n}\)。在進行無限次隨機交換後,每個位置上的元素均可能是任意一個 \(a_i\),因此期望值均為 \(\mu\)

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

int main(){
    int n; cin >> n;
    vector<long long> a(n);
    double sum = 0.0;
    for(auto &x: a){
        cin >> x;
        sum += x;
    }
    double average = sum / n;
    // 輸出每個位置的期望值
    for(int i=0;i<n;i++){
        printf("%.6lf ", average);
    }
    return 0;
}

E.g. 7 彩票中獎期望

題目描述

你正參加一個彩票遊戲,每張彩票有兩個號碼,分別是紅球和籃球。紅球的號碼範圍從 \(1\)\(R\) 中選擇,籃球的號碼從 \(1\)\(B\) 中選擇。每張彩票的中獎條件是紅球和籃球都正確。已知你購買了 \(k\) 張不同的彩票,求中獎的期望次數。

解題思路

首先先求出每張彩票的中獎機率為 \(\dfrac{1}{R} \times \dfrac{1}{B} = \dfrac{1}{RB}\)。購買 \(k\) 張獨立的彩票,每張彩票的中獎次數都是獨立的,因此總的中獎次數的期望就是 \(k \times \dfrac{1}{RB}\)

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

int main(){
    long long R, B, k;
    cin >> R >> B >> k;
    double E = (double)k / (R * B);
    printf("%.6lf\n", E);
    return 0;
}

E.g. 8 期望步數達到目標

題目描述

在一個二維平面網格上,從起點 \((0, 0)\) 開始,目標是到達終點 \((n, m)\)。每一步,你可以選擇向右或者向上移動。向右移動的機率為 \(p\),向上移動的機率為 \(1 - p\)。求從起點到達終點的期望步數。

解題思路

相比較前面幾道題目,這道題的難度有所提升。這是一個典型的動態規劃與期望相結合的問題。我們設定 \(dp_{i, j}\) 表示從點 \((i, j)\) 到達終點的期望步數。

狀態轉移方程:

  • 如果當前座標是終點,即 \(i = n\)\(j = m\) 時,則 \(dp_{i, j} = 0\),表示期望走 \(0\) 步就可以到達終點。
  • 如果在邊界移動(即 \(i = n\)\(j = m\) 時),只能單向移動:
    • \(dp_{i, j} = 1 + dp_{i, j+1}\)(向右移動)
    • \(dp_{i, j} = 1 + dp_{i+1, j}\)(向上移動)
  • 其他情況:
    • \(dp_{i, j} = 1 + p \times dp_{i, j+1} + (1 - p) \times dp_{i+1, j}\)

計算順序:從終點開始,逆序填充 \(dp\) 表格。

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

typedef long long ll;

int main(){
    int n, m; double p;
    int dp[505][505];
    cin >> n >> m >> p;
    for(int i=n; i>=0; i--){
        for(int j=m; j>=0; j--){
            if(i == n && j == m){
                dp[i][j] = 0.0;
                continue;
            }
            if(i == n) dp[i][j] = 1.0 + dp[i][j+1];
            else if(j == m) dp[i][j] = 1.0 + dp[i+1][j];
            else dp[i][j] = 1.0 + p * dp[i][j+1] +
                (1.0 - p) * dp[i+1][j];
        }
    }
    printf("%.6lf\n", dp[0][0]);
    return 0;
}

E.g. 9 P1365 WJMZBMR打osu! / Easy

解題思路

這也是一道經典的期望動態規劃的例題,與前面的題目都相同,我們先定義 \(dp_i\) 表示以第 \(i\) 個字元結尾的期望得分,用變數 \(\mathtt{len}\) 來表示連續的 o 字元出現的個數(且需要包含 \(str_i\) 的回合)。根據字串的三種字元分類進行討論:

  1. 噹噹前字元為 x 的時候:

    說明本回合遊戲失敗,期望得分將不會增加,也不會減少(與 \(dp_{i-1}\) 相同)。與此同時,需要將 \(\mathtt{len}\) 歸零,表示截至目前不存在連續的 o

  2. 噹噹前的字元為 o 的時候:

    說明本回合遊戲勝利,期望得分應該就是截止上一輪遊戲的期望得分 \(dp_{i-1}\) 加上這輪遊戲的期望得分 \((\mathtt{len} + 1)^2 - \mathtt{len}^2\)(撤銷長度為 \(\mathtt{len}\) 的連擊得分,增加長度為 \(\mathtt{len} + 1\) 的期望得分。化簡可得:\(dp_i = dp_{i-1} + 2\times \mathtt{len} + 1\)。同時在更新完 \(dp\) 陣列後將 \(\mathtt{len}\) 設定為 \(\mathtt{len} + 1\)

  3. 噹噹前字元為 ? 的時候:

    我們需要同時考慮勝利或者失敗兩種情況((成功的期望 + 失敗的期望) / 2):

    \[dp_i = dp_{i-1} + \dfrac{((\mathtt{len} + 1)^2 - \mathtt{len}^2) + 0}{2} = dp_{i-1} + \mathtt{len} + 0.5 \]

    與此同時需要把 \(\mathtt{len}\) 更新為 \(\dfrac{(\mathtt{len} + 1) + 0}{2} = \dfrac{\mathtt{len} + 1}{2}\)

接下來直接遍歷就好了。

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 3e5 + 5;
int n; char c;
long double len, dp[N];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> c;
        switch (c){
            case '?': 
                dp[i] = dp[i-1] + len + 0.5;
                len = (len + 1) / 2; break;
            case 'o': 
                dp[i] = dp[i-1] - len * len + (len + 1) * (len + 1); 
                len++; break;
            case 'x': dp[i] = dp[i-1]; len = 0; break;
        }
    }
    printf("%.4Lf\n", dp[n]);
    return 0;
}

該演算法的時間複雜度為 \(O(n)\),空間複雜度也是 \(O(n)\),但考慮到每一個 \(dp_i\) 永遠只依賴自己上一個狀態(\(dp_{i-1}\)),因此可以進一步把程式碼的空間複雜度降低到 \(O(1)\)

E.g. 10 P1850 [NOIP2016 提高組] 換教室

這道題是 NOIP 2016 年比賽的原題,可以看出期望動態規劃確實是一項重點。

解題思路

相同地,我們在一開始也需要定義 \(dp\) 狀態。定義 \(dp_{i, j, k}\) 表示走到了第 \(i\) 點,申請了 \(j\) 次換課,當前次 \(\mathtt{換/不換}(1/0)\) 的期望。程式碼用 \(map_{a, b}\) 來表示地圖中 \(a\)\(b\) 兩點的最短路(由於資料範圍和需要求解多源最短路徑的需求,這裡使用 Floyd 演算法來計算最短路徑)。

狀態轉移:

  • 未換課 (\(dp_{i, j, 0}\)):

    • 情況 1:上一步也未換課:

      \[dp_{i, j, 0} = dp_{i-1, j, 0} + \text{map}[c[i-1]][c[i]] \]

    • 情況 2:上一步換課:

      \[dp_{i, j, 0} = dp_{i-1, j, 1} + \text{map}[c[i-1]][c[i]] \cdot (1 - k[i-1]) + \text{map}[d[i-1]][c[i]] \cdot k[i-1] \]

  • 換課 (\(dp_{i, j, 1}\)):

    • 情況 1:上一步未換課:

      \[dp_{i, j, 1} = dp_{i-1, j-1, 0} + \text{map}[c[i-1]][d[i]] \cdot k[i] + \text{map}[c[i-1]][c[i]] \cdot (1 - k[i]) \]

    • 情況 2:上一步換課:

      \[dp_{i, j, 1} = dp_{i-1, j-1, 1} + \\ \text{map}[d[i-1]][d[i]] \cdot k[i-1] \cdot k[i] + \\ \text{map}[d[i-1]][c[i]] \cdot k[i-1] \cdot (1 - k[i]) + \\ \text{map}[c[i-1]][d[i]] \cdot (1 - k[i-1]) \cdot k[i] + \\ \text{map}[c[i-1]][c[i]] \cdot (1 - k[i-1]) \cdot (1 - k[i]) \]

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

constexpr int N = 2005;
constexpr int V = 305, E = 90000;
int n, m, v, e;
int c[N], d[N];
double k[N];
long long map[V][V];
// dp[i][j][k] 表示走到第 i 個點,申請了 j 次,
// 當前次 申請/不申請 (k) 的期望。
double dp[N][N][2];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m >> v >> e;
    for (int i=1; i<=n; i++) cin >> c[i];
    for (int i=1; i<=n; i++) cin >> d[i];
    for (int i=1; i<=n; i++) cin >> k[i];
    for (int i=1; i<=v; i++){
        for (int j=1; j<=v; j++){
            map[i][j] = 0x7f7f7f7f;
        }
        map[i][i] = map[i][0] = map[0][i] = 0;
    }
    for (int i=1; i<=e; i++){
        int a, b, w;
        cin >> a >> b >> w;
        map[a][b] = map[b][a] = min(map[a][b], 1LL * w);
    }
    for (int k=1; k<=v; k++){
        for (int i=1; i<=v; i++){
            for (int j=1; j<=v; j++){
                map[i][j] = min(map[i][k] + map[k][j], map[i][j]);
            }
        }
    }
    for (int i=0; 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++){
        dp[i][0][0] = dp[i-1][0][0] + map[c[i-1]][c[i]];
        for (int j=1; j<=min(i, m); j++){
            int C1 = c[i-1], C2 = d[i-1], C3 = c[i], C4 = d[i];
            dp[i][j][0] = min(dp[i][j][0], min(dp[i-1][j][0] + map[C1][C3], 
                dp[i-1][j][1] + map[C1][C3] * (1 - k[i-1]) + map[C2][C3] * k[i-1]));
            dp[i][j][1] = min(dp[i][j][1], 
                min(dp[i-1][j-1][0] + map[C1][C3] * (1 - k[i]) + map[C1][C4] * k[i],
                dp[i-1][j-1][1] + map[C2][C4] * k[i] * k[i-1] + 
                map[C2][C3] * k[i-1] * (1 - k[i]) +
                map[C1][C4] * (1 - k[i-1]) * k[i] + 
                map[C1][C3] * (1 - k[i-1]) * (1 - k[i])));
        }
    }
    double ans = 1e9;
    for (int i=0; i<=m; i++){
        ans = min(ans, dp[n][i][0]);
        ans = min(ans, dp[n][i][1]);
    }
    printf("%.2lf", ans);
    return 0;
}

E.g. 11 P1654 OSU!

與【E.g. 9 [P1365 WJMZBMR打osu! / Easy]】類似,稍作修改即可。

解題思路

要求解 \(x^3\) 的期望,那麼肯定需要維護 \(x^2\)\(x\) 的期望才可以。

具體地:

  1. \(a[i]\) 表示以第 \(i\) 個位置為終點,\(x\) 的期望。
  2. \(b[i]\) 表示以第 \(i\) 個位置為終點,\(x^2\) 的期望。
  3. \(dp[i]\) 表示以第 \(i\) 個位置為終點,\(x^3\) 的期望。

遞推公式:

  • \(a[i] = (a[i-1] + 1) * p[i]\)
    • \(a[i-1]\) 是上一輪的 \(x\) 的期望,加上當前位置的貢獻 \(1 \cdot p[i]\)
  • \(b[i] = (b[i-1] + 2 \cdot a[i-1] + 1) \cdot p[i]\)
    • 上一輪的 \(x^2\) 的期望加上新的貢獻,其中包含 \(2 \cdot a[i-1] \cdot 1\)\(1^2\)
  • \(dp[i] = dp[i-1] + (3 \cdot (a[i-1] + b[i-1]) + 1) \cdot p[i]\)
    • 最後將所有 \(x^3\) 的期望累加到當前 \(dp\) 狀態。

C++ 程式碼實現

本題的 C++ 程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

constexpr int N = 3e5 + 5;
int n;
long double p[N], dp[N], a[N], b[N];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n;
    for (int i=1; i<=n; i++) cin >> p[i];
    for (int i=1; i<=n; i++){
        a[i] = (a[i-1] + 1) * p[i];
        b[i] = (b[i-1] + 2 * a[i-1] + 1) * p[i];
        dp[i] = dp[i-1] + (3 * (a[i-1] + b[i-1]) + 1) * p[i];
    }
    printf("%.1Lf\n", dp[n]);
    return 0;
}

常見問題與誤區

誤區一:期望值與實際值混淆

期望值代表隨機變數的平均水平,但這並不意味著隨機變數每次實驗都恰好等於期望值。期望式大量實驗後的平均結果,而非單次實驗的確定結果。

誤區二:忽略條件期望

在複雜問題中,忽視條件期望可能導致錯誤的期望計算,尤其是在存在依賴關係或多階段決策的問題中。例如,在【E.g. 8 期望步數達到目標】中,如果忽略了當前位置的條件(當前座標),會導致狀態轉移方程的錯誤,從而會計算出錯誤的期望步數。

參考文獻

  • GeeksforGeeks. "ML | Expectation-Maximization Algorithm." GeeksforGeeks, https://www.geeksforgeeks.org/ml-expectation-maximization-algorithm/.
  • Bee, Chloe. "The EM Algorithm Explained." Medium, https://medium.com/@chloebee/the-em-algorithm-explained-52182dbb19d9.
  • Williams, Richard. "The Expectation Maximization (EM) Algorithm." University of Notre Dame, https://www3.nd.edu/~rwilliam/stats1/x12.pdf.

相關文章