• 动态规划大神总结——必读


    • 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 = 0; i <= m; i++)
            dp[i][0] = i;
        for(int i = 0; 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 = 0; i <= m; i++)
            dp[i][0] = i;
        for(int i = 0; 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;
    }
    
  • 相关阅读:
    7-4
    7-3
    第五章例5-2
    第五章例5-1
    第四章例4-12
    第四章例4-11
    第四章例4-10
    第四章例4-9
    第四章例4-8
    第四章例4-7
  • 原文地址:https://www.cnblogs.com/flyingrun/p/13606117.html
Copyright © 2020-2023  润新知