动态规划题目汇总

动态规划的提示

  • 多阶段决策求最优解
  • 一维数组:一维动态规划 -> 写出状态转移方程,然后进行空间压缩
  • 二维数组:二维动态规划 -> 二维状态转移表法 or 状态转移方程法

基本动态规划:一维

好像都不太容易画递归树

70. 爬楼梯

class Solution {
public:
    int climbStairs(int n) {

        // 我的写法
        /* 动态规划
            1. 问题划分:是一个多阶段决策问题,每次决定爬1或2
            2. 定义状态dp[i]:表示爬到第i阶,一共有dp[i]种方法
            3. 状态转移方程:dp[i] = dp[i-1] + dp[i-2]
        */
        if (n <= 2) return n;

        vector<int> dp(n + 1);
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; ++i)
        {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];

        // 空间压缩写法:dp[i]只跟i-1和i-2状态有关系,所以只要定义prevprev、prev和cur三个变量即可
        if(n <= 2) return n;
        int prevprev = 1, prev = 2, cur;
        for(int i = 3; i <= n; ++i)
        {
            cur = prevprev + prev;
            prevprev = prev;
            prev = cur;
        }
        return cur;
    }
};

198. 打家劫舍

class Solution {
public:
    int rob(vector<int>& nums) {
        /* 动态规划
            我最开始的思路(有问题):
            1. 问题划分:每次觉得要不要偷这个房屋,求能偷到的最高金额,是一个多阶段决策求最优解问题
            2. 定义状态dp[i]表示以会偷第i间房屋结尾时,能偷到的最高金额
            3. 状态转移方程:dp[i] = max(dp[i - 2] + nums[i], dp[i])

            正确题解思路:
            1. 定义状态dp[i]表示,到了第i间房屋时,能偷到的最高金额
            2. 状态转移方程:dp[i] = max(dp[i-1], dp[i-2]+nums[i]),第一项表示不偷第i间,第二项表示偷第i间
        */

        // (建议使用~不易错)我自己按照正确解题思路的写法:多写了一些边界条件
        int n = nums.size();
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0], nums[1]);

        vector<int> dp(n, 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for(int i = 2; i < n; ++i)
        {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];


        // leetcode 101的写法:dp设置成长度为n+1的数组,在最开头补一个0,这样可以少写边界条件;但这样要注意dp[i]对应的是nums[i-1]
        int n = nums.size();
        if(n == 0) return 0;

        vector<int> dp(n + 1, 0);
        dp[1] = nums[0];
        for(int i = 2; i <= n; ++i)
        {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        return dp[n];


        // 转成空间压缩写法
        int n = nums.size();
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0], nums[1]);
        int prevprev = nums[0], prev = max(nums[0], nums[1]), cur;
        for(int i = 2; i < n; ++i)
        {
            cur = max(prev, prevprev + nums[i]);
            prevprev = prev;
            prev = cur;
        }
        return cur;
    }
};

213. 打家劫舍 II

class Solution {
public:
    int rob(vector<int>& nums) {
        /* 动态规划 + 环形处理
           1. 分别求nums[0:-1)和nums[1:-1]的最大金额,再求最大
           2. 实现细节:
            (1) c++没有直接对数组切片的功能,所以helper里指定起始下标和终止下标;并且因为dp的下标和nums的下标难对应,所以采用空间压缩的写法忽略dp的下标
            (2) 空间压缩时,看dp[i]依赖到了什么,比如这里dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]),dp[i]只依赖到了i-1和i-2,所以只需要prevprev, prev, cur三个变量就好
        */
       int n = nums.size();
       if (n == 0) return 0;
       if (n == 1) return nums[0];
       if (n == 2) return max(nums[0], nums[1]);
       // 前面的边界,保证进到helper里时,左下标小于右下标
       return max(helper(nums, 0, n - 1), helper(nums, 1, n)); // 左闭右开
    }

    int helper(vector<int> nums, int idx_begin, int idx_end)
    {
        int prevprev = 0;
        int prev = nums[idx_begin];
        int cur = 0;
        for(int i = idx_begin + 1; i < idx_end; ++i)
        {
            cur = max(prev, prevprev + nums[i]);
            prevprev = prev;
            prev = cur;
        }
        return cur;
    }
};

413. 等差数列划分

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& nums) {
        /* 动态规划
            1. 依次查看每个数,决定是否可以作为等差数列的结尾元素
            2. 定义状态dp[i]:以第i个元素结尾的等差数列的个数
            3. 状态转移方程:如果第i个元素可以作为等差数列的结尾(nums[i]-nums[i-1]=nums[i-1]-nums[i-2]),则dp[i] = dp[i-1] + 1;否则dp[i]=0
            4. 结果是sum(dp[i]), i从0到n
        */

        int n = nums.size();
        if(n <= 2) return 0;
        vector<int> dp(n, 0);
        for(int i = 2; i < n; ++i)
        {
            if(nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) dp[i] = dp[i - 1] + 1;
        }
        int res = 0;
        for(int i = 0; i < n; ++i) res += dp[i];
        return res;
    }
};

基本动态规划:二维

