动态规划
动态规划是什么?解决动态规划问题有什么技巧?如何学习动态规划?
首先,动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举。而且,你需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因。总结的一个思维框架,来辅助思考状态转移方程:
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp
数组/函数的含义。
按上面的套路走,最后的解法代码就会是如下的框架:
# 自顶向下递归的动态规划
def dp(状态1, 状态2, ...):
for 选择 in 所有可能的选择:
# 此时的状态已经因为做了选择而改变
result = 求最值(result, dp(状态1, 状态2, ...))
return result
# 自底向上迭代的动态规划
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
拆分
343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。返回你可以获得的最大乘积。
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
dp[i]
:拆分数字i
,可以得到的最大乘积为dp[i]
。
dp[i]
最大乘积是怎么得到的呢?其实可以从1遍历j
,然后有两种渠道得到dp[i]
:一个是j * (i - j)
直接相乘;一个是j * dp[i - j]
,相当于是拆分(i - j)
。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
。
也可以这么理解,j * (i - j)
是单纯的把整数拆分为两个数相乘,而j * dp[i - j]
是拆分成两个以及两个以上的个数相乘。
初始化:可以只初始化dp[2] = 1
,从dp[i]
的定义来说,拆分数字2,得到的最大乘积是1。
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; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
96. 不同的二叉搜索树
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例:
输入:n = 3
输出:5
dp[i]
:1到i
为节点组成的二叉搜索树的个数为dp[i]
,也可以理解是i
的不同元素节点组成的二叉搜索树的个数为dp[i]
,都是一样的。
dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
,j
相当于是头结点的元素,从1遍历到i
为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]
,j - 1
为j
为头结点的左子树节点数量,i - j
为以j
为头结点的右子树节点数量。
初始化:从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,所以初始化dp[0] = 1
。
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1);
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];
}
};
爬楼梯
509.波那契数
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
dp
数组的迭代(递推)解法:
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
vector<int> dq(n + 1);
dq[0] = 0; dq[1] = 1;
for(int i = 2; i <= n; i++){
dq[i] = dq[i - 1] + dq[i - 2];
}
return dq[n];
}
};
当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
int d0 = 0, d1 = 1;
for(int i = 2; i <= n; i++){
int sum = d0 + d1;
d0 = d1;
d1 = sum;
}
return d1;
}
};
70. 爬楼梯
假设你正在爬楼梯,需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶,你有多少种不同的方法可以爬到楼顶呢?
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
dp[i]
: 爬到第i
层楼梯,有dp[i]
种方法。
dp[i] 可以有两个方向推出来:
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶就是dp[i]。还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶就是dp[i]。那么dp[i]就是 dp[i - 1]与dp[i - 2]之和,所以dp[i] = dp[i - 1] + dp[i - 2]
。
不考虑dp[0]如果初始化,只初始化dp[1] = 1
,dp[2] = 2
,然后从i = 3开始递推,这样才符合dp[i]的定义。
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) { // 注意i是从3开始的
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
优化一下空间复杂度:
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) return n;
int d1 = 1, d2 = 2;
for(int i = 3; i <= n; i++){
int sum = d1 + d2;
d1 = d2;
d2 = sum;
}
return d2;
}
};
746. 使用最小花费爬楼梯
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。支付 15 ,向上爬两个台阶,到达楼梯顶部。总花费为 15 。
dp[i]
:到达第i
个台阶的最低花费为dp[i]
。(注意:这里认为第一步一定是要花费的)
两个途径得到dp[i]
,一个是dp[i-1]
一个是dp[i-2]
。一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
。
初始化:看一下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]
和dp[1]
就够了,其他的最终都是dp[0]和dp[1]推出的。dp[0] = cost[0]
,dp[1] = cost[1]
。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> d(n);
d[0] = cost[0];
d[1] = cost[1];
for(int i = 2; i < n; i++){
d[i] = min(d[i - 1], d[i - 2]) + cost[i];
}
// 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
return min(d[n - 1], d[n - 2]);
}
};
优化空间复杂度:
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp0 = cost[0];
int dp1 = cost[1];
for (int i = 2; i < cost.size(); i++) {
int dpi = min(dp0, dp1) + cost[i];
dp0 = dp1;
dp1 = dpi;
}
return min(dp0, dp1);
}
};
不同路径
62. 不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
输入:m = 3, n = 7
输出:28
dp[i][j]
:表示从(0, 0)
出发,到 (i, j)
有dp[i][j]
条不同的路径。
想要求dp[i][j]
,只能有两个方向来推导出来,即dp[i - 1][j]
和 dp[i][j - 1]
。dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
,因为dp[i][j]
只有这两个方向过来。
初始化:首先dp[i][0]
一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]
也同理。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
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];
}
};
63. 不同路径 II
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物,那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1
和 0
来表示。
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
dp[i][j]
:表示从(0, 0)
出发,到 (i, j)
有dp[i][j]
条不同的路径。
想要求dp[i][j]
,只能有两个方向来推导出来,即dp[i - 1][j]
和 dp[i][j - 1]
。dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
,因为dp[i][j]
只有这两个方向过来。但这里需要注意一点,因为有了障碍,(i, j)
如果就是障碍的话应该就保持初始状态(初始状态为0)。
初始化:如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]
应该还是初始值0。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) continue; // 跳过障碍物
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
64. 最小路径和
给定一个包含非负整数的m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。**说明:**每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
dp[i][j]
:表示从(0, 0)
出发,到 (i, j)
路径上的数字总和最小为dp[i][j]
。
想要求dp[i][j]
,只能有两个方向来推导出来,即dp[i - 1][j]
和 dp[i][j - 1]
,取最小值。递推公式:dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid(i)
。
初始化:根据定义有d[0][0] = grid[0][0]
,而d[i][0]
表示从原点一直往右走,数字总和逐渐增加grid[i][0]
,d[0][j]
同理。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> d(m, vector<int>(n, 0));
d[0][0] = grid[0][0];
for(int i = 1; i < m; i++){
d[i][0] = d[i - 1][0] + grid[i][0];
}
for(int j = 1; j < n; j++){
d[0][j] = d[0][j - 1] + grid[0][j];
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
d[i][j] = min(d[i - 1][j], d[i][j - 1]) + grid[i][j];
}
}
return d[m - 1][n - 1];
}
};
子数组/序列
300. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
数组dp[i]
表示以 nums[i]
这个数结尾(下标i之前,包括i)的最长递增子序列的长度,dp
数组中最大的那个值就是最长的递增子序列长度。
位置i
的最长升序子序列等于j
从0
到i-1
各个位置的最长升序子序列 + 1 的最大值,所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
。注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是要取dp[j] + 1的最大值。
根据这个定义可以推出 base case:dp[i]
初始值为 1,因为以 nums[i]
结尾的最长递增子序列起码要包含它自己。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> d(n, 1);
int res = 1;
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
d[i] = max(d[i], d[j] + 1);
}
}
res = max(res, d[i]);
}
return res;
}
};
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。
输入:nums = [1,3,5,4,7]
输出:3
本题相对于300.最长递增子序列最大的区别在于“连续”,本题要求的是最长连续递增序列。
数组dp[i]
表示以下标i
为结尾的数组的连续递增子序列长度为dp[i]
。注意这里的定义,一定是以下标i
为结尾,并不是说一定以下标0为起始位置。
如果 nums[i + 1] > nums[i]
,那么以 i + 1
为结尾的数组的连续递增的子序列长度一定等于以i
为结尾的数组的连续递增的子序列长度 + 1,即dp[i + 1] = dp[i] + 1
(和300.最长递增子序列的不同之处),只需要一层for循环。
初始化:以下标i
为结尾的数组的连续递增的子序列长度最少也应该是1,即nums[i]
这一个元素,所以dp[i]
应该初始化为1;
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
vector<int> d(n, 1);
int res = 1;
for(int i = 1; i < n; i++){
if(nums[i] > nums[i - 1]){
d[i] = d[i - 1] + 1;
res = max(res, d[i]);
}
}
return res;
}
};
53. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
这道题不能用滑动窗口算法,因为数组中的数字可以是负数。
定义 dp
数组:以 nums[i]
为结尾(下标i之前,包括i)的最大子数组和。
dp[i]
有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。既然要求「最大子数组和」,当然选择结果更大的那个。
dp[i]只有两个方向可以推出来:
dp[i - 1] + nums[i]
,即nums[i]
加入当前连续子序列和;nums[i]
,即从头开始计算当前连续子数组和;
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i])
。
初始化:从递推公式可以看出来dp[i]
是依赖于dp[i - 1]
的状态的,dp[0]
就是递推公式的基础。根据dp[i]
的定义,很明显dp[0]
应为nums[0]
,即dp[0] = nums[0]
。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int size = nums.size();
vector<int> dp(size);
dp[0] = nums[0];
int res = dp[0];
for(int i = 1; i < size; i++){
dp[i] = max(nums[i], nums[i] + dp[i - 1]);
res = max(res, dp[i]);
}
return res;
}
};
718. 最长重复子数组
给两个整数数组 nums1
和 nums2
,返回两个数组中公共的 、长度最长的子数组的长度。
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
dp[i][j]
代表以下标i - 1
为结尾的nums1
,和以下标j - 1
为结尾的nums2
,最长重复子数组长度为dp[i][j]
。
当nums1[i - 1]
和nums2[j - 1]
相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1
。
初始化:
根据dp[i][j]
的定义,dp[i][0]
和dp[0][j]
其实都是没有意义的!但dp[i][0]
和dp[0][j]
要有初始值,因为为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1
,所以dp[i][0]
和dp[0][j]
初始化为0。
举个例子:nums1[0]如果和nums2[0]相同的话,dp[1][1] = dp[0][0] + 1
,只有dp[0][0]
初始为0,正好符合递推公式逐步累加起来。
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
int res = 0;
vector<vector<int>> d(m + 1, vector<int>(n + 1));
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(nums1[i - 1] == nums2[j - 1]){
d[i][j] = d[i - 1][j - 1] + 1;
res = max(res, d[i][j]);
}
}
}
return res;
}
};
1143. 最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公子序列是 "ace" ,它的长度为 3 。
数组dp[i][j]
表示索引范围为[0, i - 1]
的字符串text1
与索引范围为[0, j - 1]
的字符串text2
的最长公共子序列为dp[i][j]
。
有两种情况:
如果text1[i - 1]
与 text2[j - 1]
相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1
;
如果text1[i - 1]
与 text2[j - 1]
不相同,那就看看text1[0, i - 2]
与text2[0, j - 1]
的最长公共子序列和 text1[0, i - 1]
与text2[0, j - 2]
的最长公共子序列,取最大的,即dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
。
初始化:test1[0, i-1]
和空串的最长公共子序列自然是0,所以dp[i][0] = 0
,同理dp[0][j]
也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
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));
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(text1[i - 1] == text2[j - 1]){
dp[i][j] = 1 + dp[i - 1][j - 1];
}else{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp[m][n];
}
};
392. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
输入:s = "abc", t = "ahbgdc"
输出:true
数组dp[i][j]
表示以下标i-1
为结尾的字符串s
,和以下标j-1
为结尾的字符串t
,相同子序列的长度为dp[i][j]
。
注意这里是判断s是否为t的子序列,即t的长度是大于等于s的。
两种情况:
if (s[i - 1] == t[j - 1])
,t中找到了一个字符在s中也出现了,dp[i][j] = dp[i - 1][j - 1] + 1
;if (s[i - 1] != t[j - 1])
,相当于t要删除元素,继续匹配,dp[i][j] = dp[i][j - 1]
;
初始化:其实这里只初始化dp[i][0]
就够了,但一起初始化也方便,所以就一起操作了。
如果dp[m][n]
与 字符串s
的长度相同,则说明s与t的最长相同子序列就是s,那么s 就是 t 的子序列,返回true,否则返回false。
class Solution {
public:
bool isSubsequence(string s, string t) {
int m = s.size(), n = t.size();
vector<vector<int>> d(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]){
d[i][j] = 1 + d[i - 1][j - 1];
}else{
d[i][j] = d[i][j - 1];
}
}
}
return d[m][n] == m;
}
};
583. 两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
删除的结果不就是它俩的最长公共子序列嘛!要计算删除的次数,就可以通过最长公共子序列的长度推导出来。只要求出两个字符串的最长公共子序列长度即可,除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
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++){
for(int j = 1; j <= n; j++){
if(word1[i - 1] == word2[j - 1]){
dp[i][j] = 1 + dp[i - 1][j - 1];
}else{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return m + n - 2 * dp[m][n];
}
};
712. 两个字符串的最小ASCII删除和
给定两个字符串s1
和 s2
,返回使两个字符串相等所需删除字符的 ASCII 值的最小和 。
输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
删除的 ascii 最小,那么保留的公共子串 ascii 最大,问题转化为:求最大ascii公共子串。
数组 dp[i][j]
表示 索引范围为[0, i - 1]
的字符串s1
与索引范围为[0, j - 1]
的字符串s2
的最大ascii公共子串的ascii值。
有两种情况:
如果s1[i - 1]
与 s2[j - 1]
相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + s1[i - 1]
;
如果s1[i - 1]
与 s2[j - 1]
不相同,那就看看s1[0, i - 2]
与s2[0, j - 1]
的最大ascii公共子串的ascii值和 s1[0, i - 1]
与s2[0, j - 2]
的最大ascii公共子串的ascii值,取最大的,即dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
。
初始化:当i
或j
为0时,没有公共子串,此时状态为0。
初始化:s1[0, i-1]
和空串的最大ascii公共子串的ascii值自然是0,所以dp[i][0] = 0
,同理dp[0][j]
也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int m = s1.size(), n = s2.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(s1[i - 1] == s2[j - 1]){
// i - 1位的ascii与[0 ~ i - 1]最小ascii之和
dp[i][j] = s1[i - 1] + dp[i - 1][j - 1];
}else{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
//计算s1和s2总的ascii
int ascii1 = 0, ascii2 = 0;
for(char c : s1) acsii1 += c;
for(char c : s2) acsii2 += c;
// 总的ascii减去最大ascii公共子串的ascii值即为所求
return ascii1 - dp[m][n] + ascii2 - dp[m][n];
}
};
115. 不同的子序列
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。
输入:s = "rabbbit", t = "rabbit"
输出:3
数组dp[i][j]
表示以i-1
为结尾的s
子序列中出现以j-1
为结尾的t
的个数为dp[i][j]
。
分析两种情况:
当s[i - 1]
与 t[j - 1]
相等时,dp[i][j]
由两部分组成:一部分是用s[i - 1]
来匹配,那么个数为dp[i - 1][j - 1]
,一部分是不用s[i - 1]
来匹配,个数为dp[i - 1][j]
。所以当s[i - 1]
与 t[j - 1]
相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
;
为什么还要考虑 不用s[i - 1]来匹配,都相同了指定要匹配啊。
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
当s[i - 1]
与 t[j - 1]
不相等时,dp[i][j]
只由一部分组成,不用s[i - 1]
来匹配,即dp[i - 1][j]
,所以递推公式为dp[i][j] = dp[i - 1][j]
。
初始化:
dp[i][0]
表示以i-1
为结尾的s
可以随便删除元素,出现空字符串的个数。那么dp[i][0]
一定都是1,因为把以i-1
为结尾的s,删除所有元素,出现空字符串的个数就是1。
dp[0][j]
表示空字符串s可以随便删除元素,出现以j-1
为结尾的字符串t的个数。那么dp[0][j]
一定都是0,s如论如何也变成不了t。
dp[0][0]
应该是1,空字符串s,可以删除0个元素,变成空字符串t。
class Solution {
public:
int numDistinct(string s, string t) {
int m = s.size(), n = t.size();
vector<vector<uint64_t>> d(m + 1, vector<uint64_t>(n + 1, 0));
// base case
for(int i = 0; i <= m; i++){
d[i][0] = 1;
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(s[i - 1] == t[j - 1]){
d[i][j] = d[i - 1][j - 1] + d[i - 1][j];
}else{
d[i][j] = d[i - 1][j];
}
}
}
return d[m][n];
}
};
72. 编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数。你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
数组dp[i][j]
表示以下标i-1
为结尾的字符串word1
,和以下标j-1
为结尾的字符串word2
,最近编辑距离为dp[i][j]
。
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:
if (word1[i - 1] == word2[j - 1])
不操作
if (word1[i - 1] != word2[j - 1])
增
删
换
if (word1[i - 1] == word2[j - 1])
那么说明不用任何编辑,dp[i][j]
就应该是 dp[i - 1][j - 1]
,即dp[i][j] = dp[i - 1][j - 1]
;
if (word1[i - 1] != word2[j - 1])
,此时就需要编辑了,如何编辑呢?
操作一:word1删除一个元素,那么就是以下标i - 2
为结尾的word1 与 j-1
为结尾的word2的最近编辑距离 再加上一个操作,即 dp[i][j] = dp[i - 1][j] + 1;
操作二:word2删除一个元素,那么就是以下标i - 1
为结尾的word1 与 j-2
为结尾的word2的最近编辑距离 再加上一个操作,即 dp[i][j] = dp[i][j - 1] + 1;
怎么都是删除元素,添加元素去哪了?word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a"
,word1
删除元素'd'
和 word2
添加一个元素'd'
,变成word1="a", word2="ad"
, 最终的操作数是一样!
操作三:替换元素,word1
替换word1[i - 1]
,使其与word2[j - 1]
相同,此时不用增加元素,那么以下标i-2
为结尾的word1
与 j-2
为结尾的word2
的最近编辑距离 加上一个替换元素的操作,即 dp[i][j] = dp[i - 1][j - 1] + 1;
综上,当 if (word1[i - 1] != word2[j - 1])
时取最小的,即dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
初始化:dp[i][0]
是i
,对word1里的元素全部做删除操作,即:dp[i][0] = i
,同理dp[0][j] = j
。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> d(m + 1, vector<int>(n + 1, 0));
// base case
// dp[i][0] = i,对word1里的元素全部做删除操作
for(int i = 0; i <= m; i++){
d[i][0] = i;
}
for(int j = 0; j <= n; j++){
d[0][j] = j;
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(word1[i - 1] == word2[j - 1]){
d[i][j] = d[i - 1][j - 1];
}else{
d[i][j] = 1 + min({d[i - 1][j - 1], d[i - 1][j], d[i][j - 1]});
}
}
}
return d[m][n];
}
};
回文序列
516. 最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
数组dp[i][j]
表示字符串s
在[i, j]
范围内的最长回文子序列的长度为dp[i][j]
。
如果s[i]
与s[j]
相同,那么dp[i][j] = dp[i + 1][j - 1] + 2
,即加上 s[i+1..j-1]
中的最长回文子序列就是 s[i..j]
的最长回文子序列。
如果s[i]
与s[j]
不相同,说明s[i]
和s[j]
同时加入并不能增加[i,j]
区间内回文子串的长度,那么dp[i + 1][j - 1]
分别加入s[i]
、s[j]
看看哪一个可以组成最长的回文子序列。加入s[j]
的回文子序列长度为dp[i + 1][j]
,加入s[i]
的回文子序列长度为dp[i][j - 1]
,那么dp[i][j]
一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
。
初始化:
首先要考虑当i
和j
相同的情况,从递推公式dp[i][j] = dp[i + 1][j - 1] + 2
可以看出递推公式是计算不到i
和j
相同时候的情况,所以需要手动初始化一下。
当i
与j
相同,那么dp[i][j]
一定是等于1的,即一个字符的回文子序列长度就是1。其他情况dp[i][j]
初始为0就行,这样递推公式dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
中dp[i][j]
才不会被初始值覆盖。
遍历顺序:
从递推公式dp[i][j] = dp[i + 1][j - 1] + 2
和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
可以看出,dp[i][j]
是依赖于dp[i + 1][j - 1]
和 dp[i + 1][j]
,也就是从矩阵的角度来说,dp[i][j]
下一行的数据。 所以遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的。
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
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] = 2 + dp[i + 1][j - 1];
}else{
// s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
// 整个 s 的最长回文子串长度
return dp[0][n - 1];
}
};
647. 回文子串
给你一个字符串 s
,请你统计并返回这个字符串中回文子串 的数目。
回文字符串是正着读和倒过来读一样的字符串,子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
布尔类型的dp[i][j]
表示区间范围[i,j]
(注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]
为true,否则为false。
当s[i]
与s[j]
不相等,dp[i][j]
一定是false;当s[i]
与s[j]
相等时,有如下三种情况:
- 情况一:下标
i
与j
相同,同一个字符例如a
,当然是回文子串 - 情况二:下标
i
与j
相差为1,例如aa
,也是回文子串 - 情况三:下标:
i
与j
相差大于1的时候,例如cabac
,此时s[i]
与s[j]
已经相同了,我们看i
到j
区间是不是回文子串就看aba
是不是回文就可以了。aba
的区间就是i+1
与j-1
区间,这个区间是不是回文就看dp[i + 1][j - 1]
是否为true。
初始化:dp[i][j]
初始化为false。
遍历顺序:首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]
是否为true,再对dp[i][j]
进行赋值true的。所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的。
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int res = 0, n = s.size();
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
// 三种情况全部包含
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
res++;
dp[i][j] = true;
}
}
}
return res;
}
};
1312. 让字符串成为回文串的最少插入次数
给你一个字符串 s
,每一次操作你都可以在字符串的任意位置插入任意字符,请你返回让 s
成为回文串的 最少操作次数 。「回文串」是正读和反读都相同的字符串。
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
二维数组dp[i][j]
表示对字符串s[i..j]
最少需要进行dp[i][j]
次插入才能变成回文串。想求s
的最少插入次数,也就是求dp[0][n - 1]
的大小(n
为s
的长度)。
当算出dp[i+1][j-1]
,即知道了s[i+1..j-1]
成为回文串的最小插入次数,那么也就可以认为s[i+1..j-1]
已经是一个回文串了,所以通过dp[i+1][j-1]
推导dp[i][j]
的关键就在于s[i]
和s[j]
这两个字符。
如果s[i] == s[j]
,不需要进行任何插入,只要知道如何把s[i+1..j-1]
变成回文串即可。
如果s[i] != s[j]
,有如下步骤:
步骤一,做选择,先将s[i..j-1]
或者s[i+1..j]
变成回文串。怎么做选择呢?谁变成回文串的插入次数少,就选谁呗。
比如图二的情况,将s[i+1..j]
变成回文串的操作次数更小,因为它本身就是回文串,根本不需要插入;同理,对于图三,将s[i..j-1]
变成回文串的操作次数更小。然而,如果 s[i+1..j]
和s[i..j-1]
都不是回文串,都至少需要插入一个字符才能变成回文,所以选择哪一个都一样。
步骤二,根据步骤一的选择,将s[i..j]
变成回文串。
如果在步骤一中选择把s[i+1..j]
变成回文串,那么在s[i+1..j]
右边插入一个字符s[i]
一定可以将s[i..j]
变成回文;同理,如果在步骤一中选择把s[i..j-1]
变成回文串,在s[i..j-1]
左边插入一个字符s[j]
一定可以将s[i..j]
变成回文。
初始化:当i == j
时dp[i][j] = 0
,因为当i == j
时s[i..j]
就是一个字符,本身就是回文串,所以不需要进行任何插入操作。
class Solution {
public:
int minInsertions(string s) {
int n = s.size();
// 定义:对 s[i..j],最少需要插入 dp[i][j] 次才能变成回文
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case:i == j 时 dp[i][j] = 0,单个字符本身就是回文
// 从下向上遍历
for (int i = n - 2; i >= 0; i--) {
// 从左向右遍历
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1];
} else {
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[0][n - 1];
}
};
01背包问题
01背包问题理论
有n
件物品和一个最多能背重量为w
的背包,第i
件物品的重量是weight[i]
,得到的价值是value[i]
,每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
解法一:二维dp数组01背包
数组dp[i][j]
表示从下标为[0-i]
的物品里任意取,放进容量为j
的背包,价值总和最大是多少。
那么可以有两个方向推出来dp[i][j]
:
不放物品i:由dp[i - 1][j]
推出,即背包容量为j
,里面不放物品i
的最大价值,此时dp[i][j]
就是dp[i - 1][j]
。(其实就是当物品i
的重量大于背包j
的重量时,物品i
无法放进背包中,所以被背包内的价值依然和前面相同)。
放物品i:由dp[i - 1][j - weight[i]]
推出,dp[i - 1][j - weight[i]]
为背包容量为j - weight[i]
的时候不放物品i
的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
(物品i
的价值),就是背包放物品i
得到的最大价值。
所以递归公式为dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
。
初始化:
首先从dp[i][j]
的定义出发,如果背包容量j
为0的话,即dp[i][0]
,无论是选取哪些物品,背包价值总和一定为0。
由状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
可以看出i
是由i-1
推导出来,那么i
为0的时候就一定要初始化。dp[0][j]
,即i
为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
当 j < weight[0]
的时候,dp[0][j]
应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]
时,dp[0][j]
应该是value[0]
,因为背包容量放足够放编号0物品。
dp[0][j]
和dp[i][0]
都已经初始化了,那么其他下标应该初始化多少呢?其实从递归公式可以看出dp[i][j]
是由左上方数值推导出来了,那么其他下标初始为什么数值都可以,因为都会被覆盖。但只不过一开始就统一把dp
数组统一初始为0,更方便一些。
遍历顺序:01背包中二维dp数组的两个for遍历的先后循序是可以颠倒的。
int bagProblem(vector<int> weight, vector<int> value, int bagweight){
int m = weight.size();
vector<vector<int>> dp(m, vector<int>(bagweight + 1, 0));
for(int j = weight[0]; j <= bagweight; j++){
dp[0][j] = value[0];
}
for(int i = 1; i < m; i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
return dp[m - 1][bagweight];
}
🦄解法二:一维dp数组(滚动数组)
数组dp[j]
表示容量为j
的背包,所背的物品价值可以最大为dp[j]。
dp[j]
可以通过dp[j - weight[i]]
推导出来,dp[j - weight[i]]
表示容量为j - weight[i]
的背包所背的最大价值。
dp[j - weight[i]] + value[i]
表示 容量为 j - 物品i重量
的背包 加上 物品i
的价值。(也就是容量为j的背包,放入物品i了之后的价值即dp[j]
)
此时dp[j]
有两个选择:一个是取自己dp[j]
,相当于二维dp数组中的dp[i-1][j]
,即不放物品i
;一个是取dp[j - weight[i]] + value[i]
,即放物品i
。取最大的,求最大价值。所以递归公式为dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
。
初始化:
dp[j]
表示:容量为j
的背包,所背的物品价值可以最大为dp[j]
,那么dp[0]
就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,dp数组在推导的时候一定是取价值最大的数。如果题目给的价值都是正整数,那么非0下标都初始化为0就可以了,这样才能让dp数组在递归公式的过程中取最大的价值,而不是被初始值覆盖。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
遍历顺序:
遍历背包的顺序要从大到小倒序遍历,**倒序遍历是为了保证物品i
只被放入一次!**但如果一旦正序遍历了,那么物品就会被重复加入多次!每一个元素一定是不可重复放入,所以从大到小遍历,可以保证每个物品仅被添加一次。从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即背包里只放入了一个物品。所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!
int bagProblem(vector<int> weight, vector<int> value, int bagweight){
vector<int> dp(bagweight + 1, 0);
for(int i = 0; i < weight.size(); i++){ // 遍历物品
for(int j = bagweight; j >= weight[i]; j--){ // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[bagweight];
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
找到集合里能够出现 sum / 2
的子集总和,就可以分割成两个相同元素和子集了。
**注意:一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包。**本题中我们要使用01背包,因为每个元素只能用一次。
只有确定了如下四点,才能把01背包问题套到本题上来:
- 背包的容量为 sum / 2
- 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
以上分析完,我们就可以套用01背包,来解决这个问题了。
01背包中,dp[j] 表示容量为j
的背包,所背的物品价值可以最大为dp[j]
。套到本题,数组dp[j]
表示背包总容量是j
,最大可以凑成j
的子集总和为dp[j]
。
01背包的递推公式为dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,本题相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i],所以递推公式为dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
。
初始化:从dp[j]的定义来看,首先dp[0]一定是0。只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。dp[i]
中的i
表示背包内总和,题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200,所以总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了。
遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = 0;
for(int num : nums){
sum += num;
}
// 和为奇数时,不可能划分成两个和相等的集合
if(sum % 2 == 1) return false;
int target = sum / 2;
vector<int> d(target + 1);
for(int i = 0; i < n; i++){ // 遍历物品
for(int j = target; j >= nums[i]; j--){ // 遍历背包
d[j] = max(d[j], d[j - nums[i]] + nums[i]);
}
}
return d[target] == target;
}
};
1049. 最后一块石头的重量 II
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了,和416. 分割等和子集非常像。本题物品的重量为stones[i]
,物品的价值也为stones[i]
,对应着01背包里的物品重量weight[i]
和 物品价值value[i]
。
数组dp[j]
表示容量为j
的背包,最多可以背dp[j]
这么重的石头。
01背包的递推公式为dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,本题则是dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
。
初始化:既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。
因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。
接下来就是如何初始化dp[j],因为重量都不会是负数,所以dp[j]
都初始化为0就可以了,这样在递归公式中dp[j]
才不会被初始值所覆盖。
遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
最后dp[target]
就是容量为target
的背包所能背的最大重量。那么分成两堆石头,一堆石头的总重量是dp[target]
,另一堆就是sum - dp[target]
。在计算target
的时候,target = sum / 2
因为是向下取整,所以sum - dp[target]
一定是大于等于dp[target]
的。所以相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]
。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int n = stones.size();
int sum = 0;
for(int stone : stones){
sum += stone;
}
int target = sum / 2;
vector<int> d(target + 1);
for(int i = 0; i < n; i++){
for(int j = target; j >= stones[i]; j--){
d[j] = max(d[j], d[j - stones[i]] + stones[i]);
}
}
return sum - d[target] * 2;
}
};
494. 目标和
给你一个整数数组 nums
和一个整数 target
。向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式。
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
如何转化为01背包问题呢?假设加法的总和为x,那么减法对应的总和就是sum - x。所以我们要求的是 x - (sum - x) = target,x = (target + sum) / 2,此时问题就转化为装满容量为x背包,有几种方法。
数组dp[j]
表示填满j
(包括j)这么大容积的包,有dp[j]
种方法。
不考虑nums[i]
的情况下,填满容量为j - nums[i]
的背包,有dp[j - nums[i]]
种方法。那么得到nums[i]
的话,凑成dp[j]
就有dp[j - nums[i]]
种方法,所以递推公式为dp[j] += dp[j - nums[i]]
。
初始化:
dp[0] = 1,装满容量为0的背包,有1种方法,就是装0件物品。
dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int num : nums) sum += num;
if(abs(target) > sum || (target + sum) % 2 != 0){
return 0;
}
int bag = (sum + target) / 2;
vector<int> d(bag + 1, 0);
d[0] = 1;
for(int i = 0; i < nums.size(); i++){ // 物品
for(int j = bag; j >= nums[i]; j--){ // 背包
d[j] += d[j - nums[i]];
}
}
return d[bag];
}
};
474.一和零
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集。
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
本题是01背包问题,不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。
数组dp[i][j]
代表最多有i
个0
和j
个1
的strs
的最大子集的大小为dp[i][j]
。
dp[i][j]
可以由前一个strs
里的字符串推导出来,strs里的字符串有zeroNum
个0,oneNum
个1。dp[i][j]
就是 dp[i - zeroNum][j - oneNum] + 1
。然后我们在遍历的过程中,取dp[i][j]
的最大值,所以递推公式为dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
。
此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
。对比一下就会发现,字符串的zeroNum
和oneNum
相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。
初始化:01背包的dp数组初始化为0就可以。因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]
不会被初始值覆盖。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> d(m + 1, vector<int>(n + 1));
for(string str : strs){
int zereNum = 0, oneNum = 0;
for(char ch : str){
if(ch == '0'){
zereNum++;
}else{
oneNum++;
}
}
for(int i = m; i >= zereNum; i--){
for(int j = n; j >= oneNum; j--){
d[i][j] = max(d[i][j], d[i - zereNum][j - oneNum] + 1);
}
}
}
return d[m][n];
}
};
完全背包问题
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是可以颠倒的!**因为dp[j]
是根据下标j
之前所对应的dp[j]
计算出来的,只要保证下标j
之前的dp[j]
都是经过计算的就可以了。
int bagProblem(vector<int> weight, vector<int> value, int bagweight){
vector<int> dp(bagweight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagweight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[bagweight];
}
遍历顺序:
若外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
假设:coins[0] = 1,coins[1] = 5。
那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。此时dp[j]里算出来的就是排列数!
总结:如果求组合数就是外层for循环遍历物品,内层for遍历背包,如果求排列数就是外层for遍历背包,内层for循环遍历物品。
518. 零钱兑换 II
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。
数组dp[j]
表示凑成总金额j
的货币组合数为dp[j]
。
dp[j]
(考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]]
(不考虑coins[i])相加,所以递推公式为dp[j] += dp[j - coins[i]]
。
初始化:首先dp[0]一定要为1,dp[0] = 1
是 递归公式的基础。从dp[i]
的含义上来讲就是,凑成总金额0的货币组合数为1。下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]
的时候才不会影响真正的dp[j]。
纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即有顺序也行,没有顺序也行!而本题要求凑成总和的组合数,元素之间要求没有顺序。
纯完全背包是能凑成总和就行,不用管怎么凑的。而本题是求凑出来的方案个数,且每个方案个数是组合数。
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1);
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];
}
};
322.零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
输入:coins = [2], amount = 3
输出:-1
题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。
数组dp
表示当目标金额为 i
时,至少需要 dp[i]
枚硬币凑出。
得到dp[j](考虑coins[i]),只有一个来源,dp[j - coins[i]](没有考虑coins[i])。凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i],即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])。所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的,递推公式为dp[j] = min(dp[j - coins[i]] + 1, dp[j])
。
初始化:
首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;
其他下标对应的数值呢?考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖,所以下标非0的元素都是应该是最大值。
遍历顺序:
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。所以本题并不强调集合是组合还是排列。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
// 如果dp[j - coins[i]]是初始值则跳过
if (dp[j - coins[i]] == INT_MAX) continue;
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列!
数组dp[i]
表示凑成目标正整数为i
的排列个数为dp[i]
。
dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来,因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分,递推公式是dp[i] += dp[i - nums[j]]
。
初始化:
因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
至于dp[0] = 1 有没有意义呢?其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的,仅仅是为了推导递推公式。
至于非0下标的dp[i]应该初始为多少呢?初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。
遍历顺序:
本题为排列问题,所以遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) { // 遍历背包
for (int j = 0; j < nums.size(); j++) { // 遍历物品
if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
279.完全平方数
给你一个整数 n
,返回和为 n
的完全平方数的最少数量 。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
完全平方数就是物品(可以无限件使用),凑个正整数n
就是背包,问凑满这个背包最少有多少物品?
dp[j]:和为j的完全平方数的最少数量为dp[j]。
dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j],此时我们要选择最小的dp[j],所以递推公式为dp[j] = min(dp[j - i * i] + 1, dp[j])
。
dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。非0下标的dp[j]应该是多少呢?从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j])中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。
本题是求最小值,遍历顺序无要求。
class Solution {
public:
int numSquares(int n) {
vector<int> d(n + 1, INT_MAX);
d[0] = 0;
for(int i = 1; i <= sqrt(n); i++){ // 遍历物品
for(int j = i * i; j <= n; j++){ // 遍历背包
d[j] = min(d[j], d[j - i * i] + 1);
}
}
return d[n] == INT_MAX ? 0 : d[n];
}
};
139.单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典,请你判断是否可以利用字典中出现的单词拼接出 s
。**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。拆分时可以重复使用字典中的单词,说明就是一个完全背包!
数组dp[i]:字符串索引为0 ~ i
的话,dp[i]为true表示可以拆分为一个或多个在字典中出现的单词。
如果确定dp[j] 是true,且 [j, i]
这个区间的子串出现在字典里,那么dp[i]一定是true (j < i),所以递推公式是if([j, i] 这个区间的子串出现在字典里 && dp[j]是true)
那么 dp[i] = true。
初始化:从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。
那么dp[0]有没有意义呢?dp[0]表示如果字符串为空的话,说明出现在字典里。但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
遍历顺序:本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意!那么本题使用求排列的方式,还是求组合的方式都可以。
但本题还有特殊性,因为是要求子串,最好是遍历背包放在外循环,将遍历物品放在内循环。如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的子串都预先放在一个容器里。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> set(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> d(n + 1, false);
d[0] = true;
for(int j = 1; j <= n; j++){
for(int i = 0; i < j; i++){
string str = s.substr(i, j - i);
if(d[i] && set.count(str)){
d[j] = true;
}
}
}
return d[n];
}
};
打家劫舍问题
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
dp[i]
:考虑下标i
(包括i
)以内的房屋,最多可以偷窃的金额为dp[i]
。
决定dp[i]
的因素就是第i
房间偷还是不偷:
如果偷第i
房间,那么dp[i] = dp[i - 2] + nums[i]
,即第i-1
房一定是不考虑的,找出 下标i-2
(包括i-2
)以内的房屋,最多可以偷窃的金额为dp[i-2]
加上第i
房间偷到的钱。
如果不偷第i
房间,那么dp[i] = dp[i - 1]
,即考虑i-1
房(注意这里是考虑,并不是一定要偷i-1
房),然后dp[i]
取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
。
初始化:从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
可以看出,递推公式的基础就是dp[0]
和 dp[1]
。从dp[i]
的定义上来讲,dp[0]
一定是 nums[0]
,dp[1]
就是nums[0]
和nums[1]
的最大值,即dp[1] = max(nums[0], nums[1])
。
遍历顺序:dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> d(n);
if(n == 1) return nums[0];
d[0] = nums[0];
d[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; i++){
d[i] = max(nums[i] + d[i - 2], d[i - 1]);
}
return d[n - 1];
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
这道题目和198.打家劫舍是差不多的,唯一区别就是成环了。
对于一个数组,成环的话主要有如下三种情况:
- 情况一:考虑不包含首尾元素
- 情况二:考虑包含首元素,不包含尾元素
- 情况三:考虑包含尾元素,不包含首元素
注意:这里用的是 ⌈考虑⌋,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
分析到这里,本题其实比较简单了, 剩下的和198.打家劫舍就是一样的了。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
int a = robrange(nums, 0, n - 2);
int b = robrange(nums, 1, n - 1);
return max(a, b);
}
int robrange(vector<int>& nums, int start, int end){
int n = nums.size();
if(start == end) return nums[start];
vector<int> d(n);
d[start] = nums[start];
d[start + 1] = max(nums[start], nums[start + 1]);
for(int i = start + 2; i <= end; i++){
d[i] = max(nums[i] + d[i - 2], d[i - 1]);
}
return d[end];
}
};
337. 打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额。
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
定义函数:返回一个节点偷与不偷两个状态所得到的金钱,返回值是一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱,本题dp数组就是一个长度为2的数组。
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val1 = max(left[0], left[1]) + max(right[0], right[1])
;如果是偷当前节点,那么左右孩子就不能偷,val2 = cur->val + left[0] + right[0]
;
最后当前节点的状态就是{val2, val1}
,即{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}。
初始化:在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回。
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
// 返回长度为2的数组,0:不偷,1:偷
vector<int> robTree(TreeNode* cur) {
if (cur == NULL) return {0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
int a = max(left[0], left[1]) + max(right[0], right[1]); // 不偷cur
int b = cur->val + left[0] + right[0]; // 偷cur
return {a, b};
}
};
股票买卖问题
121. 买卖股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润,返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
定义:dp[i][1]
表示第i
天持有股票所得最多现金 ,dp[i][0]
表示第i
天不持有股票所得最多现金。
如果第i
天持有股票即dp[i][1]
, 那么可以由两个状态推出来:
- 第
i-1
天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即dp[i - 1][1]
; - 第
i
天买入股票,所得现金就是买入今天的股票后所得现金,即-prices[i]
;
那么dp[i][1]
应该选所得现金最大的,所以dp[i][1] = max(dp[i - 1][1], -prices[i])
。
如果第i
天不持有股票即dp[i][0]
, 也可以由两个状态推出来:
- 第
i-1
天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金,即dp[i - 1][0]
; - 第
i
天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金,即prices[i] + dp[i - 1][1]
;
同样dp[i][1]
取最大值,dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1])
。
初始化:
由递推公式 dp[i][1] = max(dp[i - 1][1], -prices[i])
和 dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1])
可以看出其基础都是要从dp[0][0]
和dp[0][1]
推导出来。
dp[0][1]
表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能由前一天推出来,所以dp[0][1] = - prices[0]
。dp[0][0]
表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][0] = 0
。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(2));
d[0][1] = - prices[0];
for(int i = 1; i < n; i++){
d[i][0] = max(d[i - 1][0], d[i - 1][1] + prices[i]);
d[i][1] = max(d[i - 1][1], - prices[i]);
}
return d[n - 1][0];
}
};
122. 买卖股票的最佳时机 II
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
本题和121. 买卖股票的最佳时机的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)。
定义:dp[i][1]
表示第i
天持有股票所得最多现金 ,dp[i][0]
表示第i
天不持有股票所得最多现金。
如果第i
天持有股票即dp[i][1]
, 那么可以由两个状态推出来:
- 第
i-1
天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即dp[i - 1][1]
; - 第
i
天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即dp[i - 1][0] - prices[i]
;
注意这里和121. 买卖股票的最佳时机唯一不同的地方,就是推导dp[i][1]
的时候,第i
天买入股票的情况。在121. 买卖股票的最佳时机中,因为股票全程只能买卖一次,所以如果买入股票,那么第i
天持有股票即dp[i][1]
一定就是 -prices[i]
。
而本题,因为一只股票可以买卖多次,所以当第i
天买入股票的时候,所持有的现金可能有之前买卖过的利润。那么第i
天持有股票即dp[i][1]
,如果是第i
天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即dp[i - 1][0] - prices[i]
。
如果第i
天不持有股票即dp[i][0]
的情况, 依然可以由两个状态推出来:
- 第
i-1
天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金,即dp[i - 1][0]
; - 第
i
天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金,即prices[i] + dp[i - 1][1]
;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(2));
d[0][1] = - prices[0];
for(int i = 1; i < n; i++){
d[i][0] = max(d[i - 1][0], d[i - 1][1] + prices[i]);
d[i][1] = max(d[i - 1][1], d[i - 1][0] - prices[i]);
}
return d[n - 1][0];
}
};
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices
,其中 prices[i]
表示第 i
天的股票价格 ;整数 fee
代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。**注意:**这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
本题和动态规划122.买卖股票的最佳时机II的区别就是这里需要多一个减去手续费的操作。
dp[i][1]
表示第i
天持有股票所省最多现金,dp[i][0]
表示第i
天不持有股票所得最多现金。
如果第i
天持有股票即dp[i][1]
, 那么可以由两个状态推出来:
- 第
i-1
天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即dp[i - 1][1]
- 第
i
天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即dp[i - 1][0] - prices[i]
所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
。
如果第i
天不持有股票即dp[i][0]
的情况, 依然可以由两个状态推出来:
- 第
i-1
天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金,即dp[i - 1][0]
- 第
i
天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了,即dp[i - 1][1] + prices[i] - fee
所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)
。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(2));
d[0][1] = - prices[0];
for(int i = 1; i < n; i++){
d[i][0] = max(d[i - 1][0], d[i - 1][1] + prices[i] - fee);
d[i][1] = max(d[i - 1][1], d[i - 1][0] - prices[i]);
}
return max(d[n - 1][0], d[n - 1][1]);
}
};
123. 买卖股票的最佳时机 III
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
本题中至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。
一天一共就有五个状态:0. 没有操作,1. 第一次买入,2. 第一次卖出,3. 第二次买入,4. 第二次卖出。
dp[i][j]
中 i
表示第i
天,j
为 [0 - 4]
五个状态,dp[i][j]
表示第i
天状态j
所剩最大现金。
注意:表示的是第i
天,买入股票的状态,并不是说一定要第i
天买入股票。
达到dp[i][1]
状态,有两个具体操作:
- 操作一:第
i
天买入股票,那么dp[i][1] = dp[i-1][0] - prices[i]
- 操作二:第
i
天没有操作,而是沿用前一天买入的状态,即dp[i][1] = dp[i - 1][1]
取最大值,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1])
。
同理dp[i][2]
也有两个操作:
- 操作一:第
i
天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
- 操作二:第
i
天没有操作,沿用前一天卖出股票的状态,即dp[i][2] = dp[i - 1][2]
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
。
同理可推出剩下状态部分:dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
,dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])
。
初始化:
第0天没有操作,dp[0][0] = 0
。
第0天做第一次买入的操作,dp[0][1] = -prices[0]
。
第0天做第一次卖出的操作,这个初始值应该是多少呢?首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,从递推公式中可以看出每次是取最大值,那么既然是收获利润,如果比0还小了就没有必要收获这个利润了,所以dp[0][2] = 0
。
第0天第二次买入操作,初始值应该是多少呢?第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。所以第二次买入操作,初始化为dp[0][3] = -prices[0]
。
同理,第0天第二次卖出初始化dp[0][4] = 0
。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(5));
d[0][1] = - prices[0];
d[0][3] = - prices[0];
for(int i = 1; i < n; i++){
d[i][1] = max(d[i - 1][1], d[i - 1][0] - prices[i]);
d[i][2] = max(d[i - 1][2], d[i - 1][1] + prices[i]);
d[i][3] = max(d[i - 1][3], d[i - 1][2] - prices[i]);
d[i][4] = max(d[i - 1][4], d[i - 1][3] + prices[i]);
}
return d[n - 1][4];
}
};
188.买卖股票的最佳时机IV
给定一个整数数组 prices
,它的第 i
个元素 prices[i]
是一支给定的股票在第 i
天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
本题要求至多有k
次交易。
二维数组 dp[i][j]
:第i
天的状态为j
,所剩下的最大现金是dp[i][j]
。
j
的状态表示为:0 表示不操作,1 第一次买入,2 第一次卖出,3 第二次买入,4 第二次卖出 …
发现规律:除了0以外,奇数就是买入,偶数就是卖出。题目要求至多有k笔交易,那么j
的范围就定义为 2 * k + 1
就可以了。
达到dp[i][1]
状态,有两个具体操作:
- 操作一:第
i
天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
- 操作二:第
i
天没有操作,而是沿用前一天买入的状态,即dp[i][1] = dp[i - 1][1]
选最大的,所以 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
。
同理dp[i][2]
也有两个操作:
- 操作一:第
i
天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
- 操作二:第
i
天没有操作,沿用前一天卖出股票的状态,即dp[i][2] = dp[i - 1][2]
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
。
同理可以类比剩下的状态。
初始化:
第0天没有操作,dp[0][0] = 0
。
第0天做第一次买入的操作,dp[0][1] = -prices[0]
。
第0天做第一次卖出的操作,这个初始值应该是多少呢?首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0。从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了,所以dp[0][2] = 0
。
第0天第二次买入操作,初始值应该是多少呢?不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。第二次买入操作,初始化为dp[0][3] = -prices[0]
。
可以推出dp[0][j]
当j
为奇数的时候都初始化为 -prices[0]
,当j
为偶数的时候都初始化为0。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(2 * k + 1));
for(int i = 1; i < 2 * k; i += 2){
d[0][i] = - prices[0];
}
for(int i = 1; i < n; i++){
for(int j = 1; j < 2 * k; j += 2){
d[i][j] = max(d[i - 1][j], d[i - 1][j - 1] - prices[i]);
d[i][j + 1] = max(d[i - 1][j + 1], d[i - 1][j] + prices[i]);
}
}
return d[n - 1][2 * k];
}
};
309. 最佳买卖股票时机含冷冻期
给定一个整数数组prices
,其中第 prices[i]
表示第 i
天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
dp[i][j]
:第i
天状态为j
,所剩的最多现金为dp[i][j]
。
可以区分出如下四个状态,(将买出股票状态细分为三个状态:状态0,2,3):
- 状态0:今天卖出股票
- 状态1:今天为买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
- 状态2:今天为冷冻期状态,持续一天
- 状态3:今天已经度过冷冻期,一直没操作,今天保持卖出股票状态
j
的状态为:0:状态0,1:状态1,2:状态2,3:状态3。
有 无
/|\
/ | \
卖 冻 过
1 0 2 3
达到今天就卖出股票状态(状态0),即dp[i][0]
,只有一种可能:昨天一定是买入股票状态(状态1),今天卖出,dp[i][0] = dp[i - 1][1] + prices[i]
达到买入股票状态(状态1)即dp[i][1]
,有如下情况:
- 前一天就是持有股票状态(状态0),
dp[i][1] = dp[i - 1][1]
- 前一天是冷冻期(状态2),
dp[i - 1][2] - prices[i]
- 前一天已经度过冷冻期(状态3),
dp[i - 1][3] - prices[i]
取最大值,那么dp[i][1] = max(dp[i - 1][1], max(dp[i - 1][2], dp[i - 1][3]) - prices[i])
。
达到冷冻期状态(状态2),即dp[i][2]
,只有一种情况:昨天卖出了股票(状态0),dp[i][2] = dp[i - 1][0]
。
已经度过冷冻期(状态3)即dp[i][3]
,有两个情况:
- 前一天就是冷冻期(状态2)
- 前一天依然度过了冷冻期(状态3)
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2])
初始化:
今天卖出了股票(状态0),dp[0][0]
初始化为0,因为最少收益就是0,绝不会是负数。
如果是持有股票状态(状态1)那么dp[0][1] = -prices[0]
,买入股票所剩现金为负数。
处在冷冻期(状态2),dp[0][2]
初始化为0。
保持卖出股票已经度过了冷冻期(状态3),dp[0][3]
初始化为0。
最后结果是取状态0,状态2,和状态3的最大值。别把状态2忘了,状态2是冷冻期,最后一天如果是冷冻期也可能是最大值。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> d(n, vector<int>(4));
d[0][1] = - prices[0];
for(int i = 1; i < n; i++){
d[i][0] = d[i - 1][1] + prices[i];
d[i][1] = max(d[i - 1][1], max(d[i - 1][2], d[i - 1][3]) - prices[i]);
d[i][2] = d[i - 1][0];
d[i][3] = max(d[i - 1][2], d[i - 1][3]);
}
return max(d[n - 1][0], max(d[n - 1][2], d[n - 1][3]));
}
};
其他
91. 解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
'A' -> "1"
'B' -> "2"
...
'Z' -> "26"
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RozCcWf1-1672384970117)(null)]
class Solution{
public:
int numDecodings(string s){
if (s[0] == '0') return 0;
int n = s.size();
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++){
if (s[i - 1] == '0'){
if (s[i - 2] == '1' || s[i - 2] == '2'){
dp[i] = dp[i - 2];
}else{
return 0;
}
}else if (s[i - 2] == '1' || (s[i - 2] == '2' && s[i - 1] <= '6')){
dp[i] = dp[i - 1] + dp[i - 2];
}else{
dp[i] = dp[i - 1];
}
}
return dp[n];
}
};
优化空间复杂度:
class Solution {
public:
int numDecodings(string s) {
if (s[0] == '0') return 0;
int pre = 1, cur = 1; //dp[-1] = dp[0] = 1
for (int i = 1; i < s.size(); i++) {
int tmp = cur;
if (s[i] == '0'){
if (s[i - 1] == '1' || s[i - 1] == '2'){
cur = pre;
}else{
return 0;
}
}else if (s[i - 1] == '1' || (s[i - 1] == '2' && s[i] >= '1' && s[i] <= '6')){
cur = cur + pre;
}
pre = tmp;
}
return cur;
}
};
152. 乘积最大子数组
给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。测试用例的答案是一个32位 整数。子数组是数组的连续子序列。
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
注意会出现负负得正的情况,这和加法运算是不一样的。
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
int res = nums[0];
int maxVal = nums[0];
int minVal = nums[0];
for(int i = 1; i < n; i++){
int tmp = maxVal;
maxVal = max(nums[i], max(nums[i] * maxVal, nums[i] * minVal));
minVal = min(nums[i], min(nums[i] * tmp, nums[i] * minVal));
res = max(res, maxVal);
}
return res;
}
};
343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。返回你可以获得的最大乘积。
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
dp[i]
:拆分数字i
,可以得到的最大乘积为dp[i]
。
dp[i]
最大乘积是怎么得到的呢?其实可以从1遍历j
,然后有两种渠道得到dp[i]
:一个是j * (i - j)
直接相乘;一个是j * dp[i - j]
,相当于是拆分(i - j)
。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
。
也可以这么理解,j * (i - j)
是单纯的把整数拆分为两个数相乘,而j * dp[i - j]
是拆分成两个以及两个以上的个数相乘。
初始化:可以只初始化dp[2] = 1
,从dp[i]
的定义来说,拆分数字2,得到的最大乘积是1。
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; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
53. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
这道题不能用滑动窗口算法,因为数组中的数字可以是负数。
定义 dp
数组:以 nums[i]
为结尾(下标i之前,包括i)的最大子数组和。
dp[i]
有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。既然要求「最大子数组和」,当然选择结果更大的那个。
dp[i]只有两个方向可以推出来:
dp[i - 1] + nums[i]
,即nums[i]
加入当前连续子序列和;nums[i]
,即从头开始计算当前连续子数组和;
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i])
。
初始化:从递推公式可以看出来dp[i]
是依赖于dp[i - 1]
的状态的,dp[0]
就是递推公式的基础。根据dp[i]
的定义,很明显dp[0]
应为nums[0]
,即dp[0] = nums[0]
。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int size = nums.size();
vector<int> dp(size);
dp[0] = nums[0];
int res = dp[0];
for(int i = 1; i < size; i++){
dp[i] = max(nums[i], nums[i] + dp[i - 1]);
res = max(res, dp[i]);
}
return res;
}
};
221. 最大正方形
在一个由 '0'
和 '1'
组成的二维矩阵内,找到只包含 '1'
的最大正方形,并返回其面积。
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
动态规划:
当 matrix[i][j]
为 1,且它的左边、上边、左上角都存在正方形时,matrix[i][j]
才能够作为一个更大的正方形的右下角。
所以我们可以定义这样一个二维 dp
数组:以 matrix[i - 1][j - 1]
为右下角元素的最大的全为 1 正方形矩阵的边长为 dp[i][j]
。有了这个定义,状态转移方程就是:
if (matrix[i][j] == 1)
// 类似「水桶效应」,最大边长取决于边长最短的那个正方形
dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1], dp[i][j - 1]) + 1;
else
dp[i][j] = 0;
相当于是这三处都能组成的正方形行的最小值,这样这三个小的正方形叠加到一起,就是一个缺少了右下角的正方形,故加1就为当前位置的最大的正方形。题目最终想要的答案就是最大边长 max(dp[..][..])
的平方。
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> d(m + 1, vector<int>(n + 1));
int len = 0;
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(matrix[i - 1][j - 1] == '1'){
d[i][j] = 1 + min(d[i - 1][j - 1], min(d[i - 1][j], d[i][j - 1]));
len = max(len, d[i][j]);
}
}
}
return len * len;
}
};