動態規劃大神總結——必讀

  • 0-1揹包問題(二維dp)
  • 0-1揹包升級版(二維dp)
  • 完全揹包(費解)如湊領錢(一維、二維dp)
  • 子序列問題(重要)
    • 最長遞增子序列(一維dp)
    • 最長公共子序列(二維dp)
    • 最長迴文子序列(二維dp)
    • 最短編輯距離(二維dp)
  • 最短路徑(機器人走路)(二維dp)

第一步要明確兩點,「狀態」和「選擇」。明確dp數組的定義

狀態有兩個:「揹包的容量」、「可選擇的物品」

選擇有兩個:「裝進揹包」、「不裝進揹包」

幾種狀態就是幾層for循環,也就是幾維dp

第二步,根據「選擇」,思考狀態轉移的邏輯

第三步,確定初始條件


labuladong的動歸

例題一:0-1揹包升級版

給你一個可裝載重量爲W的揹包和N個物品,每個物品有重量和價值兩個屬性。其中第i個物品的重量爲wt[i],價值爲val[i],現在讓你用這個揹包裝物品,最多能裝的價值是多少?

  • 定義二維dp[i][j]:對於前i種物品,當前揹包重量爲j時,能夠獲得的最大價值爲dp[i][j]。我們要求的就是dp[n][w]
  • 數組元素之間的關係:
    • j - wt[i - 1] < 0 時:dp[i][j] = dp[i - 1][j]。表示當前剩餘的容量裝不下當前的物品,只能繼承上一個裝填的
    • j - wt[i - 1] >= 0 時:裝或者不裝。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1])
  • 數組元素之間的關係:dp[i] = dp[i - 1] + dp[i - 2]
  • 初始條件:dp[...][0] = 0;dp[0][...] = 0。表示物品或者容量爲0時,當前價值爲0
// 經典動態規劃:0-1揹包問題
int baseDP(int w, int n, int weight[], int value[]){
    // 定義二維狀態數組
    int dp[n + 1][w + 1];
    // 初始化邊界
    for(int i = 0; i <= n; i++)
        dp[i][0] = 0;
    for(int i = 0; i <= w; i++)
        dp[0][i] = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= w; j++){
            if(j - weight[i - 1] < 0)
                // 裝不下,直接繼承前一個狀態的
                dp[i][j] = dp[i - 1][j];
            // dp[i][j] = 擇優(選擇1, 選擇2)
            // 揹包裝或者不裝,兩者擇優選擇
            else
                // 這個地方如果是一維數組的話就是倒着來的
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
        }
    }
    return dp[n][w];
}
例題二:0-1揹包變體

給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。

注意:每個數組中的元素不會超過 100;數組的大小不會超過 200
示例 1:

輸入: [1, 5, 11, 5]

輸出: true

解釋: 數組可以分割成 [1, 5, 5] 和 [11].

  • **定義dp[i][j]:對於前i個物品,當前揹包的容量爲j時,若dp[i][j]爲true,則說明可以裝滿。我們要求的就是dp[N][sum/2] **
  • 數組元素之間的關係:
    • j - wt[i - 1] < 0 時:dp[i][j] = dp[i - 1][j]。表示當前剩餘的容量裝不下當前的物品,只能繼承上一個裝填的
    • j - wt[i - 1] >= 0 時:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - wt[i - 1]]
  • 初始條件:初始條件:dp[...][0] = true;dp[0][...] = false。表示物品爲0,價值不爲0時,肯定裝不滿
class Solution {
public:

    bool ans = false;
	// 這種方法超時
    void dfs(vector<int> &num, vector<int> a, vector<int> b, int index){
        if(index == num.size()){
            int sumA = 0, sumB = 0;
            for(int i = 0; i < a.size(); i++)
                sumA += a[i];
            for(int i = 0; i < b.size(); i++)
                sumB += b[i];
            if(sumA == sumB)
                ans = true;
            return ;
        }
        // 放入A揹包
        a.push_back(num[index]);
        dfs(num, a, b, index + 1);
        a.pop_back();
        b.push_back(num[index]);
        dfs(num, a, b, index + 1);
        b.pop_back();
    }