64. 最小路径和

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        /* 动态规划
            1. 每个位置上决策往下还是往右,所以是多阶段决策求最优解问题,用动态规划
            2. 定义状态dp[i, j],是到达位置ij上的最短路径
            3. 状态转移方程:dp[i, j] = min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
            4. 结果是dp[n-1, n-1]

            5. 可以画出递归树,状态f(i,j,l)为到第ij位置的路径为l,会发现有重复的状态ij,只要记录它的最短路径即可,所以可以用动态规划
        */

        int m = grid.size(), n = grid[0].size();

        vector<vector<int>> dp(m, vector<int>(n, 0));
        
        dp[0][0] = grid[0][0];
        for(int j = 1; j < n; ++j) dp[0][j] = dp[0][j - 1] + grid[0][j];
        for(int i = 1; i < m; ++i) dp[i][0] = dp[i - 1][0] + grid[i][0];
        for(int i = 1; i < m; ++i) // 这里i从1开始,而不是2
        {
            for(int j = 1; j < n; ++j) // 这里j从1开始,而不是2
            {
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[m - 1][n - 1];


        // 进行空间压缩:从行上进行压缩,dp[j] = min(老dp[j], 新dp[j - 1])+grid[i][j],所以要先更新j-1再更新j,所以j从左往右遍历
        int m = grid.size(), n = grid[0].size();
        vector<int> dp(n, 0);
        // 初始化第一个值
        dp[0] = grid[0][0];
        // 初始化第0行
        for(int j = 1; j < n; ++j)
        {
            dp[j] = dp[j - 1] + grid[0][j];
        }
        // 对于第1到m-1行,进行状态转移
        for(int i = 1; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(j == 0) dp[j] = dp[j] + grid[i][j];
                else dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
            }
        }
        return dp[n - 1];
    }
};

542. 01 矩阵

class Solution {
public:
    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
        /* 动态规划
            1. 为什么不用广度优先搜索,而是用动态规划的状态转移?因为广度优先搜索要对每个位置上进行四向搜索,复杂度太高了
            2. dp[i][j]为第ij个元素到最近的0的距离
            3. 如果mat[i][j]==0,dp[i][j]=0;如果mat[i][j]!=0, dp[i][j] = min(dp[i-1][j], dp[i+1][j], dp[i][j-1], dp[i][j+1]) + 1。但这里的四向搜索需要满足无后效性,也就是后一个状态完全由之前的状态决定,在一个位置上通过一次遍历无法让四周先变到前序状态,只能先左上来一次、再右下来一次,取最小。
        */
        
        // // 我尝试的实现(未成功)
        // // 实现时遇到的问题及处理方法:
        // //  1.dp里的初始值设置为INT_MAX,+1时会上溢;-> 设置为INT_MAX-1,每次取min时,如果周围都是未接触到0的,那么这里会是min(INT_MAX-1, INT_MAX-1 + 1)就还是INT_MAX-1
        // //  2.边界条件太多了;-> 对于00位置、无需特殊处理,因为要么是0、要么是INT_MAX-1;对于第0行、第0列,与其他行列合并处理了,合并的方法是,把min(dp[i-1][j], dp[i][j-1])拆成了先看列:if(j>0) min(dp[i][j], dp[i][j-1]+1), 再看行:if(i>0) min(dp[i][j], dp[i-1][j]+1)。这样规避了对于第0行、第0列的特殊边界处理。
        // int m = mat.size(), n = mat[0].size();
        // vector<vector<int>> dp(m, vector<int>(n, INT_MAX));

        // // 左上->右下遍历
        // // 先初始化[0][0]
        // // 再初始化第1行
        // // 再初始化第1列
        // // 后续填表
        // dp[0][0] = mat[0][0] == 0? 0 : INT_MAX;
        // for(int j = 1; j < n; ++j) mat[0][j] == 0? 0 : dp[0][j] = dp[0][j-1] + 1;

        // // 右下->左上遍历
        // // 先初始化[m-1][n-1]
        // // 再初始化第m-1行
        // // 再初始化第n-1列
        // // 后续填表


        // 看答案后的实现
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> dp(m, vector<int> (n, INT_MAX - 1));

        // 左上->右下
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(mat[i][j] == 0) dp[i][j] = 0;
                else
                {
                    if(j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
                    if(i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
                }
            }
        }

        // 右下->左上
        for(int i = m - 1; i >= 0; --i)
        {
            for(int j = n - 1; j >= 0; --j)
            {
                if(mat[i][j] != 0)
                {
                    if(j < n - 1) dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
                    if(i < m - 1) dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
                }
            }
        }

        return dp;
    }
};

