LeetCode高频题刷题笔记(十四)动态规划

本文详细介绍了动态规划在LeetCode中解题的多种应用场景,包括但不限于爬楼梯、最短路径、股票交易、背包问题、子序列问题、回文子串等。通过具体代码示例展示了动态规划的解题思路和状态转移方程,帮助读者理解动态规划在实际问题中的应用。
摘要由CSDN通过智能技术生成

基础知识

动态规划,英文:Dynamic Programming,简称DP。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。


题目

1. 爬楼梯( LeetCode 70

难度: 简单
题目表述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
代码(C++):

class Solution {
public:
    int climbStairs(int n) {
        int p = 0, q = 1, r = 0;
        for (int i = 1; i <= n; i++) {
            r = p + q;
            p = q;
            q = r;
        }
        return r;
    }
    // 完全背包求排列数
    // 一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。本题 m = 2
    int climbStairs(int n) {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for (int j = 1; j <= n; j++) {
            for (int i = 1; i <= 2; i++) { 
                if (j >= i) dp[j] += dp[j - i];
            }
        }
        return dp[n];
    }
};

题解:
1.斐波那契数列的第n项 : f(x)=f(x−1)+f(x−2)
2.一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。完全背包求排列数问题,本题 m = 2。


2. 使用最小花费爬楼梯( LeetCode 746

难度: 简单
题目表述:
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
代码(C++):

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1);
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <= cost.size(); i++) {
            dp[i] = min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
        }
        return dp[cost.size()];
    }
};

题解:


3. 不同路径( LeetCode 62

难度: 中等
题目表述:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
代码(C++):

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n));
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) {
            dp[0][j] = 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];
    }
};

题解:
动态规划转移方程:f(i,j)=f(i−1,j)+f(i,j−1)


4. 不同路径 II( LeetCode 63

难度: 中等
题目表述:
网格中的障碍物和空位置分别用 1 和 0 来表示。
代码(C++):

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        vector<int> dp(n, 0);
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
            dp[j] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (obstacleGrid[i][j] == 1) dp[j] = 0;
                else if (j > 0) dp[j] += dp[j - 1];
            }
        }
        return dp[n - 1];
    }
};

题解:


5. 整数拆分( LeetCode 343

难度: 中等
题目表述:
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
代码(C++):

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[2] = 1;
        for (int i = 3; i <= n; i++) {
            for (int j = 1; j <= i / 2; j++) {
                dp[i] = max({dp[i], j * (i - j), j * dp[i - j]});
            }
        }
        return dp[n];
    }
};

题解:
j * (i - j) 是单纯的把整数拆分为两个数相乘,而 j * dp[i - j] 是拆分成三个以及三个以上的个数相乘。


6. 不同的二叉搜索树( LeetCode 96

难度: 中等
题目表述:
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
代码(C++):

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};

题解:
dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]


7. 分割等和子集( LeetCode 416

难度: 中等
题目表述:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
代码(C++):

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }
        if (sum % 2) return false;
        int m = nums.size();
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < m; i++) {
            for (int j = target; j >= nums[i]; j--) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[target] == target;
    }
};

题解:
背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。
问题转化为在 sum / 2 容量的背包中最多可以装多少。dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。


8. 最后一块石头的重量 II( LeetCode 1049

难度: 中等
题目表述:
每一回合,从中选出任意两块石头,然后将它们一起粉碎。最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
代码(C++):

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size();
        int sum = 0;
        for (const auto &s: stones) {
            sum += s;
        }
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < n; i++) {
            for (int j = target; j >= stones[i]; j--) {
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};

题解:
尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小。
计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
上一题相当于是求背包是否正好装满,而本题是求背包最多能装多少。
问题转化为在 sum / 2 容量的背包中最多可以装多少。dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]。


