代码随想录1刷—动态规划篇(三)
- [198. 打家劫舍](https://leetcode.cn/problems/house-robber/)
- [213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/)
- [337. 打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/)
- [121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/)
- [122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/)
- [123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/)
- [188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/)
- [309. 最佳买卖股票时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/)
- [714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
- [300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/)
- [674. 最长连续递增序列](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/)
- [718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/)
- [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/)
- [1035. 不相交的线](https://leetcode.cn/problems/uncrossed-lines/)
- [53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)
198. 打家劫舍
1、dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
2、决定 d p [ i ] dp[i] dp[i] 的因素就是第 i i i 房间偷还是不偷。
- 如果偷第
i
i
i 房间,那么
dp[i] = dp[i - 2] + nums[i]
,即:第 i − 1 i-1 i−1 房一定是不考虑的,找出 下标 i − 2 i-2 i−2 (包括 i − 2 i-2 i−2 )以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱
。 - 如果不偷第
i
i
i 房间,那么
dp[i] = dp[i - 1]
,即考虑 i-1 房
(注意是考虑,不是一定偷 i − 1 i-1 i−1 房)
然后
d
p
[
i
]
dp[i]
dp[i] 取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
3、递推公式的基础就是
d
p
[
0
]
dp[0]
dp[0] 和
d
p
[
1
]
dp[1]
dp[1] ,dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);
4、
d
p
[
i
]
dp[i]
dp[i] 是根据
d
p
[
i
−
2
]
dp[i-2]
dp[i−2] 和
d
p
[
i
−
1
]
dp[i-1]
dp[i−1] 推导出来的,那么一定是从前到后遍历
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.back();
}
};
213. 打家劫舍 II
和 198.打家劫舍 的唯一区别就是 房屋成环了。对于一个数组,成环的话主要有如下三种情况:
情况二 和 情况三 包含情况一,所以只考虑情况二和情况三就可以。
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
int result1 = robRange(nums, 0, nums.size() - 2); //情况2
int result2 = robRange(nums, 1, nums.size() - 1); //情况3
return max(result1, result2);
}
int robRange(vector<int>& nums, int start, int end){
if(end == start) return nums[start];
vector<int> dp(nums.size(), 0);
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
for(int i = start + 2; i <= end; i++){
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); //思路和 198.打家劫舍 一样
}
return dp[end];
}
};
337. 打家劫舍 III
如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就考虑抢左右孩子(注意是“考虑”不是必抢)。
后序遍历(左右中),因为需要通过递归函数的返回值来做下一步计算。
暴力递归(超时)
class Solution {
public:
int rob(TreeNode* root) {
if(root == nullptr) return 0;
if(root->left == nullptr && root->right == nullptr) return root->val;
//偷父节点
int val1 = root->val;
if(root->left) val1 += rob(root->left->left) + rob(root->left->right);
if(root->right) val1 += rob(root->right->left) + rob(root->right->right);
//不偷父节点
int val2 = rob(root->left) + rob(root->right);
return max(val1, val2);
}
};
这个递归的过程中其实是有重复计算的。计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。
记忆化递归
可以使用map把计算过的结果保存,如果计算过孙子了,那算孩子的时候可以复用孙子节点的结果。
class Solution {
public:
unordered_map<TreeNode* ,int> umap;
int rob(TreeNode* root) {
if(root == nullptr) return 0;
if(root->left == nullptr && root->right == nullptr) return root->val;
if(umap[root]) return umap[root];
//偷父节点
int val1 = root->val;
if(root->left) val1 += rob(root->left->left) + rob(root->left->right);
if(root->right) val1 += rob(root->right->left) + rob(root->right->right);
//不偷父节点
int val2 = rob(root->left) + rob(root->right);
umap[root] = max(val1, val2);
return max(val1, val2);
}
};
动态规划
前两种方法其实对一个节点 偷与不偷得到的最大金钱 都没有做记录,而是进行实时计算。
而动态规划其实就是使用状态转移容器来记录状态的变化,因此这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
树形dp,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
1、确定递归函数的参数和返回值
要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组,参数为当前节点。
vector<int> robTree(TreeNode* cur) {
其实这里的返回数组就是dp数组。dp数组下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
长度为2的数组怎么标记树中每个节点的状态呢?在递归的过程中,系统栈会保存每一层递归的参数。
2、确定终止条件:在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
if (cur == NULL) return vector<int>{0, 0};
这也相当于dp数组的初始化。
3、确定遍历顺序:后序遍历。 因为通过递归函数的返回值来做下一步计算。
4、确定单层递归的逻辑:
-
如果是偷当前节点,那么左右孩子就不能偷,
val1 = cur->val + left[0] + right[0];
-
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:
val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1};
即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
vector<int> robTree(TreeNode* cur){
if(cur == nullptr) return vector<int> {0,0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
int val1 = cur->val + left[0] + right[0]; //偷cur
int val2 = max(left[0],left[1]) + max(right[0],right[1]); //不偷cur
return {val2, val1};
}
};
121. 买卖股票的最佳时机
暴力解法(超时)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for (int i = 0; i < prices.size(); i++) {
for (int j = i + 1; j < prices.size(); j++){
result = max(result, prices[j] - prices[i]);
}
}
return result;
}
};//找最优间距
贪心解法
股票就买卖一次,那么取最左最小值,取最右最大值,那么得到的差值就是最大利润。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = INT_MAX;
int result = 0;
for (int i = 0; i < prices.size(); i++) {
low = min(low, prices[i]); // 取最左最小价格
result = max(result, prices[i] - low); // 直接取最大区间利润
}
return result;
}
};
动态规划
1、 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示第 i i i 天持有股票所得最多现金 ; d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第 i i i 天不持有股票所得最多现金。
2、如果第 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来:
- 第
i
−
1
i-1
i−1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:
dp[i - 1][0]
- 第
i
i
i 天买入股票,所得现金就是买入今天的股票后所得现金即:
-prices[i]
那么
d
p
[
i
]
[
0
]
dp[i][0]
dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1], 也可以由两个状态推出来:
- 第
i
−
1
i-1
i−1 天就不持有股票,那就保持现状,所得现金就是昨天不持有股票的所得现金 即:
dp[i - 1][1]
- 第
i
i
i 天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:
prices[i] + dp[i - 1][0]
同样
d
p
[
i
]
[
1
]
dp[i][1]
dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
本题中不持有股票状态所得金钱一定比持有股票状态得到的多!最后直接取 d p [ i ] [ 1 ] dp[i][1] dp[i][1]就可以啦。
3、由递推公式可以看出其基础都是要从
d
p
[
0
]
[
0
]
dp[0][0]
dp[0][0] 和
d
p
[
0
]
[
1
]
dp[0][1]
dp[0][1] 推导出来的。那么
d
p
[
0
]
[
0
]
dp[0][0]
dp[0][0]表示第
0
0
0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] = -prices[0]
; 而
d
p
[
0
]
[
1
]
dp[0][1]
dp[0][1]表示第
0
0
0 天不持有股票,不持有股票那么现金就是
0
0
0,所以dp[0][1] = 0;
4、从递推公式可以看出
d
p
[
i
]
dp[i]
dp[i] 都是有
d
p
[
i
−
1
]
dp[i - 1]
dp[i−1] 推导出来的,那么一定是从前向后遍历
。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if(len == 0) return 0;
vector<vector<int>> dp(len,vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < len; i++){
dp[i][0] = max(dp[i-1][0],-prices[i]);
dp[i][1] = max(dp[i-1][1],prices[i]+dp[i-1][0]);
}
return dp[len-1][1];
}
};
优化
从递推公式可以看出, d p [ i ] dp[i] dp[i]只是依赖于 d p [ i − 1 ] dp[i - 1] dp[i−1]的状态。那只需要记录当前天的 d p dp dp 状态和前一天的 d p dp dp 状态就可以了,可以使用滚动数组来节省空间。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if(len == 0) return 0;
vector<vector<int>> dp( 2,vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < len; i++){
dp[i%2][0] = max(dp[(i-1)%2][0],-prices[i]);
dp[i%2][1] = max(dp[(i-1)%2][1],prices[i]+dp[(i-1)%2][0]);
}
return dp[(len-1)%2][1];
}
};
122. 买卖股票的最佳时机 II
本题和 121. 买卖股票的最佳时机 的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票),这个区别主要是体现在递推公式上,其他和121. 买卖股票的最佳时机一样。
dp[i][0] 表示第i天持有股票所得现金。dp[i][1] 表示第i天不持有股票所得最多现金。
如果第i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 dp[i - 1 dp[i−1][0]
- 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i−1][1]−prices[i]
在 121 中股票全程只能买卖一次,如果买入股票,那 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0]一定是 − p r i c e s [ i ] -prices[i] −prices[i]。而本题因为一只股票可买卖多次,所以第 i i i 天买入股票时,所持有的现金可能有之前买卖过的利润。那第 i i i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0],如果第 i i i天买入,所得现金就是昨天不持有股票的所得现金减去今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i−1][1]−prices[i]。
如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1],依然可以由两个状态推出来:
- 第
i
−
1
i-1
i−1 天就不持有股票,那就保持现状,所得现金就是昨天不持有股票的所得现金 即:
dp[i - 1][1]
- 第
i
i
i 天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:
prices[i] + dp[i - 1][0]
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
优化
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
}
return dp[(len - 1) % 2][1];
}
};
123. 买卖股票的最佳时机 III
至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。
1、确定dp数组以及下标的含义,一天一共有五个状态,如下:
- 没有操作
- 第一次买入
- 第一次卖出
- 第二次买入
- 第二次卖出
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
需要明确:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票
2、递推公式:
达到 d p [ i ] [ 1 ] dp[i][1] dp[i][1]状态,有两个具体操作:
- 操作一:第 i i i天买入股票了,那么 d p [ i ] [ 1 ] = d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] dp[i][1] = dp[i-1][0] - prices[i] dp[i][1]=dp[i−1][0]−prices[i]
- 操作二:第 i i i天没有操作,而是沿用前一天买入的状态,即: d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] dp[i][1] = dp[i - 1][1] dp[i][1]=dp[i−1][1]
所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
同理 d p [ i ] [ 2 ] dp[i][2] dp[i][2]也有两个操作:
- 操作一:第 i i i天卖出股票了,那么 d p [ i ] [ 2 ] = d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] dp[i][2] = dp[i - 1][1] + prices[i] dp[i][2]=dp[i−1][1]+prices[i]
- 操作二:第 i i i天没有操作,沿用前一天卖出股票的状态,即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 2 ] dp[i][2] = dp[i - 1][2] 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]);
3、初始化:
没有操作:dp[0][0] = 0;
第一次买入:dp[0][1] = -prices[0];
第一次卖出:卖出操作一定收获利润,但最差情况也就是没有盈利即全程无操作现金为0,递推公式中可以看出每次是取利润最大值,那么收获利润如果比0还小了就没有必要收获这个利润了。所以dp[0][2] = 0;
第二次买入:第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
第二次卖出:同理第二次卖出初始化dp[0][4] = 0;
4、一定是从前向后遍历
,因为
d
p
[
i
]
dp[i]
dp[i],依靠
d
p
[
i
−
1
]
dp[i - 1]
dp[i−1]的数值。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for(int i = 1; i < prices.size(); i++){
dp[i][0] = dp[i-1][0];
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]);
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]);
}
return dp[prices.size()-1][4];
}
};
优化
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0) return 0;
vector<int> dp(5, 0);
dp[1] = -prices[0];
dp[3] = -prices[0];
for(int i = 1; i < prices.size(); i++){
dp[1] = max(dp[1], dp[0] - prices[i]);
dp[2] = max(dp[2], dp[1] + prices[i]);
dp[3] = max(dp[3], dp[2] - prices[i]);
dp[4] = max(dp[4], dp[3] + prices[i]);
}
return dp[4];
}
};
188. 买卖股票的最佳时机 IV
要求至多有k次交易。
1、使用二维数组 d p [ i ] [ j ] dp[i][j] dp[i][j] :第 i i i天的状态为 j j j,所剩下的最大现金是 d p [ i ] [ j ] dp[i][j] dp[i][j]
j j j 的状态表示为:
- 0 表示不操作
- 1 第一次买入
- 2 第一次卖出
- 3 第二次买入
- 4 第二次卖出
- …除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么 j j j 的范围就定义为 2 ∗ k + 1 2 * k + 1 2∗k+1就可以了。
所以二维dp数组定义为:
vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
PS: d p [ i ] [ 1 ] dp[i][1] dp[i][1],表示的是第 i i i天,买入股票的状态,并不是说一定要第 i i i天买入股票
2、类比123即可。唯一不同的是需要类比j为奇数是买,偶数是卖的状态
for (int j = 0; j < 2 * k - 1; j += 2) {
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
3、不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。因此 d p [ 0 ] [ j ] dp[0][j] dp[0][j]当 j j j 为奇数的时候都初始化为 − p r i c e s [ 0 ] -prices[0] −prices[0]。
for (int j = 1; j < 2 * k; j += 2) {
dp[0][j] = -prices[0];
}
4、一定是从前向后遍历,因为 d p [ i ] dp[i] dp[i],依靠 d p [ i − 1 ] dp[i - 1] dp[i−1]的数值。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if(prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(2*k+1, 0));
for(int j = 1; j < 2 * k; j += 2){
dp[0][j] = -prices[0];
}
for(int i = 1; i < prices.size(); i++){
for(int j = 0; j < 2 * k; j += 2){
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
}
return dp[prices.size() - 1][2 * k];
}
};
309. 最佳买卖股票时机含冷冻期
和 122. 买卖股票的最佳时机 II相比唯一的不同在于:含有冷冻期。
1、dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。
- 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
- 卖出股票状态
- 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
- 状态三:今天卖出了股票
- 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
2、递推公式
达到买入股票状态(状态一)即: d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两个具体操作:
- 操作一:前一天就是持有股票状态(状态一), d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0] = dp[i - 1][0] dp[i][0]=dp[i−1][0]
- 操作二:今天买入了,有两种情况
- 前一天是冷冻期(状态四), d p [ i − 1 ] [ 3 ] − p r i c e s [ i ] dp[i - 1][3] - prices[i] dp[i−1][3]−prices[i]
- 前一天是保持卖出股票状态(状态二), d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i−1][1]−prices[i]
所以操作二取最大值,即: m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 1 ] ) − p r i c e s [ i ] max(dp[i - 1][3], dp[i - 1][1]) - prices[i] max(dp[i−1][3],dp[i−1][1])−prices[i]
那么 d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 1 ] ) − p r i c e s [ i ] ) ; dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); dp[i][0]=max(dp[i−1][0],max(dp[i−1][3],dp[i−1][1])−prices[i]);
达到保持卖出股票状态(状态二)即: d p [ i ] [ 1 ] dp[i][1] dp[i][1],有两个具体操作:
- 操作一:前一天就是状态二, d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i−1][1]
- 操作二:前一天是冷冻期(状态四), d p [ i − 1 ] [ 3 ] dp[i - 1][3] dp[i−1][3]
d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 3 ] ) ; dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); dp[i][1]=max(dp[i−1][1],dp[i−1][3]);
达到今天就卖出股票状态(状态三),即:$dp[i][2] $,昨天一定是买入股票状态(状态一),今天卖出
即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] ; dp[i][2] = dp[i - 1][0] + prices[i]; dp[i][2]=dp[i−1][0]+prices[i];
达到冷冻期状态(状态四),即: d p [ i ] [ 3 ] dp[i][3] dp[i][3],昨天一定卖出了股票(状态三), d p [ i ] [ 3 ] = d p [ i − 1 ] [ 2 ] ; dp[i][3] = dp[i - 1][2]; dp[i][3]=dp[i−1][2];
综上分析,递推代码如下:
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
3、如果是持有股票状态(状态一)那么: d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] dp[0][0] = -prices[0] dp[0][0]=−prices[0],买入股票所剩现金为负数。保持卖出股票状态(状态二),第0天没有卖出 d p [ 0 ] [ 1 ] dp[0][1] dp[0][1]初始化为0就行,今天卖出了股票(状态三),同样 d p [ 0 ] [ 2 ] dp[0][2] dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。同理 d p [ 0 ] [ 3 ] dp[0][3] dp[0][3]也初始为0。
4、从递归公式上可以看出, d p [ i ] dp[i] dp[i] 依赖于 d p [ i − 1 ] dp[i-1] dp[i−1],所以是从前向后遍历。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(4, 0));
dp[0][0] = -prices[0];
for(int i = 1; i < prices.size(); i++){
dp[i][0] = max(dp[i-1][0], max(dp[i-1][3],dp[i-1][1]) - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][3]);
dp[i][2] = dp[i-1][0] + prices[i];
dp[i][3] = dp[i-1][2];
}
return max(dp[prices.size()-1][1], max(dp[prices.size()-1][2], dp[prices.size()-1][3]));
}
};
//时间复杂度:O(n);空间复杂度:O(n)
优化
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0) return 0;
int f0 = -prices[0];
int f1 = 0, f2 = 0, f3 = 0;
for(int i = 1; i < prices.size(); i++){
int newf0 = max(f0, max(f3,f1)-prices[i]);
int newf1 = max(f1, f3);
int newf2 = f0 + prices[i];
int newf3 = f2;
f0 = newf0;
f1 = newf1;
f2 = newf2;
f3 = newf3;
}
return max(f1, max(f2, f3));
}
};
//时间复杂度:O(n);空间复杂度:O(1)
714. 买卖股票的最佳时机含手续费
相对于122. 买卖股票的最佳时机 II,本题只需要在计算卖出操作的时候减去手续费就可以了。
d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示第 i i i天持有股票所省最多现金。 d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第 i i i天不持有股票所得最多现金
如果第 i i i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来
- 第 i − 1 i-1 i−1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 ] [ 0 ] dp[i - 1][0] dp[i−1][0]
- 第 i i i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i−1][1]−prices[i]
所以: d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) ; dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); dp[i][0]=max(dp[i−1][0],dp[i−1][1]−prices[i]);
如果第 i i i天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的情况, 依然可以由两个状态推出来
- 第 i − 1 i-1 i−1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即: d p [ i − 1 ] [ 1 ] dp[i - 1][1] dp[i−1][1]
- 第 i i i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了,即: d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e dp[i - 1][0] + prices[i] - fee dp[i−1][0]+prices[i]−fee
所以: d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e ) ; dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); dp[i][1]=max(dp[i−1][1],dp[i−1][0]+prices[i]−fee);
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2, 0));
dp[0][0] -= 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] - fee);
}
return max(dp[n - 1][0], dp[n - 1][1]);
}
};
300. 最长递增子序列
1、dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
2、位置
i
i
i 的最长升序子序列等于
j
j
j从
0
0
0到
i
−
1
i-1
i−1各个位置的最长升序子序列
+
1
+1
+1 的最大值。所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要
d
p
[
i
]
dp[i]
dp[i]与
d
p
[
j
]
+
1
dp[j]+1
dp[j]+1进行比较,而是要取
d
p
[
j
]
+
1
dp[j]+1
dp[j]+1的最大值。
3、每一个 i i i ,对应的 d p [ i ] dp[i] dp[i](即最长上升子序列)起始大小至少都是 1 1 1
4、 d p [ i ] dp[i] dp[i] 是由 0 0 0到 i − 1 i-1 i−1各个位置的最长升序子序列 推导而来,那么遍历 i i i一定是从前向后遍历。 j j j其实就是 0 0 0到 i − 1 i-1 i−1,遍历 i i i的循环在外层,遍历 j j j则在内层。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for(int i = 1;i < nums.size();i++){
for(int j = 0;j < i;j++){
if (nums[i] > nums[j]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
if(dp[i] > result){
result = dp[i];
}
}
return result;
}
};
674. 最长连续递增序列
动态规划
和300. 最长递增子序列最大的区别在于 本题 要求 连续。
不连续递增子序列的跟前 0 − i 0-i 0−i 个状态有关,连续递增的子序列只跟前一个状态有关
1、dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。
注意这里的定义,一定是以下标
i
i
i为结尾,并不是说一定以下标
0
0
0为起始位置。
2、如果
n
u
m
s
[
i
+
1
]
>
n
u
m
s
[
i
]
nums[i + 1] > nums[i]
nums[i+1]>nums[i],那么以
i
+
1
i+1
i+1为结尾的数组的连续递增的子序列长度 一定等于 以
i
i
i 为结尾的数组的连续递增的子序列长度
+
1
+1
+1 。即:dp[i + 1] = dp[i] + 1;
3、dp[i] = 1;
4、
d
p
[
i
+
1
]
dp[i + 1]
dp[i+1]依赖
d
p
[
i
]
dp[i]
dp[i],所以一定是从前向后遍历
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int result = 1;
vector<int> dp(nums.size(), 1);
for(int i = 0;i < nums.size() - 1;i++){
if (nums[i + 1] > nums[i]){
dp[i + 1] = dp[i] + 1;
}
if(dp[i + 1] > result){
result = dp[i + 1];
}
}
return result;
}
};
//时间复杂度:O(n);空间复杂度:O(n)
贪心解法
遇到 n u m s [ i + 1 ] > n u m s [ i ] nums[i + 1] > nums[i] nums[i+1]>nums[i]的情况, c o u n t + + count++ count++,否则 c o u n t count count为 1 1 1,记录 c o u n t count count的最大值就可以了。
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int result = 1;
int count = 1;
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
count++;
} else { // 不连续,count从头开始
count = 1;
}
if (count > result) result = count;
}
return result;
}
};
//时间复杂度:O(n);空间复杂度:O(1)
718. 最长重复子数组
1、dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
注意,在遍历
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]的时候
i
i
i和
j
j
j都要从
1
1
1开始。
2、根据
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]的定义,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]的状态只能由
d
p
[
i
−
1
]
[
j
−
1
]
dp[i - 1][j - 1]
dp[i−1][j−1]推导出来。即当
A
[
i
−
1
]
A[i - 1]
A[i−1]和
B
[
j
−
1
]
B[j - 1]
B[j−1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
3、根据 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义, d p [ i ] [ 0 ] dp[i][0] dp[i][0]和 d p [ 0 ] [ j ] dp[0][j] dp[0][j]其实都是没有意义的,但 d p [ i ] [ 0 ] dp[i][0] dp[i][0]和 d p [ 0 ] [ j ] dp[0][j] dp[0][j]要初始值,因为为了方便递归公式 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 ; dp[i][j] = dp[i - 1][j - 1] + 1; dp[i][j]=dp[i−1][j−1]+1;所以 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 和 d p [ 0 ] [ j ] dp[0][j] dp[0][j]初始化为 0 0 0。
举个例子: A [ 0 ] = B [ 0 ] A[0] = B[0] A[0]=B[0],则 d p [ 1 ] [ 1 ] = d p [ 0 ] [ 0 ] + 1 dp[1][1] = dp[0][0] + 1 dp[1][1]=dp[0][0]+1,只有 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]初始为 0 0 0,符合递推公式逐步累加。
4、外层 f o r for for循环遍历 A A A,内层 f o r for for循环遍历 B B B。内外层调转也可以。同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把 d p [ i ] [ j ] dp[i][j] dp[i][j]的最大值记录下来。
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int> (nums2.size() + 1 ,0));
int result = 0;
for(int i = 1; i <= nums1.size(); i++){
for(int j = 1; j <= nums2.size(); j++){
if(nums1[i - 1] == nums2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if(dp[i][j] > result){
result = dp[i][j];
}
}
}
return result;
}
};
//时间复杂度:O(n × m),n 为A长度,m为B长度;空间复杂度:O(n × m)
优化:滚动数组
d p [ i ] [ j ] dp[i][j] dp[i][j]都是由 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i−1][j−1]推出。那么压缩为一维数组,就是 d p [ j ] dp[j] dp[j]都是由 d p [ j − 1 ] dp[j - 1] dp[j−1]推出,相当于可以把上一层 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j]拷贝到下一层 d p [ i ] [ j ] dp[i][j] dp[i][j]来继续用。此时遍历B数组的时候要从后向前遍历,避免重复覆盖。
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<int> dp(nums2.size() + 1 ,0);
int result = 0;
for(int i = 1; i <= nums1.size(); i++){
for(int j = nums2.size(); j >= 1; j--){
if(nums1[i - 1] == nums2[j - 1]){
dp[j] = dp[j - 1] + 1;
}else{
dp[j] = 0;
//如果不相等需要赋值为0 ,否则上层的数据未被覆盖会对后面的推导造成影响
}
if(dp[j] > result){
result = dp[j];
}
}
}
return result;
}
};
1143. 最长公共子序列
1、dp[i][j]:长度为[0, i - 1]的字符串1与长度为[0, j - 1]的字符串2的最长公共子序列为dp[i][j]
2、如果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]);
3、
t
e
s
t
1
[
0
,
i
−
1
]
test1[0, i-1]
test1[0,i−1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]=0
。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
4、从递推公式,可以看出,有三个方向可以推出 d p [ i ] [ j ] dp[i][j] dp[i][j],为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for(int i = 1; i <= text1.size(); i++){
for(int j = 1;j <= text2.size(); 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[text1.size()][text2.size()];
}
};
1035. 不相交的线
绘制一些连接两个数字 A [ i ] A[i] A[i] 和 B [ j ] B[j] B[j] 的直线,只要 A [ i ] = = B [ j ] A[i] == B[j] A[i]==B[j],且直线不能相交!
直线不能相交,这就是说明在字符串 A A A中找到一个与字符串 B B B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!那么就和1143. 最长公共子序列是一样的了。代码0改动…
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
for(int i = 1; i <= nums1.size(); i++){
for(int j = 1;j <= nums2.size(); 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[nums1.size()][nums2.size()];
}
};
53. 最大子数组和
1、dp[i]:包括下标i之前的最大连续子序列和为dp[i]
2、 d p [ i ] dp[i] dp[i]只有两个方向可以推出来:
dp[i - 1] + nums[i]
,即:nums[i]加入当前连续子序列和nums[i]
,即:从头开始计算当前连续子序列和
3、dp[0] = nums[0]
4、
d
p
[
i
]
dp[i]
dp[i]依赖于
d
p
[
i
−
1
]
dp[i - 1]
dp[i−1]的状态,需要从前向后遍历
。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0) return 0;
vector<int> dp(nums.size(), 0); //dp[i]表示包括i之前的最大连续子序列和
dp[0] = nums[0];
int result = dp[0];
for(int i = 1; i < nums.size(); i++){
dp[i] = max(dp[i - 1] + nums[i], nums[i]); //状态转移公式
if(dp[i] > result)
result = dp[i];
}
return result;
}
};