221. 最大正方形

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        /* 动态规划
            1. 为什么不是深度优先搜索,而是动态规划?如果是找不规则的最大岛屿面积、那是在每个位置上去进行深度优先搜索;但这里有要求形状是正方形,就需要对ij位置的元素进行规则判断、和决策是否加入,所以是动态规划;
            2. 定义状态dp[i][j]:以ij位置的元素为右下角的最大边长
            3. 状态转移方程:
                if matrix[i][j] == 0, dp[i][j] = 0;
                else:
                    (最开始的想法,dp[i][j]由dp[i-1][j-1]决定,想错了
                    if(matrix[i][j-1] == 1 && matrix[i-1][j] == 1 && matrix[i-1][j-1]==1) dp[i][j] = dp[???])
                    正确的想法,dp[i][j]由dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]里的最小值决定,因为是个正方形
                    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 // 别忘了加一
        */

        int m = matrix.size(), n = matrix[0].size(), max_size = 0;
        vector<vector<int>> dp(m, vector<int> (n, INT_MAX));
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(matrix[i][j] == '0') dp[i][j] = 0;
                else
                {
                    dp[i][j] = 1;
                    if(i >= 1 && j >= 1)
                    {
                        dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
                    }
                }
                max_size = max(max_size, dp[i][j]);
            }
        }

        return max_size * max_size;
    }
};

分割类型题

279. 完全平方数

class Solution {
public:
    int numSquares(int n) {
        /* 动态规划(自己完全没想出来怎么做)
            1. 为什么用动态规划,而不是回溯、贪心?这道题一定有解(比如n个1),并且可能有多种解(比如剩下的全用1来补充),所以会涉及到达到同状态下的重复子问题,所以适合用动态规划而非回溯,来减少复杂度;不用贪心是因为贪心不一定是最优,而本题要最优解。
            2. 我自己的思考是:可以看作是从1,2,3...,int(sqrt(n))里可放回地采样,求和为指定值时,最少采样次数,有点像找零钱的问题。但有点定义不出来状态,不知道这样看成完全背包问题要怎么解???
            3. 答案的做法:分割问题当前状态由前序满足条件的状态决定、而非由前序连续状态决定。定义dp[i]为数字i最少可由dp[i]个完全平方数相加得到,那么dp[i] = 1 + min{dp[i-1], dp[i-4], ..., dp[i-j*j]},实现的时候,i从0推到n;对于每个i,j从1遍历到j*j<=i。
        */
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for(int i = 1; i <= n; ++i)
        {
            for(int j = 1; j * j <= i; ++j) // j * j <= i,不是j * j < i,不然下面i-j*j会出错
            {
                dp[i] = min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }
};

91. 解码方法

class Solution {
public:
    int numDecodings(string s) {
        /* 动态规划
            1. 定义dp[i]:到第i个字符为止,最多有dp[i]种解码方法
            2. 状态转移方程:以下两种情况解码方法数量之和
            (1)第一种情况,只使用第i个字符解码。条件为nums[i]!=0,解码方法数为dp[i-1];
            (2)第二种情况,使用第i-1和第i个字符解码。条件为i>1 && nums[i-1] != 0 && nums[i-1]*10+nums[i]<=26,解码方法数为dp[i-2];
            3. 写代码时注意,dp多加了一个标志位,所以dp里的i对应到nums里是i-1。
        */

       int n = s.size();
       vector<int> dp(n + 1); // 初始化为全0
       dp[0] = 1; // 方便dp[i] = dp[i - 1]
       for(int i = 1; i <= n; ++i)
       {
            if(s[i - 1] - '0' != 0) dp[i] = dp[i - 1];
            if(i > 1 && (s[i - 2] - '0') != 0 && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0')) <= 26) dp[i] += dp[i - 2];
       }
       return dp[n];
    }
};

139. 单词拆分

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        /* DP
            1. dp[i] = true/false: 到s的第i个字符为止,是否可分割
            2. 状态转移方程:dp[i] = dp[i] || dp[i - len] if i > len && s.substr(i-len, len)==word for word in wordDict
            3. 注意s.substr()第一个参数是起始位置,第二个参数是长度(不是结束位置)
        */
       int n = s.size();
       vector<bool> dp(n + 1, false);
       dp[0] = true;
       for(int i = 1; i <= n; ++i)
       {
            for(auto & word : wordDict)
            {
                int len = word.length();
                if(i >= len && s.substr(i - len, len) == word) dp[i] = dp[i] || dp[i - len]; // 本来应该是i-len+1,但s比dp要少1,所以是i-len
            }
       }
       return dp[n];
    }
};

343. 整数拆分

class Solution {
public:
    int integerBreak(int n) {
        /* 动态规划 + 分割问题
            1. 定义dp[i]为将正整数i拆分为多个正整数的和时的最大乘积

            // 我最初的想法,错误的,这样的问题是少算了拆成两个数字(i-j)*j的情况,也即部分dp没有正确初始化
            2. dp[i] = max(dp[i-1]*1, dp[i-2]*2, ..., dp[i-j]*j, dp[1]*(i-1)),j从1到i-1
            // 官方的做法,对于j从1到i-1的每个j,要从以下两种情况里求max
            (1)i拆成2个数字,所以乘积是(i-j)*j
            (2)i拆成多个数字,所以最大乘积是dp[i-j]*j
        */

        // // 我最初的想法,错误的
        // vector<int> dp(n + 1, 1);
        // for(int i = 1; i <= n; ++i)
        // {
        //     for(int j = 1; j < i; ++j)
        //     {
        //         dp[i] = max(dp[i], dp[i - j] * j);
        //     }
        // }
        // return dp[n];

        // 官方做法
        vector<int> dp(n + 1, 1);
        for(int i = 2; i <= n; ++i)
        {
            for(int j = 1; j < i; ++j)
            {
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); // 别忘了dp[i]自己记录住最大值
            }
        }
        return dp[n];
    }
};