9. 目标和( LeetCode 494

难度: 中等
题目表述:
返回可以向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数构造一个 表达式 、运算结果等于 target 的不同 表达式 的数目。
代码(C++):

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum = 0;
        for (const int &d : nums ) {
            sum += d;
        }
        if ((target + sum) % 2 || abs(target) > sum) return 0;
        int left = (target + sum) / 2;
        vector<int> dp(left + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < n; i++) {
            for (int j = left; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[left];
    }
};

题解:
left - right = target,left + right = sum => right = sum - left,则,
left - (sum - left) = target => left = (target + sum)/2 。
问题转化为装满容量为left的背包有几种方法。dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法


10. 一和零( LeetCode 474

难度: 中等
题目表述:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
代码(C++):

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>((n + 1), 0));
        for (string str : strs) {
            int zeroNum = 0, oneNum = 0; 
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

题解:
m 和 n相当于是一个背包,两个维度的背包
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。


11. 零钱兑换 II( LeetCode 518

难度: 中等
题目表述:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
代码(C++):

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < coins.size(); i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

题解: 本质是求完全背包 组合数 (先遍历物品,再遍历背包)


12.组合总和 Ⅳ( LeetCode 377

难度: 中等
题目表述:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。顺序不同的序列被视作不同的组合。
代码(C++):

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);
        dp[0] = 1;
        for (int j = 0; j <= target; j++) {
            for (int i = 0; i < nums.size(); i++) {
                if (j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]]) dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
};

题解: 本质是求完全背包 排列数 (先遍历背包,再遍历物品)


13. 零钱兑换( LeetCode 322

难度: 中等
题目表述:
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数
代码(C++):

class Solution {
public:
	//动态规划
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, amount + 1);
        dp[0] = 0;
        for (int i = 0; i < coins.size(); i++) {
            for (int j = coins[i]; j <= amount; j++) {//遍历背包【正序】
                dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
};

题解: 本质是求完全背包 组合数 / 排列数(for循环嵌套顺序无所谓)
要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!


14.完全平方数( LeetCode 279

难度: 中等
题目表述:
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
代码(C++):

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 1; i * i <= n; i++) {// 遍历物品
            for (int j = i * i; j <= n; j++) {// 遍历背包
                dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i <= n; i++) { // 遍历背包
            for (int j = 1; j * j <= i; j++) { // 遍历物品
                dp[i] = min(dp[i - j * j] + 1, dp[i]);
            }
        }
        return dp[n];
    }
};

题解: 本质是求完全背包 组合数 / 排列数(for循环嵌套顺序无所谓)
dp[j]:和为j的完全平方数的最少数量为dp[j]


15.单词拆分( LeetCode 139

难度: 中等
题目表述:
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
代码(C++):

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i++) { 	// 遍历背包
            for (string word : wordDict) {   	// 遍历物品
                int len = word.size();
                if (i >= len && dp[i - len] && word == s.substr(i - len, len)) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i++) {   // 遍历背包
            for (int j = 0; j < i; j++) {       // 遍历物品
                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        return dp[s.size()];
    }
};

题解: 本质是求完全背包 排列数 (先遍历背包,再遍历物品)
强调物品之间顺序,一定是 先遍历 背包,再遍历物品。


16. 打家劫舍( LeetCode 198

难度: 中等
题目表述:
不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[nums.size() - 1];
    }
};

题解: 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i],i间有偷或不偷两种情况


17. 打家劫舍 II( LeetCode 213

难度: 中等
题目表述:
所有的房屋都 围成一圈,不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

class Solution {
public:
    int robRange(vector<int>& nums, int left, int right) {
        if (left == right) return nums[left];
        vector<int> dp(nums.size());
        dp[left] = nums[left];
        dp[left + 1] = max(nums[left], nums[left + 1]);
        for (int i = left + 2; i <= right; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[right];
    }
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        return max(robRange(nums, 0, nums.size() - 2), robRange(nums, 1, nums.size() - 1));
    }
};

题解: max(考虑包含首元素,不包含尾元素, 考虑包含尾元素,不包含首元素)


18. ⭐打家劫舍 III( LeetCode 337

难度: 中等
题目表述:
房屋的排列类似于一棵二叉树,不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

class Solution {
public:
	// 递归
	unordered_map<TreeNode*, int> umap;
    int rob(TreeNode* root) {
        if (!root) return 0;
        if (!root->left && !root->right) return root->val;
        if (umap[root]) return umap[root];
        int val1 = rob(root->left) + rob(root->right); // 不偷父节点
        int val2 = root->val; //偷父节点
        if (root->left) {
            val2 += rob(root->left->left);
            val2 += rob(root->left->right);
        }
        if (root->right) {
            val2 += rob(root->right->left);
            val2 += rob(root->right->right);
        }
        umap[root] = max(val1, val2);
        return umap[root];
    }
    // 动态规划:长度为2的数组,0:不偷,1:偷
    vector<int> robTree(TreeNode* root) {
        if (!root) return {0, 0};
        vector<int> left = robTree(root->left);
        vector<int> right = robTree(root->right);
        int val1 = max(left[0], left[1]) + max(right[0], right[1]); // 不偷父节点
        int val2 = root->val + left[0] + right[0]; // 偷父节点
        return {val1, val2};
    }
    int rob(TreeNode* root) {
        vector<int> res = robTree(root);
        return max(res[0], res[1]);
    }
};

题解: 树形DP
下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。


19. 买卖股票的最佳时机 ( LeetCode 121

难度: 简单
题目表述:
只能买卖一次
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) {
            return 0;
        }
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], -prices[i]);
        }
        return dp[n - 1][0];    
    }
    // 空间复杂度降为O(1)
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) {
            return 0;
        }
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            dp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, -prices[i]);
        }
        return dp0;    
    }
};

