动态规划三
6/23
T53 最大子序和
1、这一题之前有贪心解法,但贪心解法虽然简洁,理解难度却比动态规划大
2、动态规划解法中,赋值dp[i]需要相当谨慎:既然是连续的,就要卡死结尾;而我又想要最大和,因此可以只要一个结尾,也可以加上左侧部队一起。这样万一左侧总和为负,就可以抛弃之
3、初始元素和result的初始赋值很重要,要考虑nums只有一个元素、初始元素为负数的情况,即便全数组都为负数,result也可以取最小的一个元素,而该元素可能为nums[0]
4、代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size(),0);
int result = nums[0];
if(nums.size() == 1)
return result;
//dp[i]表示数组[0...i]之间的以nums[i]结尾的具备最大和的连续子数组
dp[0] = nums[0];
for(int i = 1;i < nums.size();i++){
dp[i] = max(nums[i],nums[i] + dp[i-1]);
if(dp[i] > result)
result = dp[i];
}
return result;
}
};
T1035 不相交的线
1、不相交意味着:所有组合之间要么两个元素都大于对方两者,要么两个元素都小于
2、和上一题类似,若相等,加一;不相等,只取其一更新max。重要:之所以只取其一,是因为若新的两个元素都取,且队伍统一在左侧,必然会出现两线交叉情况,故舍弃
3、代码如下:
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()];
}
};
T1143 最长公共子序列
1、这一题很适合用来养成良好习惯,手搓时没想清楚边界问题一下就会暴露出来。首先,dp[i][j]代表的是长度为i j的两个数组的最长公共子序列的长度,那么坐标最大值就是i-1 j-1,这才是当前需要比较是否相等的值;其次,遍历时也应该把结尾遍历到,而非常见的取小于号;最后,dp[0][0]是无意义的,没有坐标为-1的元素,这也是最终循环统一右移一位的原因
2、不要死套公式,这一题既没有连续的要求,又无单调性要求,毫无必要卡死“以…为结尾”这一要求,这样的序列是可以灵活跳脱的
3、当心:并不是不等就不管它或直接赋值上一层,即便最后两个元素不等,它们也可以分别加入与对面序列进行灵活取公,也就是dp[i-1][j] dp[i][j-1],这点思考很重要,容易漏掉。顺便,string是可以直接按照char取坐标位的,无需拆入新的容器,也可以直接调用size()方法
4、代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1,vector<int>(text2.size() + 1,0));
//dp[i][j]代表在text1[0...i-1]和text2[0...j-1]中最长公共子序列的长度
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()];
}
};
6/20
T718 最长重复子数组
1、这道题有滑动窗口解法,二轮复习时重点研究
2、这一题关键在于拓展思路:既然涉及两个数组,那么严肃上上道经典题目,采取尾部比较方法,直接让dp[i][j]利用二维来解决双数组问题即可,非常聪明的做法
3、本题还有滚动数组一维解法,有时间可以探究一下
4、代码如下:
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;
}
};
T674 最长连续递增序列
1、学会了上一题后,这题稍微变通一下就自己写出来啦~主要区别:既然要求连续递增,就只需关心i相邻的前一个j,即i-1就够了,因为以nums[i]结尾的序列必须和前面的序列连续。其它部分例如比较结尾元素大小等和上一题均如出一致
2、代码如下:
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
//dp[i]表示以nums[i]结尾的连续递增子序列的最大长度
vector<int> dp(nums.size(),1);
int result = 0;
if(nums.size() == 1)
return 1;
for(int i = 1;i < nums.size();i++){
if(nums[i-1] < nums[i])
dp[i] = dp[i-1] + 1;
if(dp[i] > result)
result = dp[i];
}
return result;
}
};
【重要】T300 最长递增子序列
1、这一题看着代码短,没有接触过类似问题的话思路很难想到,是一系列经典思维的开端:固定某个序列的结尾值,按照右侧截断数组的方式从前往后比较
2、关键:dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度,if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
3、假如小于呢?是否会漏掉情况?实则不会,因为每个i之前的所有j都遍历过,因此假设某个j对应的最长子序列是1 3 5 7 9,nums[i] = 8,虽然在个情况下被舍弃了,但j稍微往前走一点,最长子序列为1 3 5 7 时就可以纳入nums[i]形成1 3 5 7 8序列,dp[i]依旧会被正确更新
4、代码如下:
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;
}
};
6/18
T309买卖股票的最佳时期含冷冻期
1、这一题比较难,复杂度较高,摘录随想录分析如下: 达到买入股票状态(状态一)即:dp[i][0],有两个具体操作: 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0] 操作二:今天买入了,有两种情况 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i] 前一天是保持卖出股票的状态(状态二),dp[i - 1][1] - prices[i] dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作: 操作一:前一天就是状态二 操作二:前一天是冷冻期(状态四) dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作: 昨天一定是持有股票状态(状态一),今天卖出 即:dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态(状态四),即:dp[i][3],只有一个操作: 昨天卖出了股票(状态三) dp[i][3] = dp[i - 1][2];
2、状态转移图
3、注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态
4、代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0;
vector<vector<int>> dp(n, vector<int>(4, 0));
dp[0][0] -= prices[0]; // 持股票
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], 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[n - 1][3], max(dp[n - 1][1], dp[n - 1][2]));
}
};
T188 买卖股票的最佳时机四
1、上一题进阶版!实则发现多状态转移规律后很简单,用一个for循环上公式即可
2、关键:除了0以外,偶数就是卖出,奇数就是买入
3、核心状态转移方程: 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]); }
4、代码:
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 - 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]);
}
}
return dp[prices.size() - 1][2 * k];
}
};
6/17
【重要】T123 买卖股票的最佳时机三
1、这一题级别为困难,的确有两个不好解决的地方:首先两笔交易不能重合,这样我们就不能直接在切割时保留一个min,这样无法确保中间无重叠区域;其次,第二笔交易未必是相邻非负差值,很可能买入日期涉及到前面的日子,无法直接直觉解题
2、官方解法:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int buy1 = -prices[0], sell1 = 0;
int buy2 = -prices[0], sell2 = 0;
for (int i = 1; i < n; ++i) {
buy1 = max(buy1, -prices[i]);
sell1 = max(sell1, buy1 + prices[i]);
buy2 = max(buy2, sell1 - prices[i]);
sell2 = max(sell2, buy2 + prices[i]);
}
return sell2;
}
};
3、关键题解摘录:
4、这一题是少见的具备超过两个状态,且转移方程也有多个的题目,类似计算机网络里的一些状态转移传输逻辑
5、困惑点一:为何要每次取最大值,新买入了股票一定是扣钱的,理应变小一些,为何还强行要取最大值
答案:例如两个buy的方程,对比的实际上是什么时候买入更划算,负数绝对值更小,第三天价格3的话-3就比第一题价格5时的-5划算;sell方程则
6、困惑点二:为何要按照这样的顺序列方程,我换一下顺序可以吗
答案:不能,需要使用更新后的值
7、该题动态规划法总体思路仍较为抽象,需要严格的数学证明、逻辑推导,待解决+听详细网课
T121 买卖股票的最佳时机二
1、之前贪心专题涉及过,此次采用动态规划
2、这一题的动态规划意外地比贪心思路简单,甚至比第一题思路更清晰:截止某日的最大利润等于截止前一日的最大利润+非负相邻增值,时间复杂度也很好,不需要像贪心那样利用上升坡理解抽象思维
3、代码如下,AC:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//dp[i]表示截止到第i日的最大利润
vector<int> dp(prices.size());
dp[0] = 0;
for(int i = 1;i < prices.size();i++){
dp[i] = dp[i-1] + max(0,prices[i] - prices[i-1]);
}
return dp[prices.size() - 1];
}
};
T121 买卖股票的最佳时机
1、这一题我第一眼觉得和贪心很像,但既然在动态规划专题,选择动态规划的做法。我的思路是定义一个“最迟交易日”,然后根据是否在最后一天交易来构建状态方程,同时维护一个最小值。这一点和背包问题中单拎出来一个物品是否纳入很像
2、当心:变量命名不要和标准库函数名重合,例如min
3、我的代码AC如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//dp[i]表示最迟在第i天卖出这只股票所能获得的最大利润
vector<int> dp(prices.size());
dp[0] = 0;
int myMin = prices[0];
for(int i = 1;i < prices.size();i++){
dp[i] = max(dp[i-1],prices[i] - myMin);
myMin = min(myMin,prices[i]);
}
return dp[prices.size()-1];
}
};
4、注意:股票系列的几道题,未来贪心方法也全部探索梳理一遍
6/16
【重要】T337打家劫舍三
1、…..还有三?谁家小区房屋排列成二叉树形状
2、传统递归方法可解决(后序遍历最佳,配合函数返回值)
3、若用动态规划,有一定难度,需要对一颗树的状态建模:dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱
4、递归方法(含时空复杂度优化):
class Solution {
public:
unordered_map<TreeNode* , int> umap; // 记录计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
umap[root] = max(val1, val2); // umap记录一下结果
return max(val1, val2);
}
};
5、动态规划方法:
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 vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
T231 打家劫舍二
1、和初始题目的主要区别:屋子成环,之前的情况一被融合
2、变化不大,代码如下:
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); // 情况二
int result2 = robRange(nums, 1, nums.size() - 1); // 情况三
return max(result1, result2);
}
// 198.打家劫舍的逻辑
int robRange(vector<int>& nums, int start, int end) {
if (end == start) return nums[start];
vector<int> dp(nums.size());
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]);
}
return dp[end];
}
};