子序列问题

300. 最长递增子序列

  • 动态规划解法,时间复杂度 ( N 2 ) (N^2) (N2)
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        /* 动态规划
            定义状态:dp[i]表示以位置i结尾的最长严格递增子序列的长度
            状态转移方程:dp[i] = max(dp[j] + 1, dp[i]), j from [0, i) && nums[j] < nums[i]
        */
        int n = nums.size();
        if(n < 2) return n;

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

646. 最长数对链

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        /* 动态规划 + 最长递增子序列的变形
            1. “以任何顺序”指的是从左到右但可跳跃?
            2. dp[i]为以第i个数对结尾的最长数对链的长度
            3. dp[i]为以下情况求最大:
                (1) 不包含以前的数对,dp[i] = 1
                (2) 包含以前的数对,dp[i] = max(dp[i-1]+1, dp[i-2]+1, ..., dp[i-j]+1, ..., dp[1] + 1), 遍历所有满足nums[i-j][1] < nums[i][0]的j

            然而“以任何顺序”并非如此,它是指任意顺序,所以以上想法不对。得先对pairs排序,再按以上方法。

        */

        sort(pairs.begin(), pairs.end()); //默认递增排序,默认先按第一个元素、再按第二个元素排序

        int n = pairs.size();
        vector<int> dp(n + 1, 1);
        dp[0] = 0;
        for(int i = 1; i <= n; ++i)
        {
            for(int j = 1; j < i; ++j)
            {
                if(pairs[i - j - 1][1] < pairs[i - 1][0]) // pairs的索引比dp少1
                {
                    dp[i] = max(dp[i], dp[i - j] + 1);
                }
            }
        }
        return dp[n];
    }
};

376. 摆动序列

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        /* 动态规划 + 最长递增子序列的变形

        // 我的思路(忽略掉):不太fancy,需要额外记录sign
            1. dp[i]为以第i个元素结尾的最长摆动子序列的长度, sign[i]为以第i个元素结尾的最长摆动子序列最后一个差值
            2. dp[i] = max(dp[i], dp[i - j] + 1) 对于所有满足(nums[i] - nums[i - j]) * sign[i - j] < 0的j,j从1到i-2

        // 按照这个链接里的解题思路,定义up和down两个状态:https://leetcode.cn/problems/wiggle-subsequence/solution/tan-xin-si-lu-qing-xi-er-zheng-que-de-ti-jie-by-lg/
            1. up[i]表示nums[0..i]中最后两个数字递增的最长摆动序列长度(不一定以i结尾);
               down[i]表示nums[0...i]中最后两个数字递减的最长摆动序列长度(不一定以i结尾)
            2. 状态转移:up一定由前序down来,down一定又前序up来
                (1)如果nums[i] == nums[i-1], 那么up[i]=up[i-1], down[i]=down[i-1]
                (2)如果nums[i] < nums[i-1], 
                    (2.1)如果up[i-1]是以i-1结尾的, 那么down[i] = up[i-1] + 1;
                    (2.2)如果up[i-1]不是以i-1结尾的,是以j结尾的,j<i-1,那么从j到i-1一定是递减的,不然up[i-1]就大于up[j]了,所以up[j]=up[j+1]=...=up[i-1],所以down[i] = up[i-1]+1.
                (3)如果nums[i] > nums[i-1],
                    (3.1)如果down[i-1]是以i-1结尾的,那么up[i] = down[i-1] + 1;
                    (3.2)如果down[i-1]不是以i-1结尾的,是以j结尾的,j<i-1,那么从j到i-1一定是递增的,要不然down[i-1]就大于down[j]了,所以down[j]=down[j+1]=...=down[i-1],所以up[i] = down[i-1] + 1。
        */

        int n = nums.size();
        vector<int> up(n, 1);
        vector<int> down(n, 1);
        for(int i = 1; i < n; ++i)
        {
            if(i == 1)
            {
                if(nums[i] > nums[i - 1]) up[i] = 2;
                else if (nums[i] < nums[i - 1]) down[i] = 2;
            }
            else
            {
                if(nums[i] == nums[i - 1])
                {
                    up[i] = up[i - 1];
                    down[i] = down[i - 1];
                }
                else if(nums[i] < nums[i - 1])
                {
                    down[i] = up[i - 1] + 1;
                    up[i] = up[i - 1];
                }
                else
                {
                    up[i] = down[i - 1] + 1;
                    down[i] = down[i - 1];
                }
            }
        }
        return max(up[n - 1], down[n - 1]);
    }
};

