确定dp数组以及下标的含义
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
确定递推公式
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
dp数组如何初始化
递推公式的基础依赖于dp[0] 和 dp[1]
i=0时,dp[0]只能是nums[0],i=1时,dp[1]取nums[0]和nums[1]的最大值
dp[0]= =nums[0],dp[1]=max(nums[0], nums[1]);
确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么从前到后遍历
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-1], dp[i-2]+nums[i]);
}
return dp[nums.size()-1];
}
};
情况三考虑包含尾元素,但不一定要选尾部元素,并且对于情况三,取nums[1] 和 nums[3]就是最大的。
同时情况二和情况三都包含了情况一,所以只考虑情况二和情况三就可以了。
步骤
确定dp数组以及下标的含义
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
确定递推公式
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
dp数组如何初始化
递推公式的基础依赖于dp[0] 和 dp[1]
i=0时,dp[0]只能是nums[0],i=1时,dp[1]取nums[0]和nums[1]的最大值
dp[0]= =nums[0],dp[1]=max(nums[0], nums[1]);
确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么从前到后遍历
C++实现
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);
}
int robrange(vector<int>& nums, int start, int end)
{
if(start == end) 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-1], dp[i-2]+nums[i]);
}
return dp[end];
}
};
和198.打家劫舍类似,但是房屋是二叉树形结构,对于树的遍历方式,前中后序(深度优先搜索),层序遍历(广度优先搜索)。本题一定是后序遍历,因为通过递归函数的返回值来做下一步计算。
与198.打家劫舍,213.打家劫舍II一样,关键在于当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子
动态规划使用状态转移容器来记录状态的变化,可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
步骤
确定终止条件
遍历的过程中,如果遇到空节点的话,无论偷还是不偷都是0,返回,相当于dp数组的初始化
确定遍历顺序
val1 = cur->val + left[0] + right[0];
val2 = max(left[0], left[1]) + max(right[0], right[1]);
{val2, val1};
即 {不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
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);
//偷cur 不偷左右节点
int var1 = cur->val + left[0] + right[0];
//不偷cur 可以偷或者不偷左右节点 取较大值
int var2 = max(left[0], left[1]) + max(right[0], right[1]);
return {var2, var1};
}
};
步骤
dp[i][0]可以由两个状态推出来
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1]可以由两个状态推出来
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
dp[0][0] = -prices[0];
dp[0][1] = 0;
确定遍历顺序
dp[i]由dp[i - 1]推导出而来,从前向后遍历
举例推导dp数组
输入: 输入:[7,1,5,3,6,4]
最终结果是dp[5][1],而不是dp[5][0],因为不持有股票状态所得金钱一定比持有股票状态得到的多
C++实现
class Solution {
public:
int maxProfit(vector<int>& prices) {
//二维dp数组
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];
/*
//二维dp滚轮数组
int len = prices.size();
if(len == 0) return 0;
vector> dp(2, vector(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for(int i=1; i
}
};
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;
}
};
本题和121. 买卖股票的最佳时机的唯一区别是股票可以买卖多次,注意最多只能持有 一只股票,再次购买前要出售掉之前的股票。区别主要是体现在递推公式上,其他都一样。
步骤
dp[i][0]可以由两个状态推出来
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1]可以由两个状态推出来
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
dp[0][0] -= prices[0];
dp[0][1] = 0;
确定遍历顺序
dp[i]由dp[i - 1]推导出而来,从前向后遍历
C++实现
class Solution {
public:
int maxProfit(vector<int>& prices) {
//动态规划
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2, 0));
dp[0][0] = -prices[0];//持股票
dp[0][1] = 0;//持现金
for(int i=1; i<n; i++)
{
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);//第i天买了股票
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);//第i天卖了股票
}
return dp[n-1][1];
/*
//二维dp滚轮数组
int len = prices.size();
if(len == 0) return 0;
vector> dp(2, vector(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for(int i=1; i
}
};
class Solution {
public:
//贪心算法
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i=1; i<prices.size(); i++)//i从1开始,第二天才有利润
{
//累加每天的正利润,最后求的最大利润
result += max(prices[i]-prices[i-1], 0);
}
return result;
}
};
和122.买卖股票的最佳时机II主要区别在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。因此,可以假设一天有五种状态来记录:
0-没有操作
1-第一次持有股票
2-第一次不持有股票
3-第二次持有股票
4-第二次不持有股票
步骤
确定dp数组以及下标的含义
dp[i][j]:i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
要注意的是,dp[i][1]表示的是第i天持有股票的状态,并不一定是第i天买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续持有股票的这个状态。
确定递推公式
五种状态的推导
dp[i][1],持有股票,两个状态中取最大值
dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
dp[i][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]);
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
红色框为最后两次卖出的状态,现金最多的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出,因为dp[i][j]表示第i天状态j所剩最大现金。
保存每一天的五种状态
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size() == 0) return 0;
//保存每一天的五种状态
vector<vector<int>> dp(prices.size(), vector<int>(5, 0));//第0天没有操作 第一次卖出 第二次卖出操作
dp[0][1] = -prices[0];//第0天第一次买股票
dp[0][3] = -prices[0];//第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]);//max()里的dp都是前一天的状态
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];//第二次不持有股票的时候所得现金最多
}
};
在 123.买卖股票的最佳时机III的基础上,要求至多有k次交易,那么当天有2k+1次操作:
0-不操作(可以不定义)
1-第一次持有股票
2-第一次不持有股票
3-第二次持有股票
4-第二次不持有股票
5-第三次持有股票
…
除了0以外,偶数就是卖出,奇数就是买入
步骤
确定dp数组以及下标的含义
dp[i][j]:i表示第i天,j有2k+1种状态,dp[i][j]表示第i天状态j所剩最大现金。
要注意的是,dp[i][1]表示的是第i天持有股票的状态,并不一定是第i天买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续持有股票的这个状态。
确定递推公式
dp[i][1],持有股票,两个状态中取最大值
dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
dp[i][2],不持有股票,两个状态中取最大值
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
和123.买卖股票的最佳时机III 最大的区别就是j为奇数是买入,偶数是卖出状态
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
dp[0][j]当j为奇数时都初始化为 -prices[0],j为偶数是卖、奇数是买的状态
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k],红框部分
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));
//初始化dp[i][j] j为奇数的状态
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];
}
};
dp[i][0],状态一,持有股票状态的两种方法
1)前一天持有股票,dp[i][0] = dp[i - 1][0]
2)前一天是冷冻期,dp[i - 1][3] - prices[i];或者前一天是保持卖出状态,dp[i - 1][1] - prices[i]
dp[i][0]取所得现金最大的,dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
dp[i][1],状态二,保持卖出股票状态的两种方法
1)前一天是保持卖出状态,dp[i - 1][1]
2)前一天是冷冻期,dp[i - 1][3]
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];
确定遍历顺序
dp[i]由dp[i - 1]推导出而来,从前向后遍历
举例推导dp数组
输入[1,2,3,0,2]
最后结果是取状态二,状态三,和状态四的最大值。状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。
C++实现
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n == 0) return 0;
//0-状态一 持有股票
//1-状态二 保持卖出 之前的某一天卖出了股票
//2-状态三 今天卖出
//3-状态四 冷冻期
vector<vector<int>> dp(n, vector<int>(4, 0));//状态二三四 都初始化为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], 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][1], max(dp[n-1][2], dp[n-1][3]));
}
};
相对于122.买卖股票的最佳时机II,本题只需要在计算卖出操作的时候减去手续费,其他一样,主要区别体现在递推公式
步骤
dp[i][0]可以由两个状态推出来
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1]可以由两个状态推出来
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
dp[0][0] = -prices[0];
dp[0][1] = 0;
确定遍历顺序
dp[i]由dp[i - 1]推导出而来,从前向后遍历
C++实现
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] = -prices[0];
//0-持有股票
//1-不持有股票
for(int i=1; i<len; 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 dp[len-1][1];
/*//贪心算法
int result = 0;
int minprice = prices[0];
for(int i=1; i= minprice && prices[i] <= minprice+fee) continue;
//计算利润 最后一天计算利润才是真的卖出日期
if(prices[i] > minprice+fee)
{
result += prices[i] - minprice - fee;
minprice = prices[i] - fee;//每天要更新最低价格
}
}
return result;*/
}
};
劫舍系列分别是198题数组上连续元素二选一,213题成环之后连续元素二选一,377题在树上连续元素二选一,所能得到的最大价值。
股票系列分别是从买卖一次到买卖多次,从最多买卖两次到最多买卖k次,从冷冻期再到手续费:121题只能买卖一次,122题可以买卖多次,123题最多买卖两次,188题最多买卖k次,309题买卖多次且卖出一天有冷冻期,714题买卖多次且有手续费。