题解: 股票全程只能买卖一次,那么推导第i天持有股票时用到的dp[i-1][0]一定就是0。
k = 1 维度k可省略
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
T[i][0][0] = 0


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

难度: 中等
题目表述:
可以买卖多次
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) return 0;
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
   	// 空间复杂度降为O(1)
   	int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) return 0;
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int newdp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, dp0 - prices[i]);
            dp0 = newdp0;
        }
        return dp0;
    }
};

题解: 一只股票可以买卖多次,那么推导第i天持有股票时用到的dp[i-1][0]就是之前买卖过的利润。
k = +∞ k 和 k - 1 可以看成是相同的,维度k可省略
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i]) = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
T[i - 1][k - 1][0] = T[i - 1][k][0]


21. 买卖股票的最佳时机 III( LeetCode 123 )⭐

难度: 困难
题目表述:
最多买卖两次
代码(C++):

class Solution {
public:
	int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp1(prices.size(), vector<int>(2, 0));
        vector<vector<int>> dp2(prices.size(), vector<int>(2, 0));
        dp1[0][0] = 0;
        dp1[0][1] = -prices[0];
        dp2[0][0] = 0;
        dp2[0][1] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            dp1[i][0] = max(dp1[i - 1][0], dp1[i - 1][1] + prices[i]);
            dp1[i][1] = max(dp1[i - 1][1], -prices[i]);
            dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1] + prices[i]);
            dp2[i][1] = max(dp2[i - 1][1], dp1[i - 1][0] - prices[i]);
        }
        return dp2[prices.size() - 1][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int profitOne0 = 0, profitOne1 = -prices[0], profitTwo0 = 0, profitTwo1 = -prices[0];
        for (int i = 1; i < n; i++) {
        	// 更新用到上一行结果,故 按照 k 的反序更新 2->1
        	profitTwo0 = max(profitTwo0, profitTwo1 + prices[i]);
            profitTwo1 = max(profitTwo1, profitOne0 - prices[i]);
            profitOne0 = max(profitOne0, profitOne1 + prices[i]);
            profitOne1 = max(profitOne1, -prices[i]);
        }
        return profitTwo0;
    }
};

题解:
T[i][2][0] = max(T[i - 1][2][0], T[i - 1][2][1] + prices[i])
T[i][2][1] = max(T[i - 1][2][1], T[i - 1][1][0] - prices[i])
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1),但是要注意更新的先后顺序,优先更新会用到其他尚未更新的变量的变量。


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