1143. 最长公共子序列

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        /* 动态规划
            1. 公共子序列:可以不连续;公共子串:必须连续
            2. 定义状态dp[i][j]:text1[0,...,i]和text2[0,...,j]的最长公共子序列长度,最长公共子序列不必非得以i、j结尾
            3. 状态转移方程(自己没太想清楚,看了理论讲解,又看了答案):
                if text1[i] == text2[j]: dp[i][j] = max(dp[i-1][j-1]+1, dp[i-1][j], dp[i][j-1]),第一项是指什么都不删,后面两项是说要删掉text2[j]和text1[i]。实际上dp[i-1][j]和dp[i][j-1]一定是>=dp[i-1][j-1]的,并且不会大超过1,所以只需要dp[i][j] = dp[i-1][j-1]+1
                else: dp[i][j] = max(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]),第一项是指删掉text1[i]和text2[j],后面两项是说要删掉text2[j]和text1[i]。实际上dp[i-1][j]和dp[i][j-1]一定是>=dp[i-1][j-1]的,所以只需要dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        */

        int m = text1.length(), n = text2.length();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 这样定义,dp[0][j]和dp[i][j]可以不用特殊初始化了
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[m][n];
    }
};

子串问题

53. 最大子数组和

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        /* 动态规划
            1. 子数组=子串=subarray,是连续的;子序列=subsequence,是可以不连续的
            2. 定义dp[i]为以第i个元素为结尾的子数组的最大和,最后的结果是遍历dp各元素里的最大值
            3. 转移方程dp[i] = max(nums[i], dp[i-1] + nums[i])
        */

        // // 我自己最开始的写法,会有单个负值元素过不了,因为如果dp[0]初始为0之后,如果nums[0]是负值,那么dp[1]会是0、而不是负值。
        // int n = nums.size();
        // // vector<int> dp(n + 1, 0); // 这里要初始化为可能的最小值
        // vector<int> dp(n + 1, -10001);
        // for(int i = 1; i <= n; ++i)
        // {
        //     dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1]);
        // }

        // int res = dp[0];
        // for(int i = 1; i <= n; ++i)
        // {
        //     res = res < dp[i]? dp[i] : res;
        // }
        // return res;

        // 空间压缩的写法
        int n = nums.size();
        int prev = nums[0], cur;
        int res = prev;
        for(int i = 1; i < n; ++i)
        {
            cur = max(nums[i], prev + nums[i]);
            res = max(res, cur);
            prev = cur; // 别丢了
        }
        return res;
    }
};

背包问题

有N个物品和容量为W的背包,每个物品有自己的体积w和价值v,求拿哪些物品可以使得背包所装下的物品等总价值最大。定义dp[N][W]为决策到第i个物品(不管选几次、都在一行里)、容量为W时的最大价值。

1. 0-1背包问题:每个物品只能选0次或1次

二维转移矩阵

在这里插入图片描述

状态转移方程、代码

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w]+v) if j>=w else dp[i-1][j]

  • dp[i-1][j]是对于当前物品的决策是不选
  • dp[i-1][j-W]+v表示对于当前物品的决策是选

在这里插入图片描述

空间优化

从右向左遍历,因为j依赖上次的j-w,所以要避免j-w先更新:
在这里插入图片描述

2. 完全背包问题:每个物品可以选任意次

二维转移矩阵

可以理解成:第i个物品可以进行多次决策,每次决策要么不选(向下)、要么在上次选了的基础上再选一次(向右)
在这里插入图片描述

状态转移方程、代码

dp[i][j] = max(dp[i-1][j], dp[i][j-w]+v) if j>=w else dp[i-1][j]

  • dp[i-1][j]是对于当前物品的决策是不选
  • dp[i][j-W]+v表示对于当前物品的决策是选(在对当前物品多次决策的基础上选,所以是i、不是i-1)
    在这里插入图片描述

空间优化

从左向右遍历,因为j依赖这次的j-w,所以要让j-w先更新:
在这里插入图片描述

416. 分割等和子集

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        /* 动态规划
            1. 0-1背包问题:对于nums里每个数,做拿/不拿的决策,使得拿的总值达到sum(nums)/2。每个数只有一次选择机会,所以是0-1背包,不是完全背包。
            2. 定义状态dp[i][j]=true/false,表示选择到第i个样本时,总和能否达到j这个值。i从0到nums.size(),j从0到sum(nums)/2。
            3. 状态转移:对于第i个元素做决策,
            (1)情况一:不选,dp[i][j] = dp[i - 1][j]
            (2)情况二:选,dp[i][j] = dp[i - 1][j - nums[i]]
        */
        int m = accumulate(nums.begin(), nums.end(), 0);
        if(m % 2) return false;

        int n = nums.size();
        vector<vector<bool>> dp(n + 1, vector<bool> (m / 2 + 1, false));
        dp[0][0] = true;
        for(int i = 1; i <= n; ++i)
        {
            for(int j = 0; j <= m / 2; ++j)
            {
                if(j == 0) dp[i][j] = true;
                else if(nums[i - 1] <= j) dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]; // 注意nums的i比dp的i要小1
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[n][m / 2]; 
    }
};