    bool canPartition(vector<int>& nums) {
        // 定義狀態數組
        int sum = 0;
        for(int i : nums)
            sum += i;
        if(sum % 2 != 0)
            return false;
        int n = nums.size();
        sum = sum / 2;
        bool dp[n + 1][sum + 1];
        // 初始化
        for(int i = 0; i <= n; i++)
            dp[i][0] = true;
        for(int i = 0; i <= sum; i++)
            dp[0][i] = false;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= sum; j++){
                if(j - nums[i - 1] < 0)
                    dp[i][j] = dp[i - 1][j];
                else
                    // 這個地方如果是一維數組的話就是倒着來的
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
        return dp[n][sum];
    }
};
例題三:完全揹包問題

湊領錢1:給你k種面值的硬幣,面值分別爲c1, c2 ... ck,每種硬幣的數量無限,再給一個總金額amount,問你最少需要幾枚硬幣湊出這個金額,如果不可能湊出,算法返回 -1 。

  • 定義一維數組dp[i]:當前總金額爲i時,需要最少dp[i]個硬幣湊出這個金額。我們要求的就是dp[amount]
  • 數組元素之間的關係:dp[i] = min(dp[i - coin] + 1)
  • 初始條件:dp[0] = 0。即金額爲0就需要0枚硬幣
