一、動態規劃的思考藝術 動態規劃其實就是一種暴力枚舉的優化,在暴力枚舉的過程中有著大量的重複,藉由「備忘錄(memoization)」的方式做到剪枝(pruning)來達到優化的一種演算法。 舉例來說: Leetcode 62. Unique Paths
機器人由左上走到右下角星星有幾種走法,其中機器人只能選擇往右走或往下走。 試想機器人從 (1,1) 走到 (m,n) 的不同路徑中,可見有大量的重複,比如過程中有一點 (i,j),其 (1,1) 走到 (i,j) 有 k 條不同路徑,麼那對於任何一條固定 (i,j) 到 (m,n) 的路徑,都需走 k 遍來模擬。 但其實我們不必關心具體的走法,我們只關心狀態,也就是走法的數目。 同理,我們若知道 (i,j) 到 (m,n) 共有 t 條不同的路徑,那麼 (1,1) -> (i,j) -> (m,n) 的不同路徑總數就是 k*s。 我們知道最左邊那欄與最上面那列都只有可能有一種路徑可以走,又每一格的路徑來自於上方與左方的和: sum of (i,j) = sum of (i-1,j) + sum of (i,j-1) \(\begin{array}{|c|c|c|c|c|c|c|}\hline \text{1}&\text{1}&\text{1}&\text{1}&\text{1}&\text{1}&\text{1}\\\hline \text{1}&\text{2}&\text{3}&\text{4}&\text{5}&\text{6}&\text{7}\\\hline \text{1}&\text{3}&\text{6}&\text{10}&\text{15}&\text{21}&\text{28}\\\hline \end{array}\) 寫成程式碼就是 int uniquePaths(int m, int n) { vector<vector<int>> dp(m+1, vector<int>(n+1,0)); for (int i = 1; i <= m; i++) // 將第一列填成 1 dp[i][1] = 1; for (int j = 1; j <= n; j++) // 將第一欄填成 1 dp[1][j] = 1; for (int i = 2; i <= m; i++) { // 將剩下的格子填完 for (int j = 2; j <= n; j++) { dp[i][j] = dp[i-1][j] + dp[i][j-1]; } } return dp[m][n]; } 注意填格子的順序是有一定的限制的,必須要確保相關聯的子問題已經處理過。 動態規劃 由上例我們可以發現,原本的問題可以拆解成更小的問題(從 (1,1)->(m,n) 變成從 (1,1)->(i,j) 和從 (i,j)->(m,n))。 我們令 f(i,j) 表示從 (1,1)->(i,j) 的不同路徑數,則我們可以得到轉移方程式 f(i,j)=f(i-1,j)+f(i,j-1)。 我們發現,想求出 f(i,j) 只需要知道幾個更小的 f(i',j')。我們將 f(i',j') 稱為子問題。 我們捨棄冗餘的訊息(具體的走法),只記錄對解決問題有幫助的結果。 動態規劃的兩大特點(適用前提) 無後效性 一旦 f(i,j) 確定,就不用關心我們如何計算出 f(i,j) 想要確定 f(i,j),只需要知道 f(i-1,j) 和 f(i,j-1) 的值,而至於它們是如何算出來的,對當前或之後的任何子問題都沒有影響。 過去不依賴未來,未來不影響過去。 最優子結構 f(i,j) 的定義就已經蘊含了最優。 大問題的最優解可以由若干個小問題的最優解推出。(max, min, sum…) DP 能適用於:能將大問題拆成若干小問題,滿足無後效性、最優子結構性質。 以下介紹幾種刷題會遇到的動態規劃套路: 二、動態規劃框架 1....