474. 一和零

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        /* 动态规划
            1. 0-1背包问题,对于strs里的每个string逐个决策要不要选,决策时需受限于0的个数(最多m个)和1的个数(最多n个)
            2. 定义状态dp[i][j][k],表示决策到第i个string为止,最多j个0、k个1的情况下,最多可以构成多少个string
            3. 状态转移方程:对于第i个字符串,假设有n0个0、n1个1
            (1)如果n0>j or n1>k,dp[i][j][k] = dp[i-1][j][k]
            (2)如果n0<=j && n1<=k,dp[i][j][k] = max(dp[i-1][j][k], dp[i][j-n0][k-n1]+1)
            4. 代码写法:注意同时返回两个数的函数,返回值类型是pair<int, int>,return是make_pair(a, b),返回值是auto [a, b]
        */

       int num_str = strs.size();
       vector<vector<vector<int>>> dp(num_str + 1, vector<vector<int>> (m + 1, vector<int> (n + 1, 0)));
       for(int i = 1; i <= num_str; ++i)
       {
            for(int j = 0; j <= m; ++j)
            {
                for(int k = 0; k <= n; ++k)
                {
                    auto [n0, n1] = count(strs[i - 1]); // 注意返回两个数的写法
                    if(n0 > j || n1 > k) dp[i][j][k] = dp[i - 1][j][k];
                    else dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - n0][k - n1] + 1);
                }
            }
       }
       return dp[num_str][m][n];
    }

    pair<int, int> count(string & str) // 同时返回两个数的函数
    {
        int n0 = 0, n1 = 0;
        for(auto & c: str) // 这样是char,下面得是单引号
        {
            if(c == '0') ++n0;
            else if(c == '1') ++n1;
        }
        return make_pair(n0, n1); // 同时返回两个数的函数
    }
};

494. 目标和

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        /* 动态规划:0-1背包问题
            1. 定义dp[i][j]为选择到第i个元素、target等于j时,不同表达式的数目
            2. 状态转移方程:dp[i][j]可以从左上or右上转移过来
            (1)是由'+'来的:if nums[j]<=j, dp[i][j] += dp[i-1][j-nums[j]]
            (2)是由'-'来的:if nums[j]>=j, dp[i][j] += dp[i-1][j+nums[j]]
            3. 可以由右侧转移过来,所以j的范围不只是0到target,而是[0,sum-1]、sum、[sum+1, 2*sum]一共2*sum+1个,也即全负到全正向右平移sum
            4. target在题目里可以是负数,不能直接作为下标,要做一下hash映射为非负整数,也即统一加sum;

            我的思路跟他的一样:https://leetcode.cn/problems/target-sum/solution/dong-tai-gui-hua-si-kao-quan-guo-cheng-by-keepal/
        */

        int n = nums.size();
        int sum = accumulate(nums.begin(), nums.end(), 0); // 还有个起始值0
        vector<vector<int>> dp(n, vector<int> (2 * sum + 1, 0));

        // 边界初始化
        if(nums[0] == 0) dp[0][0 + sum] = 2; // 0向右平移sum,如果是0加正负号都可
        else
        {
            dp[0][-nums[0] + sum] = 1; // 加负号后,向右平移sum
            dp[0][nums[0] + sum] = 1; // 加正号后,向右平移sum
        }
        
        // 填表
        for(int i = 1; i < n; ++i)
        {
            for(int j = 0; j < 2 * sum + 1; ++j)
            {
                if(j - nums[i] >= 0) dp[i][j] += dp[i - 1][j - nums[i]]; // 记得nums的索引是i不是j;这里是在j的基础上往左往右看,所以跟平移量sum无关
                if(j + nums[i] < 2 * sum + 1) dp[i][j] += dp[i - 1][j + nums[i]];
            }
        }
        return (target + sum >= 2 * sum + 1) || (target + sum < 0)? 0 : dp[n - 1][target + sum]; // target向右平移sum。如果target超出范围的话说明没有能够达成的
    }
};

322. 零钱兑换

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        /* 动态规划:
            1. 每个硬币可以多次选择,所以是完全背包问题,二维dp,行表示第i种硬币(一共coins.size()种,同一种不管选几次都只影响到列金额)、列表示金额j;
            2. 状态转移方程dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1)
            3. 可以将二维转到一维,进行空间优化
        */

    //    // 二维
    //    vector<vector<int>> dp(coins.size() + 1, vector<int> (amount + 1, amount + 1)); // 选择到第i种硬币时,能够达到金额j的最少硬币个数
    //    for(int i = 0; i <= coins.size(); ++i) dp[i][0] = 0;
    //    for(int i = 1; i <= coins.size(); ++i)
    //    {
    //         int w = coins[i - 1]; // 注意dp的行列各扩充了一格,对应到原来的数组时就要-1
    //         for(int j = 1; j <= amount; ++j)
    //         {
    //             if(j >= w) dp[i][j] = min(dp[i - 1][j], dp[i][j - w] + 1);
    //             else dp[i][j] = dp[i - 1][j];
    //         }
    //    }
    //    return dp[coins.size()][amount] > amount ? -1 : dp[coins.size()][amount];

       // 空间优化:一维dp
       vector<int> dp(amount + 1, amount + 1); // 去掉i
       dp[0] = 0;
       for(int i = 1; i <= coins.size(); ++i)
       {
            int w = coins[i - 1];
            for(int j = 1; j <= amount; ++j)
            {
                if(j >= w) dp[j] = min(dp[j], dp[j - w] + 1);
            }
       }
       return dp[amount] > amount ? -1 : dp[amount];

    }
};