难度: 困难
题目表述:
最多买卖k次
代码(C++):

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k >= n / 2) return maxProfit(prices);
        vector<vector<vector<int>>> dp(n, vector<vector<int>>(k + 1, vector<int>(2)));
        for (int j = 0; j < k + 1; j++) {
            dp[0][j][0] = 0;
            dp[0][j][1] = -prices[0];
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < k + 1; j++) {
                dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return dp[n - 1][k][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
};
// 空间复杂度降到 O(k)
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k >= n / 2) return maxProfit(prices);
        vector<vector<int>> dp(k + 1, vector<int>(2));
        for (int j = 1; j < k + 1; j++) {
            dp[j][0] = 0;
            dp[j][1] = -prices[0];
        }
        for (int i = 1; i < n; i++) {
            for (int j = k; j > 0; j--) {
            	// 注意内层更新用到上一行结果,故反序遍历
                dp[j][0] = max(dp[j][0], dp[j][1] + prices[i]);
                dp[j][1] = max(dp[j][1], dp[j - 1][0] - prices[i]);
            }
        }
        return dp[k][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
        	// 注意更新均要用到上一行,因此需要暂存更新后的结果,待全部变量完成更新后再更新进维护变量
            int newdp0 = max(dp0, dp1 + prices[i]);
            int newdp1 = max(dp1, dp0 - prices[i]);
            dp0 = newdp0;
            dp1 = newdp1;
        }
        return dp0;
    }
};

题解:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
本题是最通用的情况,如果 k 超过一个临界值,最大收益就不再取决于允许的最大交易次数,而是取决于股票价格数组的长度,因此可以进行优化。一个有收益的交易至少需要两天(在前一天买入,在后一天卖出,前提是买入价格低于卖出价格)。如果股票价格数组的长度为 n,则有收益的交易的数量最多为 n / 2(整数除法)。因此 k 的临界值是 n / 2。如果给定的 k 不小于临界值,即 k >= n / 2,则可以将 k 扩展为正无穷,此时问题等价于题 6 。


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

难度: 中等
题目表述:
买卖多次,卖出有一天冷冻期
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); 
            dp[i][1] = max(dp[i - 1][1], (i >= 2 ? dp[i - 2][0] : 0) - prices[i]); 
        }
        return dp[n - 1][0];
    }
    // 空间复杂度降到O(1)
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int preProit0 = 0, profit0 = 0, profit1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int newprofit0 = max(profit0, profit1 + prices[i]); 
            profit1 = max(profit1, preProit0 - prices[i]); 
            preProit0 = profit0;
            profit0 = newprofit0;
        }
        return profit0;
    }
};

题解:
k 为正无穷但有冷冻期,意味着在 i 天买入的时候应该使用 i - 2 天的卖出([0])收益
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 2][k][0] - prices[i])


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

难度: 中等
题目表述:
买卖多次,每次有手续费
代码(C++):

class Solution {
public:
	// 买入时扣手续费
	int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0] - fee;
        for (int i = 1; i < n; i++) {
            int tmp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, dp0 - prices[i] - fee);
            dp0 = tmp0;
        }
        return dp0;
    }
    // 卖出时扣手续费
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int tmp0 = max(dp0, dp1 + prices[i] - fee);
            int dp1 = max(dp1, dp0 - prices[i]);
            dp0 = tmp0;
        }
        return dp0;
    }
};

题解:
k 为正无穷但有手续费
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i] - fee)


25. 最长递增子序列( LeetCode 300

难度: 中等
题目表述:
索引不连续的递增
代码(C++):

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int ans = 1, n = nums.size();
        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[i], dp[j] + 1);
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size(), ans = 0, len = 1;
        vector<int> d(n + 1, 0);
        d[len] = nums[0];
        for (int i = 1; i < n; i++) {
            if (nums[i] > d[len]) {
                len++;
                d[len] = nums[i];
            } else {
                int l = 1, r = len;
                while (l < r) {
                    int mid = (l + r) / 2;
                    if (d[mid] >= nums[i]) {
                        r = mid;
                    } else {
                        l = mid + 1;
                    }
                }
                d[l] = nums[i];
            }
        }
        return len;
    }
};

题解: 以nums[i]结尾
dp[i]表示以nums[i]结尾的最长递增子序列的长度


26. 最长连续递增序列( LeetCode 674

难度: 简单
题目表述:
索引连续的递增
代码(C++):

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        vector<int> dp(nums.size(), 1);
        int ans = 1;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) 
                dp[i] = dp[i - 1] + 1;
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i]表示以nums[i]结尾的连续递增的子序列长度为dp[i]