int coinChangeDP(vector<int> &coins, int amount){
    // 初始化備忘錄
    vector<int> dp(amount + 1, amount + 1);
    dp[0] = 0;
    // 填表
    for(int i = 1; i < dp.size(); i++){
        // 內層循環,找最小
        for(int coin : coins){
            if(i - coin < 0)
                continue;
            dp[i] = min(dp[i], dp[i - coin] + 1);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

湊領錢2:給定不同面額的硬幣和一個總金額,寫出函數來計算可以湊成總金額的硬幣組合數,假設每種面額的硬幣有無限個。

  • 定義二維數組dp[i][j]:當前總金額爲j時,前i個物品可能有dp[i][j]種可能湊齊。我們要求的就是dp[n][amount]
  • 數組元素之間的關係:
    • j-coins[i - 1] < 0時:dp[i][j] = dp[i - 1][j]。表示當前容量裝不下當前的硬幣,只能繼承上一個狀態的
    • j-coins[i - 1] >= 0時:dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
  • 初始條件:dp[i][0] = 1;dp[0][i] = 0;
int change(int amount, vector<int>& coins) {

    if(amount == 0 && coins.size() == 0)
        return 1;

    int n = coins.size();
    int dp[n + 1][amount + 1];
    // 初始化
    for(int i = 0; i <= n; i++)
        dp[i][0] = 1;
    for(int i = 0; i <= amount; i++)
        dp[0][i] = 0;

    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= amount; j++){
            if(j - coins[i - 1] < 0)
                dp[i][j] = dp[i - 1][j];
            else
                // 注意這裏是i不是i-1了,這樣就保證了物品可以選無數次,如果是i-1的話,就是普通揹包,只能選一次
                // 這個地方如果用一維數組,那麼就是順着來的
                dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
        }
    }
    
    return dp[n][amount];
}
例題四:最長公共子序列

  解決兩個字符串的動態規劃問題,一般都是用兩個指針i,j分別指向兩個字符串的最後,然後一步步往前走,縮小問題的規模。都是建立一個二維的dp數組。

求兩個字符串的 LCS 長度:

輸入: str1 = "abcde", str2 = "ace" 
輸出: 3  
解釋: 最長公共子序列是 "ace",它的長度是 3
  • 定義二維dp[i][j]:表示str1的(0, i)子序列與str2的(0, j)子序列的最長公共序列。我們要求的就是dp[m][n]
  • 數組元素之間的關係:
    • str1[i] = str2[j]時:dp[i][j] = dp[i - 1][j - 1] + 1
    • str1[i] != str2[j]時:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
  • 初始條件:dp[...][0] = dp[0][...] = 0
int myMax(int a, int b, int c){
    return max(max(a, b), c);
}

int longestComStr(string s1, string s2){
    int m = s1.size(), n = s2.size();
    int dp[m + 1][n + 1];
    // 初始化
    for(int i = 0; i <= m; i++)
        dp[i][0] = 0;
    for(int i = 0; i <= n; i++)
        dp[0][i] = 0;
    for(int i = 1; i <= m; i++){
        for(int j = 1; j <= n; j++){
            if(s1[i - 1] == s2[j - 1])
                dp[i][j] = dp[i - 1][j - 1] + 1;
            else
                // 這個地方寫成dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])就行了
                dp[i][j] = myMax(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
    }
    return dp[m][n];
}
例題五:求兩個字符串的最小編輯距離

  和上一個題一樣,一般來說,處理兩個字符串的動態規劃問題,都是按本文的思路處理,建立 DP table。爲什麼呢,因爲易於找出狀態轉移的關係。這裏的dp(i)(j)數組表示的是 s1[0..i] 和 s2[0..j] 的最小編輯距離。

  • 定義二維數組dp[i][j]:當字符串s1長度爲i,字符串s2長度爲j時,它們的最短編輯距離是dp[i][j]
  • 數組元素之間的關係:
    • s1[i - 1] == s2[j - 1]時:dp[i][j] = dp[i - 1][j - 1]
    • s1[i - 1] != s2[j - 1]時:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
  • 初始條件:dp[i][0] = i;dp[0][i] = i
int min(int a, int b, int c){
    return min(min(a, b), c);
}

int minDistance(string s1, string s2){
    int m = s1.size(), n = s2.size();
    int dp[m + 1][n + 1];
    // 初始化
    for(int i = 1; i <= m; i++)
        dp[i][0] = i;
    for(int i = 1; i <= n; i++)
        dp[0][i] = i;
    for(int i = 1; i <= m; i++){
        for(int j = 1;j <= n; j++){
            if(s1[i - 1] == s2[j - 1])
                dp[i][j] = dp[i - 1][j - 1];
            else{
                dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
            }
        }
    }
    // 儲存着整個s1和s2的最小編輯距離
    return dp[m][n];
}

總結子序列問題模板

首先注意區分一個問題:

  • 子序列:可以不連續的子字符串/子數組
  • 子串:必須是連續的子字符串/子數組

遇到子序列問題,首先想到兩種動態規劃思路,然後根據實際問題看看哪種思路容易找到狀態轉移關係。

  這類問題都是讓你求一個最長子序列,因爲最短子序列就是一個字符嘛,沒啥可問的。一旦涉及到子序列和最值,那幾乎可以肯定,考察的是動態規劃技巧,時間複雜度一般都是 O(n^2)

1 第一種思路模板是一維的 dp 數組

最長遞增子序列(注意是序列,可以不連續)

  • 定義一維dp[i]:數組中以num[i]結尾的最長遞增序列爲dp[i]。我們要求的就是所有的dp[i]中最大的那一個
  • 數組元素之間的關係:dp[i] = max(dp[i], dp[j] + 1),其中num[j] < num[i]
  • 初始條件:dp[...] = 1,保證最短爲1
int lengthOfLIS(int nums[], int n){

    vector<int> dp(n, 1);

    for(int i = 0; i < n; i++){
        for(int j = 0; j < i; j++){
            if(nums[j] < nums[i])
                dp[i] = max(dp[i], dp[j] + 1);
        }
    }

    int ans = INT_MIN;
    for(int i = 0; i < n; i++)
        ans = max(ans, dp[i]);

    return ans;
}

2 第二種思路模板是二維的 dp 數組

這種思路數組含義又分爲「只涉及一個字符串」和「涉及兩個字符串」兩種情況

2.1 涉及兩個字符串/數組時
  • 最長公共子序列
  • 最短編輯距離
2.2 涉及一個字符串/數組時

最長迴文子序列(注意,和最長迴文子串不一樣,子序列可以不連續)

  • 定義二維dp[i][j] 數組:在子串s[i..j]中,最長迴文子序列的長度爲dp[i][j]。我們要求的就是dp[0][n - 1]
  • 數組元素之間的關係
    • s[i] = s[j]時:dp[i][j] = dp[i + 1][j - 1] + 2
    • s[i] != s[j]時:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
  • 初始條件:dp[i][i] = 1

爲了保證每次計算dp[i][j],左、下、左下三個方向的位置已經被計算出來,只能斜着遍歷或者反着遍歷,本例選擇反着遍歷:

// 反着遍歷
int longestPalindromeSubseq(string s){
    int n = s.size();
    int dp[n][n];
    // 初始化
    memset(dp, 0, sizeof(dp));
    for(int i = 0; i < n; i++)
        dp[i][i] = 1;
    for(int i = n - 1; i >= 0; i--){
        for(int j = i + 1; j < n; j++){
            if(s[i] == s[j])
                dp[i][j] = dp[i + 1][j - 1] + 2;
            else
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
        }
    }
    // 返回整個s的最長迴文子序列長度
    return dp[0][n - 1];
}

帥地的動歸

1 一維dp

例題一:青蛙跳臺階

一隻青蛙一次可以跳上1級臺階,也可以跳上2級臺階。求該青蛙跳上一個n級臺階總共有多少種跳法?

  • 定義一維dp[i]:跳上一個i級的臺階共有dp[i]種跳法,我們要求的就是dp[n]
  • 數組元素之間的關係:dp[i] = dp[i - 1] + dp[i - 2]
  • 初始條件:dp[0] = 0;dp[1] = 1;dp[2] = 2
完整代碼
// 跳臺階問題
// dp[n]表示跳上一個n階臺階共有dp[n]種跳法
int f(int n){
    if(n <= 2)
        return n;
    int dp[n + 1];
    dp[0] = 0;
    dp[1] = 1;
    dp[2] = 2;
    for(int i = 3; i <= n; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[n];
}

2 二維dp

例題二:機器人走路(不含權值)

⼀個機器⼈位於⼀個 m x n ⽹格的左上⻆ (起始點在下圖中標記爲“Start” )。
機器⼈每次只能向下或者向右移動⼀步。機器⼈試圖達到⽹格的右下⻆(在下圖中標記爲“Finish”)。
問總共有多少條不同的路徑?

  • 定義二維dp[i][j]:當機器人從左上角走到(i, j)這個位置,共有dp[i][j]種路徑,我們要求的就是dp[m - 1][n - 1]
  • 數組元素之間的關係:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
  • 初始條件:dp[...][0] = 1;dp[0][...] = 1,因爲第一行只能往左走,第一列只能往下走
完整代碼
// 機器人走路(無路徑權值)
// dp[i][j]表示當機器人從左上角走到(i,j)這個位置時,一共有dp[i][j]種路徑
int f(int m, int n){
    if(m < 0 || n < 0)
        return 0;

    int dp[m][n];
    for(int i = 0; i < m; i++)
        dp[i][0] = 1;
    for(int i = 0; i < n; i++)
        dp[0][i] = 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];
}
例題三:機器人走路的最短路徑(含權值)

給定⼀個包含⾮負整數的 m x n ⽹格,請找出⼀條從左上⻆到右下⻆的路徑,使得路徑上的數字總和爲最⼩。

  • 定義二維dp[i][j]:當機器人從左上角走到(i, j)這個位置的最短路徑,我們要求的就是dp[m - 1][n - 1]
  • 數組元素之間的關係:dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j]
  • 初始條件:dp[i][0] = dp[i - 1][0] + val[i][0];dp[0][i] = dp[0][i - 1] + val[0][i]
完整代碼
// 機器人走路,有路徑權值
// dp[i][j]表示機器人從左上角走到(i,j)這個位置的最短路徑值
int f(int val[][], int m, int n){
    int dp[m][n];
    dp[0][0] = val[0][0];
    for(int i = 1; i < m; i++)
        dp[i][0] = dp[i - 1][0] + val[i][0];

    for(int i = 1; i < n; i++)
        dp[0][i] = dp[0][i - 1] + val[0][i - 1];

    for(int i = 1; i < m; i++){
        for(int j = 1; j < n; j++){
            dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j];
        }
    }
    return dp[m - 1][n - 1];
}
例題四:最短編輯距離

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使⽤的最少操作數 。
你可以對⼀個單詞進⾏如下三種操作:

插⼊⼀個字符 刪除⼀個字符 替換⼀個字符

  • 定義二維數組dp[i][j]:當字符串s1長度爲i,字符串s2長度爲j時,它們的最短編輯距離是dp[i][j]
  • 數組元素之間的關係:
    • s1[i - 1] == s2[j - 1]時:dp[i][j] = dp[i - 1][j - 1]
    • s1[i - 1] != s2[j - 1]時:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
  • 初始條件:dp[i][0] = i;dp[0][i] = i
完整代碼
int min(int a, int b, int c){
    return min(min(a, b), c);
}

int minDistance(string s1, string s2){
    int m = s1.size(), n = s2.size();
    int dp[m + 1][n + 1];
    // 初始化
    for(int i = 1; i <= m; i++)
        dp[i][0] = i;
    for(int i = 1; i <= n; i++)
        dp[0][i] = i;
    for(int i = 1; i <= m; i++){
        for(int j = 1;j <= n; j++){
            if(s1[i - 1] == s2[j - 1])
                dp[i][j] = dp[i - 1][j - 1];
            else{
                dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
            }
        }
    }
    // 儲存着整個s1和s2的最小編輯距離
    return dp[m][n];
}

王爭老師的動歸

例題一:0-1揹包問題
//weight:物品重量,n:物品個數,w:揹包可承載重量
// 二維dp
public int knapsack(int[] weight, int n, int w) {
  boolean[][] states = new boolean[n][w+1]; // 默認值false
  states[0][0] = true;  // 第一行的數據要特殊處理,可以利用哨兵優化
  if (weight[0] <= w) {
    states[0][weight[0]] = true;
  }
  for (int i = 1; i < n; ++i) { // 動態規劃狀態轉移
    for (int j = 0; j <= w; ++j) {// 不把第i個物品放入揹包
      if (states[i-1][j] == true) states[i][j] = states[i-1][j];
    }
    for (int j = 0; j <= w-weight[i]; ++j) {//把第i個物品放入揹包
      if (states[i-1][j]==true) states[i][j+weight[i]] = true;
    }
  }
  for (int i = w; i >= 0; --i) { // 輸出結果
    if (states[n-1][i] == true) return i;
  }
  return 0;
}

// 一維dp
public static int knapsack2(int[] items, int n, int w) {
  boolean[] states = new boolean[w+1]; // 默認值false
  states[0] = true;  // 第一行的數據要特殊處理,可以利用哨兵優化
  if (items[0] <= w) {
    states[items[0]] = true;
  }
  for (int i = 1; i < n; ++i) { // 動態規劃
    for (int j = w-items[i]; j >= 0; --j) {//把第i個物品放入揹包
      if (states[j]==true) states[j+items[i]] = true;
    }
  }
  for (int i = w; i >= 0; --i) { // 輸出結果
    if (states[i] == true) return i;
  }
  return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章