字符串编辑

72. 编辑距离

class Solution {
public:
    int minDistance(string word1, string word2) {
        /* 动态规划
            1. 定义dp[i][j]为将word1[0,...,i-1]转换成word2[0,...,j-1]的最少操作数
            
            2. (我自己初步想的,想的不对)dp[i][j] = 以下情况求最小
                (1) 不改动:dp[i][j]
                (2) 插入一个字符:dp[i][j] + 1
                (3) (i > 1)删除一个字符:dp[i - 1][j] + 1
                (4) 替换一个字符:dp[i][j] + 1

            2. 分情况讨论:
            (1)如果word1[i] == word2[j],则不用改动:dp[i][j] = dp[i-1][j-1]
            (2)如果word1[i] != word2[j]:
                a. 替换word1[i]为word2[j](与替换word2[j]为word1[i]等价):dp[i-1][j-1] + 1(这里比较难想到,看等价就理解了)
                b. 删除word1[i](与插入word2[j]等价):dp[i][j] = dp[i - 1][j] + 1
                c. 插入word1[i](与删除word2[j]等价):dp[i][j] = dp[i][j - 1] + 1 (这里也比较难想到,看等价就理解了)
        */
        int m = word1.length(), n = word2.length();
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
        
        // 边界初始化
        dp[0][0] = 0;
        for(int j = 1; j <= n; ++j) dp[0][j] = dp[0][j - 1] + 1; // 相当于插入word1
        for(int i = 1; i <= m; ++i) dp[i][0] = dp[i - 1][0] + 1; // 相当于删除word1

        // 填表
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = min(dp[i - 1][j - 1] + 1, min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
            }
        }
        return dp[m][n];
    }
};

583. 两个字符串的删除操作

class Solution {
public:
    int minDistance(string word1, string word2) {
        /* 动态规划
            1. 定义dp[i][j]为word1[0...i-1]和word2[0...j-1]相同所需要的最小步数
            2. dp[i][j]由以下情况求min
            (1)如果word1[i]==word2[j],不需要删除,dp[i-1][j-1]
            (2)如果word1[i]!=word2[j]
                (2.1)只删除word1[i],dp[i - 1][j] + 1
                (2.2)只删除word2[j],dp[i][j - 1] + 1
                (2.3)既删除word1[i]又删除word2[j],dp[i - 1][j - 1] + 2 // 这种情况忘记考虑了
        */

        int m = word1.size(), n = word2.size();
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, m + n)); // 初始化值是既删除word1的所有值又删除word2的所有值,所以是m+n
        // 边界初始化
        dp[0][0] = 0; // 别忘了
        for(int i = 1; i <= m; ++i) // word2为空,删word1
        {
            dp[i][0] = i;
        }
        for(int j = 1; j <= n; ++j) // word1为空,删word2
        {
            dp[0][j] = j;
        }
        // 填表
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(word1[i - 1] == word2[j - 1]) dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]); // 注意这里word1和word2的下标,要比dp少1
                else dp[i][j] = min(min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + 2);
            }
        }
        return dp[m][n];
    }
};

650. 只有两个键的键盘

class Solution {
public:
    int minSteps(int n) {
        /* 动态规划(我的想法)
            1. dp[i]为输出恰好i个字符时,需要使用的最少操作次数
            2. dp[i]可由以下状态转移过来,取min
            (1)这次是复制,dp[i] + 1
            (2)上次是复制,这次是粘贴,dp[i / 2] + 1 if i % 2 == 0
            (3)上次是粘贴,这次也是粘贴,但不知道粘贴了几次,所以不知道是粘贴了多少次,所以对所有可取余的遍历一遍。可以和(2)合并
        */
        vector<int> dp(n + 1, n); // 默认初始值:到n,需要复制1次、粘贴n-1次,一共n次
        dp[0] = 0;
        dp[1] = 0;
        for(int i = 2; i <= n; ++i)
        {
            for(int j = 2; j <= n; ++j) // j是份数
            {
                if(i % j == 0)
                {
                    dp[i] = min(dp[i], dp[i / j] + 1 + j - 1); // 复制1次,粘贴j-1次
                }
            }
        }
        return dp[n];
    }
};

10. 正则表达式匹配