27. 最长重复子数组( LeetCode 718

难度: 中等
题目表述:
索引连续的公共
代码(C++):

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        int ans = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (nums1[i - 1] == nums2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<int> dp(n + 1, 0);
        int ans = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = n; j >= 1; j--) {
                if (nums1[i - 1] == nums2[j - 1])
                    dp[j] = dp[j - 1] + 1;
                else // 注意这里不相等的时候要有赋0的操作
                    dp[j] = 0;
                ans = max(ans, dp[j]);
            }
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组(连续子序列)长度为dp[i][j]。


28. 最长公共子序列( LeetCode1143

难度: 中等
题目表述:
索引不连续的公共
代码(C++):

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        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][j - 1], dp[i - 1][j]);
            }
        }
        return dp[m][n];
    }
};

题解: 以 i - 1结尾
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列长度为dp[i][j]
无需知道前一个的公共的位置,只用根据当前的字符判重就可以推导当前dp状态,故不需要像题25一样通过遍历来找到前一个递增尾部数字,来和当前进行比较,推导出当前是否需要+1。


29. 不相交的线( LeetCode1035

难度: 中等
题目表述:
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j],且绘制的直线不与任何其他连线(非水平线)相交。
索引不连续的公共
代码(C++):

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (nums1[i - 1] == nums2[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];
    }
};

题解: 以 i - 1结尾
本质是求最长公共子序列


30. 最大子数组和( LeetCode53

难度: 中等
题目表述:
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
代码(C++):

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int ans = dp[0];
        for (int i = 1; i < nums.size(); i++) {
            dp[i] = max(nums[i], dp[i - 1] + nums[i]);
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。


31. 判断子序列( LeetCode392

难度: 简单
题目表述:
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
代码(C++):

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int m = s.size(), n = t.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s[i - 1] == t[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = dp[i][j - 1];
            }
        }
        return dp[m][n] == m;
    }
};

题解: 以 i - 1结尾
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
本题 如果删元素一定是字符串t,而 题28 最长公共子序列 是两个字符串都可以删元素。


32. 不同的子序列( LeetCode115

难度: 困难
题目表述:
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
代码(C++):

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size(), n = t.size();
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
        for (int i = 0; i <= m ; i++) dp[i][0] = 1;
        for (int i = 1; i <= m ; i++) {
            for (int j = 1; j <= n; j++) {
                if (s[i - 1] == t[j - 1]) 
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];//s串用最后一位匹配 + 不用最后一位。
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[m][n];
    }
};

题解: 以 i - 1结尾
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。


33. 两个字符串的删除操作( LeetCode583

难度: 中等
题目表述:
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
代码(C++):

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size(), n = word2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i++) dp[i][0] = i;
        for (int j = 1; j <= n; j++) 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] = dp[i - 1][j - 1];
                else
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
            }
        }
        return dp[m][n];
    }
};

题解: 以 i - 1结尾
dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。


34. 编辑距离( LeetCode 72

难度: 困难
题目表述:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:插入、删除、替换
代码(C++):

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size(), n = word2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 0; i <= m; i++) dp[i][0] = i;
        for (int j = 0; j <= n; j++) 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] = dp[i - 1][j - 1];
                else
                    dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
            }
        }
        return dp[m][n];
    }
};

题解: 以 i - 1结尾
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。


35. 回文子串( LeetCode647

难度: 中等
题目表述:
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
代码(C++):

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        int cnt = 0;
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) {
                        dp[i][j] = true;
                        cnt++;
                    } else if (dp[i + 1][j - 1]) {
                        dp[i][j] = true;
                        cnt++;
                    }
                }
            }
        }
        return cnt;
    }
};

题解: 索引连续的回文
dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。


36. 最长回文子序列( LeetCode516

难度: 中等
题目表述:
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
代码(C++):

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(s.size(), vector<int>(n, 0));
        for (int i = n - 1; i >= 0; i--) {
        	dp[i][i] = 1;
            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]);
            }
        }
        return dp[0][n - 1];
    }
};

题解: 索引不连续的回文
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]


小结

解题步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
    组合类问题仅仅是求个数的话,就可以用dp,递推公式类似:dp[j] += dp[j - nums[i]]
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

背包问题

