算法筆記:動態規劃(2)

問題類型

       在LeetCode上目前共有200道左右的動態規劃相關的題,根據現有題目可以總結出一些題型,熟悉題型以及相關的描述能夠幫助我們更準確地判斷動態規劃使用的場景。

  • 通向目標點的最小(最大)路徑
  • 不同方法數
  • 區間合併
  • 字符串上的動態規劃
  • 決策類

通向目標點的最小(最大)路徑

問題描述

       這一類問題通常是給定一個目標點(目標狀態),要求到達該目標點的代價最小(或最大)的路徑的代價。一般來說都是求“代價”,而不是求“路徑”。這種題的建模技巧在於,首先要明確“目標點”是什麼,“代價”是什麼,以及在通向該目標點的過程中經過的“中間狀態”是哪些,dp[]數組就保存這些狀態就可以。創建狀態轉移方程的時候通常是從前面的狀態中選一個最大(最小)的再加上當前狀態的權值。

例題

最小路徑和

(LeetCode.64)給定一個包含非負整數的 m×nm\times n網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。
說明:每次只能向下或者向右移動一步。
示例:

輸入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
輸出: 7
解釋: 因爲路徑 1→3→1→1→1 的總和最小。

       此題的目標點很明確,就是grid[m-1][n-1]這個點,代價就是路徑上經過的所有的點的權值,中間狀態就是grid中的所有點,因爲在走向目標點的過程中,所有其他的點都有可能經過,因此可以建立一個dp[m][n]的數組用以保存中間狀態。

  1. 確定狀態:dp[i][j]表示的是到達(i,j)(i,j)這個點的最小代價。
  2. 邊界值:本題的狀態數組是二維的,邊界值是兩組,分別是dp[0][0]到dp[0][n-1]和dp[0][0]到dp[m-1][0]。可以看出就是dp數組的第0行和第0列。dp[0][j]的值應該是grid[0][0]到grid[0][j]的和(想要到達點(0,j)(0,j)只能是從點(0,0)(0,0)一路向右走),或者說dp[0][j] = dp[0][j-1]+grid[0][j]。同理有dp[i][0] = dp[i-1][0]+grid[i][0]
  3. 狀態轉移方程:對於點(i,j)(i,j),達到該點之前的一個點只有兩種可能:(i1,j)(i-1, j)(i,j1)(i,j-1)。因此如果想要到該點的代價最小,那麼只要其是從代價較小的前一個點到達即可。所以dp[i][j] = min(dp[i-1][j], dp[i][j-1])+grid[i][j]

最終實現代碼如下:

int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    for (int i = 1; i < m; i++) {
        dp[i][0] = dp[i-1][0] + grid[i][0];
    }
    for (int i = 1; i < n; i++) {
        dp[0][i] = dp[0][i - 1] + grid[0][i];
    }
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
        }
    }
    return dp[m - 1][n - 1];
}

零錢兌換

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。

示例1:

輸入: coins = [1, 2, 5], amount = 11
輸出: 3
解釋: 11 = 5 + 5 + 1

示例2:

輸入: coins = [2], amount = 3
輸出: -1

說明: 你可以認爲每種硬幣的數量是無限的。

        “目標點”比較不直觀,是硬幣的總面值達到amount;代價是硬幣的個數,中間狀態應該是在總面值達到amount之前的所有面值的最小硬幣數目。

  1. 確定狀態:創建數組dp[amount+1],其中dp[i]表示總面值爲i的最少硬幣數目,如果無法組成該數目,則爲-1
  2. 邊界值:對於coins中所有的硬幣面值c,都應該有dp[c]=1。
  3. 狀態轉移方程:dp[i]=minccoins,dp[ic]>0dp[ic]+1dp[i]= \min_{c \in coins, dp[i-c]>0}{dp[i-c]}+1,即對於總面值i,掃描coins中所有的單個面值c,找出代價最小的前一個狀態。
public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];

    Arrays.fill(dp, -1);
    dp[0] = 0;
    for (int i : coins) {
        if(i <= amount) {
            dp[i] = 1;
        }
    }

    for (int i = 1; i <= amount; i++) {
        if (dp[i] < 0) {
            int min = Integer.MAX_VALUE;
            for (int coin : coins) {
                if(coin < i && dp[i-coin] > 0) {
                    min = Math.min(min, dp[i-coin]);
                }
            }
            if(min < Integer.MAX_VALUE) {
                dp[i] = min+1;
            }
            else dp[i] = -1;
        }
    }

    return dp[amount];
}

其他