class Solution {
public:
    bool isMatch(string s, string p) {
        /* 动态规划
            1. 定义状态dp[i][j]为s[0...i-1]和p[0...j-1]是否匹配
            2. 状态转移方程的分析过程见官方题解下的评论区
            (1)如果p[j-1]是字母或'.':
                a. 如果p[j-1]和s[i-1]匹配,那么dp[i][j] = dp[i-1][j-1]
                b. 如果p[j-1]和s[i-1]不匹配,那么dp[i][j] = false
            (2)如果p[j-1]是'*':
                a. 把p[j-2]和p[j-1]当成一个整体,去和s[i-1]匹配:
                    a1: 如果匹配
                        1) 一方面可以看当前:dp[i][j] = dp[i][j-2]
                        2)另一方面也可以继续向前匹配,变成和s[i-2]是否匹配的子问题: dp[i][j] = dp[i-1][j]
                    a2: 如果没匹配(也即匹配0次,*是支持匹配0次的),只能看当前了:dp[i][j] = dp[i][j-2]
        */

       int m = s.size();
       int n = p.size();
       vector<vector<bool>> dp(m + 1, vector<bool> (n + 1, false));

       /* 边界初始化
          1. s空p空时,匹配
          2. s非空p空时,不匹配,默认false
          3. s空p非空时,p是"#*#*#*..."才匹配
       */
       dp[0][0] = true; 
       for(int j = 2; j <= n; j += 2)
       {
            if(p[j - 1] == '*') dp[0][j] = dp[0][j - 2];
       }

       /* 开始实现状态转移
       */
       for(int i = 1; i <= m; ++i) // 从s非空开始考虑
       {
            for(int j = 1; j <= n; ++j) // 从p非空开始考虑
            {
                if(p[j - 1] != '*')
                {
                    if(p[j - 1] == '.' || p[j - 1] == s[i - 1]) dp[i][j] = dp[i - 1][j - 1];
                    else dp[i][j] = false;
                }
                else // p是*的话(一定不是p下标0的位置(也即j下标1,也即j下标一定>=2),因为题目里已经说了出现*前面一定有有效字符)
                {
                    if(p[j - 2] == '.' || p[j - 2] == s[i - 1]) dp[i][j] = dp[i][j-2] || dp[i - 1][j];
                    else dp[i][j] = dp[i][j-2];
                }
            }
       }

       return dp[m][n];
    }
};

股票交易

121. 买卖股票的最佳时机

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /* 动态规划
            1. 定义dp[i]为到第i天为止能获取到的最大利润
            2. 状态转移方程:dp[i] = max(dp[i-1], prices[i] - min_price) if prices[i] > min_price else dp[i] = dp[i-1]
        */
        int min_price = prices[0];
        vector<int> dp(prices.size(), 0);
        for(int i = 1; i < prices.size(); ++i)
        {
            if(prices[i] > min_price)
            {
                dp[i] = max(dp[i - 1], prices[i] - min_price);
            }
            else
            {
                dp[i] = dp[i - 1];
            }
            min_price = min(min_price, prices[i]);
        }
        return dp[prices.size() - 1];


		/* 方法二:动态规划:每天都买卖,看状态转移
           1. 定义buy和sell分别是买入后手里的最大利润、卖出后手里的最大利润
           2. 按天更新buy和sell,最后的动作一定是卖出,所以sell就是返回值
        */
       int buy = INT_MIN, sell = 0;
       for(int i = 0; i < prices.size(); ++i)
       {
            buy = max(buy, -prices[i]);
            sell = max(sell, buy + prices[i]); // 这里buy是目前手头的钱,卖出相当于+prices;隔天才能卖在这里也不会影响,因为如果是当前买卖buy + prices[i]是0
       }
       return sell;
    }
};

122. 买卖股票的最佳时机 II

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /* 动态规划
            1. 状态转移:买-卖-买-卖...
            2. 定义状态:buy买入后当前最大利润,sell卖出后当前最大利润
        */
       int buy = INT_MIN, sell = 0;
       for(int i = 0; i < prices.size(); ++i)
       {
            buy = max(buy, sell - prices[i]);
            sell = max(sell, buy + prices[i]);
       }
       return sell;
    }
};

188. 买卖股票的最佳时机 IV

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        /* 动态规划
            1. 状态转移:买1-卖1-买2-卖2-...-买k-卖k
        */
       vector<int> buy(k + 1, INT_MIN), sell(k + 1, 0);
       for(int i = 0; i < prices.size(); ++i)
       {
            for(int j = 1; j <= k; ++j)
            {
                // buy[j] = max(buy[j - 1], sell[j - 1] - prices[i]);
                // sell[j] = max(sell[j - 1], buy[j] + prices[i]);

                buy[j] = max(buy[j], sell[j - 1] - prices[i]); // max(不买,买)
                sell[j] = max(sell[j], buy[j] + prices[i]); // // max(不卖,卖)
            }
       }
       return sell[k];
    }
};

309. 最佳买卖股票时机含冷冻期

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /* 动态规划
            1. 状态转移:买-卖-冷冻期-买-卖...
        */
       int buy = INT_MIN, sell = 0, cold = 0;
       for(int i = 0; i < prices.size(); ++i)
       {
            buy = max(buy, cold - prices[i]); // buy从cold来
            cold = max(cold, sell); // cold从sell来,放在sell之前是因为sell要变更了,得先记住它???
            sell = max(sell, buy + prices[i]); // sell从buy来
       }
       return sell;
    }
};

714. 买卖股票的最佳时机含手续费

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        /* 动态规划
            1. 无限次交易
            2. 有交易费用,在buy时算上fee即可
        */ 
       int buy = INT_MIN, sell = 0;
       for(int i = 0; i < prices.size(); ++i)
       {
            buy = max(buy, sell-prices[i]-fee);
            sell = max(sell, buy + prices[i]);
       }
       return sell;
    }
};
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值