动态规划 是解决「0−1 背包问题」和「完全背包问题」的标准做法。
0-1背包: 如果限定每件物品最多只能选取 1 次 (即0 或 1 次),则问题称为 0-1背包问题。
完全背包: 如果每件物品最多可以选取无限次,则问题称为 完全背包问题。
多重背包: 是0-1背包的一种,第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。在01背包里面在加一个for循环遍历一个每种物品的数量。

记第 i 件物品的重量 (weight) 为 wi ,价值 (value) 为 vi​。dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值。
「0-1背包状态转移方程」:dp[i][j] = max/min{ dp[i−1][j], dp[i−1][j−wi​ ]+vi } , 0 ≤ wi ≤ j
「完全背包状态转移方程」:dp[i][j] = max/min{ dp[i−1][j], dp[i][j−wi ]+vi }, 0 ≤ wi <=j
差别(以红色标注)在于第二项中的状态转移是来自上一行还是本行。

如果 i 的最优解只和 i - 1 的最优解相关,可去掉 [i] 来降低空间复杂度,使二维降为一维,但需要注意:「完全背包问题」内层循环正序,而「0-1 背包问题」中内层循环反序。因为正序决定了计算dp[j] 的 dp[j−wi ]来自本行,而反序决定dp[j−wi ]来自上一行。


1. 0-1背包

纯 0 - 1 背包 是求 给定背包容量 装满背包 的最大价值是多少。
416. 分割等和子集 是求 给定背包容量,能不能装满这个背包。
1049. 最后一块石头的重量 II 是求 给定背包容量,尽可能装,最多能装多少
494. 目标和 是求 给定背包容量,装满背包有多少种方法。
474. 一和零 是求 给定背包容量,装满背包最多有多少个物品。
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。内层反序!!!


2. 完全背包

内层正序!!!
此外,还有一个关键点在于for循环嵌套的顺序:
组合数(无序):外层遍历物品,内层遍历背包。
排列数(有序):外层遍历背包,内层遍历物品。
求组合数:518.零钱兑换II
求排列数:377. 组合总和 Ⅳ、70. 爬楼梯进阶版(完全背包)、139.单词拆分
求最小数(顺序无所谓):322. 零钱兑换、279.完全平方数


股票问题

持有股票的数量是隐藏的关键因素,该因素影响第 i 天可以进行的操作,进而影响最大收益。因此对 T[i][k] 的定义需要分成两项:

  • T[i][k][0] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 0 份股票的情况下可以获得的最大收益;
  • T[i][k][1] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 1 份股票的情况下可以获得的最大收益。

基准情况:
T[-1][k][0] = 0, T[-1][k][1] = -prices[0]
T[i][0][0] = 0, T[i][0][1] = -prices[0]
状态转移方程:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
注:第一天对应 i = 0;因为每次交易包含两次成对的操作,买入和卖出,只有买入操作会改变允许的最大交易次数。
最终答案是 T[n - 1][k][0],因为结束时持有 0 份股票的收益一定大于持有 1 份股票的收益。

同背包问题类似,如果 i 的最优解只和 i - 1 的最优解相关,可去掉 [i] 来降低空间复杂度,但需要注意:内层循环反序 / 优先更新会用到其他尚未更新的变量的变量。 因为计算新变量要用到的变量均是来自上一行。


子序列问题

1. 索引不连续的( 递增 / 公共 )子序列: 最长递增子序列、最长公共子序列、不相交的线
2. 索引连续的( 递增 / 公共 )子序列: 最长连续递增序列、最长重复子序列、最大子数组和
dp定义:数组以nums[i]结尾(含nums[i]),字符串以 i - 1 结尾(不一定含s[i - 1])
若是 索引连续 / 递增的前后依赖关系,则dp需要以nums[i]结尾

编辑距离: 判断子序列、不同的子序列、两个字符串的删除操作、编辑距离
字符串以 i - 1 结尾(不一定含s[i - 1])

回文: 从左下角开始遍历
1. 索引连续的回文:
回文子串:[i,j]闭区间是否是回文 bool
2. 索引不连续的回文:
最长回文子序列: [i,j]范围内最长回文长度


参考链接

玩转 LeetCode 高频 100 题
https://leetcode.cn/problems/coin-change/solution/by-flix-su7s/
https://leetcode.cn/circle/article/qiAgHn/
LeetCode 刷题攻略

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值