不同方法數

        這一類問題的描述通常是給定一個目標要求我們計算達到該目標的方法(路徑)數有多少。創建狀態轉移方程的時候通常是找出所有前置狀態,將其路徑數相加,即dp[i]的含義一般是到達狀態i的方法數。

不同路徑

LeetCode.62 不同路徑一個機器人位於一個m×nm\times n網格的左上角。機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。
問總共有多少條不同的路徑?
說明:m 和 n 的值均不超過 100。

示例:
輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右
  1. 確定狀態:dp[i][j]表示到達點(i,j)的路徑數
  2. 邊界值:dp二維數組的第0列和第0行的值應該均爲1
  3. 狀態轉移方程:在到達點(i,j)之前,一定是在點(i-1,j)或者(i,j-1),所以dp[i][j] = dp[i][j-1]+dp[i-1][j]
public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int i = 0; i < m; i++) {
        dp[i][0] = 1;
    }
    Arrays.fill(dp[0], 1);
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }
    return dp[m - 1][n - 1];
}

其他

區間合併

        這類問題的描述一般爲給定一列數,求解過程中要考慮的是數列中的某個數以及其左側的子列和右側的子列分別能得出的某些結論。一般有如下解題思路

for(int l = 1; l<n; l++) {
   for(int i = 0; i<n-l; i++) {
       int j = i+l;
       for(int k = i; k<j; k++) {
           dp[i][j] = max(dp[i][j], dp[i][k] + result[k] + dp[k+1][j]);
       }
   }
}
 
return dp[0][n-1]

例題

字符串上的動態規劃

        這一類題目一般都是給定兩個字符串s1和s2,求一些值。一般來說要構建一個二維數組dp[][],其中dp[i][j]表示在s1的第i個字符以及s2的第j個字符這樣的狀態的值。通常的解題過程爲

// i - indexing string s1
// j - indexing string s2
for (int i = 1; i <= n; ++i) {
   for (int j = 1; j <= m; ++j) {
       if (s1[i-1] == s2[j-1]) {
           dp[i][j] = /*code*/;
       } else {
           dp[i][j] = /*code*/;
       }
   }
}

最長公共子序列

1143. 最長公共子序列給定兩個字符串 text1 和 text2,返回這兩個字符串的最長公共子序列。

一個字符串的 子序列 是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)後組成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。兩個字符串的「公共子序列」是這兩個字符串所共同擁有的子序列。

若這兩個字符串沒有公共子序列,則返回 0。
示例:

輸入:text1 = “abcde”, text2 = “ace”
輸出:3
解釋:最長公共子序列是 “ace”,它的長度爲 3。

  1. 確定狀態:創建二維數組dp[len1][len2],其中dp[i][j]表示到text1的第i個字符和text2的第j個字符時,最長的公共子序列的值。
  2. 邊界值:如果text1[0]==text2[0],dp[0][0]= 1,否則dp[0][0]=0;
  3. 狀態轉移方程:如果text1[i]==text2[j],dp[i][j] = dp[i-1][j-1]+1,否則dp[i][j]=max(dp[i][j-1], dp[i-1][j]);
public int longestCommonSubsequence(String text1, String text2) {
    int l1 = text1.length(), l2 = text2.length();
    int[][] dp = new int[l1][l2];

    if (text1.charAt(0) == text2.charAt(0)) dp[0][0] = 1;
    else dp[0][0] = 0;

    int max = 0;
    for (int i = 1; i < l1; i++) {
        if (text1.charAt(i) == text2.charAt(0)) {
            dp[i][0] = 1;
            max = 1;
        } else {
            dp[i][0] = dp[i-1][0];
        }
    }

    for (int i = 1; i < l2; i++) {
        if (text1.charAt(0) == text2.charAt(i)) {
            dp[0][i] = 1;
            max = 1;
        } else {
            dp[0][i] = dp[0][i-1];
        }
    }

    for (int i = 1; i < l1; i++) {
        for (int j = 1; j < l2; j++) {
            if (text1.charAt(i) == text2.charAt(j)) {
                dp[i][j] = dp[i-1][j-1]+1;
                max = Math.max(max, dp[i][j]);
            } else dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
        }
    }
    return max;
}

其他

決策類問題

       此模式的一般問題描述是針對給定情況決定是否使用當前狀態。因此,問題需要您在當前狀態下做出決定。一般的創建數組的思路是創建一個二維數組dp[n][k],其中n爲候選狀態的數目,k爲對狀態能夠採取的操作的數目。這樣就可以保存對每一種狀態採取所有不同措施的狀態值。

例題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章