打家劫舍Ⅰ-Ⅳ
打家劫舍Ⅰ
[题]
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
[例]
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
[思路]
当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。通过这种依赖关系想到动态规划。
定义这样一个dp数组: dp[i]
表示抢劫前i
个房间可得到的最大金额。
尝试写出递推公式: 不抢i
房间,dp[i]=dp[i-1]
; 抢i房间, dp[i] = dp[i-2] + nums[i]
。dp[i]
取2种情况的最大值。
dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
或许你有疑问: 万一dp[i-1]没有抢房间i-1,但是dp[i-1]>dp[i-2]。dp[i] 是不是应该等于dp[i-1]+nums[i]?
其实这种情况不可能发生。因为如果dp[i-1]没有抢房间i-1,那么dp[i-1]一定等于dp[i-2],不可能会是dp[i-1]>dp[i-2]。
初始化: 从递推公式可知,dp[i]
的值取决于dp[i-1]
和dp[i-2]
的值。所以要对dp[0]
和dp[1]
初始化。其他值先初始化为0。
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
[代码]
int rob(vector<int>& nums) {
if(nums.size()== 1) return nums[0];//长度为1直接返回唯一的结果
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 = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
[思路]
这题对比上一题多了一个条件:不能同时偷窃第一个房间和最后一个房间。
那么我们可以分2种情况讨论:
- 只偷窃前n-1个房间;
- 只偷窃后n-1个房间。
得到的结果一定不会同时包含第一个房间和最后一个房间。最后,返回2种情况的最大金额。
[代码]
int rob1(vector<int>& nums, int start, int end) {
if(start==end) 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-1], dp[i-2]+nums[i]);
}
return dp[end];
}
public:
int rob(vector<int>& nums) {//分2种情况偷窃
if(nums.size()==1) return nums[0];
int res1 = rob1(nums, 0, nums.size()-2);//偷窃前n-1个房间得到的最大金额
int res2 = rob1(nums, 1, nums.size()-1);//偷窃后n-1个房间得到的最大金额
return max(res1, res2);
}
打家劫舍Ⅲ
[题]
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
[例]
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
[思路]
该题与之前的数组不一样,是二叉树。所以先考虑优先二叉树的遍历:由于父节点的偷窃金额需要知道子节点的偷窃金额,所以优先考虑后序遍历。
对于每个节点,都存在2种状态(不偷窃,偷窃)。所以这里我们可以设计一个dp数组,表示该节点2种状态下可获得的最大金额。
- 如果遇到空节点的话,很明显,无论偷还是不偷都是0。
if(root==NULL) return vector<int> {0,0};
- 对于其他节点,通过子节点计算偷与不偷可得的最大金额,然后放回该节点的dp数组
int rob_cur = root->val + left[0] + right[0];//若偷窃该节点,结果为当前节点金额+不偷左孩子节点可得的钱+不偷右孩子节点可得的钱
int norob_cur = max(left[0], left[1]) + max(right[0], right[1]);//如果不偷该节点,结果为左孩子结果的最大值(偷不偷都可)+右孩子结果的最大值
return {norob_cur, rob_cur};//返回当前节点的结果(不偷,偷)
或许你有疑问: 为什么数组dp需要与数组一样大小的dp数组,而二叉树dp只需要一个极少元素的dp数组?
这说明你对二叉数的遍历还理解不够。二叉树中的dp数组是递归的,这意味着其实每一个节点都会有自己的dp数组。所以实际上,二叉树dp比数组dp多一个维度。
[代码]
vector<int> robtree(TreeNode* root)//后序遍历,因为要用孩子的抢劫结果推出父节点的偷窃结果
{
if(root==NULL) return vector<int> {0,0};//遇到空节点,偷不偷都为0
vector<int> left = robtree(root->left);//递归左孩子,得到左孩子的dp数组
vector<int> right = robtree(root->right);//递归右孩子,得到右孩子的dp数组
//中节点
int rob_cur = root->val + left[0] + right[0];//若偷窃该节点,结果为当前节点金额+不偷左孩子节点可得的钱+不偷右孩子节点可得的钱
int norob_cur = max(left[0], left[1]) + max(right[0], right[1]);//如果不偷该节点,结果为左孩子结果的最大值(偷不偷都可)+右孩子结果的最大值
return {norob_cur, rob_cur};//返回当前节点的结果(不偷,偷)
}
public:
int rob(TreeNode* root) {
vector<int> res = robtree(root);
return max(res[0], res[1]);
}
打家劫舍Ⅳ
[题]
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums 表示每间房屋存放的现金金额。形式上,从左起第 i 间房屋中放有 nums[i] 美元。
另给你一个整数 k ,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k 间房屋。
返回小偷的 最小 窃取能力。
[例]
输入:nums = [2,3,5,9], k = 2
输出:5
解释:
小偷窃取至少 2 间房屋,共有 3 种方式:
- 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。
- 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。
- 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。
因此,返回 min(5, 9, 9) = 5 。
[思路]
参考这篇二分+dp
[代码]
class Solution {
public:
int minCapability(vector<int> &nums, int k) {
int left = 0, right = *max_element(nums.begin(), nums.end());
while (left + 1 < right) {
int mid = left + (right - left) / 2;
int f0 = 0, f1 = 0;
for (int x : nums)
if (x > mid) f0 = f1;
else {
int tmp = f1;
f1 = max(f1, f0 + 1);
f0 = tmp;
}
(f1 >= k ? right : left) = mid;
}
return right;
}
};
背包问题
背包问题是典型的动态规划问题。重点掌握0-1背包和完全背包即可。0-1背包和完全背包的区别在于,完全背包可以重复选取同一物品,而0-1背包一个物品只有一个。
1. 0-1背包
假设你有n件物品和一个最多能背重量为w 的背包。每件物品都有自己的价值和重量,且每个物品只有一个。每件物品你都可以选择背或不背,这就是0-1的意思。那么,问你将哪些物品装入背包里获得的物品价值总和最大。
举个例子:假设有如下三件物品,背包容量为4。求可获得得最大价值。
物品 | 重量 | 价值 |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
对于这个问题,假设背包内部已经放了一些物品,那么背包下一步的状态和当前状态是强烈相关的。所以可以考虑用动态规划的方法。
用一个二维数组dp[i][j]
记录背包的状态。数组中的每一项表示背包的容量为 j 时,只考虑放入 0~i 中的物品,可获得的最大价值。
不难写出状态转移方程:dp[i][j] = max(dp[i-1]dp[j], dp[i - 1][j - weight[i]] + value[i])
。其中,dp[i-1]dp[j]
表示不放物品 i,容量为 j 的背包可获得的最大价值;dp[i - 1][j - weight[i]] + value[i]
表示放物品 i,可获得的最大价值。
初始化: 从递推公式中可以看出dp[i][j]
跟上一行的元素是相关的。所以得先初始化第一行,即dp[0][j]
。直接遍历j, 当 j>=weight[0] && j<=4, dp[0][j] = value[0]。 其他dp元素因为还没有装入任何物品,所以都初始化为0.
返回值: 最后返回dp[2][4] 即为背包容量为4时,放入0~2中得物品可得的最大价值。
- 根据状态转移方程和初始化,写出如下代码:
void zero_one_bag(){
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); 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]);
}
}
cout<< dp[weight.size()-1][bagweight]<<endl;
}
- 动态规划过程如图:
- 优化:一维dp数组(滚动数组)
从上面的动态规划过程中,可以发现
dp[i][j]
实际上只由dp[i-1][j]
和dp[i-1][j]
左边的某一元素决定。所以实际上我们只需要维护一个一维的数组。遍历背包容量改为从右到左,这样就可以利用到上一层遍历后得到的结果。
状态转移方程改为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
。其中max里面的dp[j],和dp[j - weight[i]] + value[i]是上一层遍历后得到的背包容量为j时可得的最大价值和背包容量为j - weight[i]得到的最大价值。 这2项都不含有物品i。
void zero_one_bag2(){
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
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]);
}
}
cout << dp[bagWeight] << endl;
}
2. 完全背包
还是用上面那个例子。假设有如下三件物品,背包容量为4。但是物品可以重复选取,求可获得得最大价值。
物品 | 重量 | 价值 |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
对于这个问题,因为物品可以重复选取。所以状态方程要有所改变。
状态转移方程:dp[i][j] = max(dp[i-1]dp[j], dp[i][j - weight[i]] + value[i])
其中,dp[i-1]dp[j]
还是不变,表示不装入物品i可获得的最大价值;后一项改为dp[i][j - weight[i]] + value[i]
,表示背包容量为 [j - weight[i]] 且可能装有物品 i 时可获得的最大价值。所以与0-1背包的主要区别在于,在考虑装入物品i 时,不再是比较不装物品 i 时2个状态的比较,而是与装有物品 i 的状态比较。这就体现了完全背包可以重复选取同一物品的特点。
初始化: 从状态方程可以看出,想求得dp[i]dp[j]
必须知道dp[i-1]dp[j]
,dp[i][j - weight[i]]
的值。所以需要初始化第一行和第一列的值。第一列的初始化由于此时背包容量为0,所以dp[i][0] 都一定为0。第一行的初始化要考虑dp[0][j - weight[i]]
的值。
根据意思分析写出如下代码:
void complete_bag(){
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = dp[0][j - weight[i]]+value[0];//这里变了!!!
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); 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][j - weight[i]] + value[i]);//这里变了!!!
}
}
cout<< dp[weight.size()-1][bagweight]<<endl;
}
- 动态规划过程如图:
优化:一维dp数组(滚动数组)
跟0-1背包一样,也可以通过维护一个一维的dp数组到达二维数组一样的效果。 从状态转移方程可知,
dp[i][j]
的值跟dp[i-1]dp[j]
和dp[i][j - weight[i]]
值有关。所以在对j遍历的时候需要从左到右遍历。
状态方程改为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
。dp[j]
相当于dp[i-1]dp[j]
,dp[j - weight[i]] + value[i]
相当于dp[i][j - weight[i]]+ value[i]
void complete_bag2() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
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]);//
}
}
cout << dp[bagWeight] << endl;
}
1641. 统计字典序元音字符串的数目(完全背包求排列数)
【题】
给你一个整数n,请返回长度为 n 、仅由元音 (a, e, i, o, u) 组成且按字典序排列的字符串数量。
字符串 s 按字典序排列 需要满足:对于所有有效的 i,s[i] 在字母表中的位置总是与 s[i+1] 相同或在 s[i+1] 之前。
【例】
输入:n = 1
输出:5
解释:仅由元音组成的 5 个字典序字符串为 [“a”,“e”,“i”,“o”,“u”]
1. 公式法
[思路]
大家也可以想成是:把 n 个字符分配给五个元音所代表的盒子中,且部分盒子可以为空。一旦每个盒子中的字符个数定了,那么这个字符串也固定下来了。
我们先把 n + m 个小球放到 m 个元音盒子里,盒子不能为空,然后再在每个盒子里拿走 1 个小球,总共拿走了 m 个小球,得到的结果,就是把 n 个小球放到 m 个盒子里,盒子可以为空的解。
把 n + m 个小球放到 m 个盒子里,盒子不能为空的分法,就是C(n + m - 1, m - 1)
,即在n+m个小球的n+m-1个间隔中插入m-1个隔板,分成不为空的m份。
所以,把 n 个小球放到 m 个盒子里,盒子可以为空,答案为C(n + m - 1, m - 1)
。
对于这个问题,m = 5,盒子可以为空,所以答案是 C(n + 5 - 1, 5 - 1) = C(n + 4, 4)
[代码]
class Solution {
public:
int countVowelStrings(int n) {
return (n + 4) * (n + 3) * (n + 2) * (n + 1) / 24;
}
};
2. 动态规划
本题其实也可以看成是完全背包问题:一个容量为n的背包,往里面放入5个元素,可重复放入同一元素,求排列数。
dp[j]
——以第j个字符结尾的按字典序排列的字符串数量,状态转移方程如下:
dp[j] = dp[j] + dp[j - 1];
第j个字符结尾的n长度的序列数量=j字符结尾的n-1长度的序列数量+j-1字符结尾的n长度的序列数量.
遍历顺序:可重复选取,所以从左向右遍历背包;
求排列,所以外层遍历背包,内层遍历元素初始化:
容量为1时,都为1.
class Solution {
public:
int countVowelStrings(int n) {
vector<int> dp(5, 1);//长度为1的序列都只有一种可能
for (int i = 1; i < n; i++) {//从长度为2开始迭代
for (int j = 1; j < 5; j++) {//j从1开始遍历,因为a结尾的序列不管长度多少都只有一种可能
dp[j] += dp[j - 1];//第j个字符结尾的n长度的序列数量=j字符结尾的n-1长度的序列数量+j-1字符结尾的n长度的序列数量
}
}
return accumulate(dp.begin(), dp.end(), 0);//统计所有的序列数量
}
};
买卖股票问题
买卖股票问题是典型的动态规划的问题。因为股票问题一般求可获得的最大价值。最后可获得的最大价值明显和上一步可获得最大价值强烈相关。此类问题的关键在于根据题意确定每支股票的状态下可获得最大值。
买卖股票的最佳时机Ⅰ
[题]
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
1 <= prices.length <= 105
0 <= prices[i] <= 104
[例]
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
[思路]
因为只可以买卖一次,所以只有2种状态:
- 当前已经持有股票;
- 当前未持有股票。
dp数组设计为:
vector<int> dp(2,0)
.
dp[0]
表示当前未持有股票可得的最大价值,dp[1]
表示当前持有股票可获得的最大价值。状态转移方程:
dp[0] = max(dp[0], dp[1]+prices[i]
当前不持有股票可获得的最大价值 = max(之前卖出股票可获得最大价值, 当前卖出股票可获得的最大价值)
dp[1] = max(dp[1], -prices[i])
当前持有股票可获得的最大价值 = max(之前买股票可获得的最大值,当前买股票获得的价值)初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
dp[0] = 0;
dp[1] = -prices[0];
股票 | dp[0] | dp[1] |
---|---|---|
7 | 0 | -7 |
1 | 0 | -1 |
5 | 4 | -1 |
3 | 4 | -1 |
6 | 5 | -1 |
4 | 5 | -1 |
[代码]
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size();
vector<int> dp(2, 0);
dp[0] = 0;
dp[1] = -prices[0];
for(int i=1; i<size; i++)
{
dp[0] = max(dp[0], dp[1]+prices[i]);
dp[1] = max(-prices[i], dp[1]);
}
return dp[0];
}
};
买卖股票的最佳时机Ⅱ
[题]
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
[例]
输入: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 。
[思路]
本题和上一题题意基本一致,但是本题可以无限次买入和卖出。
dp数组设计为:vector<int> dp(2,0)
.本题依旧只有2种状态,所以dp数组定义不变。状态转移方程: 注意dp[1]改变了,因为不限次买卖股票,所以买入股票可以在之前买卖股票获得收益的基础上再次买入股票。
dp[0] = max(dp[0], dp[1]+prices[i]
当前不持有股票可获得的最大价值 = max(之前买卖(不持有)股票可获得最大价值, 还持有股票在当前卖出股票可获得的最大价值)
dp[1] = max(dp[1], p[0]-prices[i])
当前持有股票可获得的最大价值 = max(之前持有股票可获得的最大值,之前不持有股票但买下当前股票获得的价值)初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
dp[0] = 0;
dp[1] = -prices[0];
整体流程如下表:
股票 | dp[0] | dp[1] |
---|---|---|
7 | 0 | -7 |
1 | 0 | -1 |
5 | 4 | -1 |
3 | 4 | 1 |
6 | 7 | 1 |
4 | 7 | 3 |
[代码]
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> dp(2,0);
dp[0] = 0;
dp[1] = -prices[0];
for(int i=1; i<size; i++)
{
dp[0] = max(dp[0], dp[1]+prices[i]);
dp[1] = max(dp[0]-prices[i], dp[1]);//这里变了
}
return dp[0];
}
};
买卖股票的最佳时机Ⅲ(hard)
[题]
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
1 <= prices.length <= 105
0 <= prices[i] <= 105
[例]
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
[思路]
本题限制了买卖的次数。所以存在5种状态:
- 买卖0次;
- 买了1次;
- 卖了1次;
- 买了2次;
- 卖了2次;
dp数组设计为:
vector<int> dp(5,0)
.
状态转移方程:dp[0] = 0;
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]);初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
dp[0] = dp[2] = dp[4] =0;
dp[1] = dp[3] = -prices[0];
股票 | dp[0] | dp[1] | dp[2] | dp[3] | dp[4] |
---|---|---|---|---|---|
3 | 0 | -3 | 0 | -3 | 0 |
3 | 0 | -3 | 0 | -3 | 0 |
5 | 0 | -3 | 2 | -3 | 2 |
0 | 0 | 0 | 2 | 2 | 2 |
0 | 0 | 0 | 2 | 2 | 2 |
3 | 0 | 0 | 3 | 2 | 5 |
1 | 0 | 1 | 3 | 2 | 5 |
4 | 0 | 0 | 4 | 2 | 6 |
[代码]
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];
}
};
买卖股票的最佳时机Ⅳ(hard)
[题]
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000
[例]
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
[思路]
本题限制了买卖的次数为
k
。所以存在2k+1
种状态,就不列举了,和上一题类似。
dp数组设计为: vector<int> dp(2*k+1,0);
状态转移方程:
for(int i= 1; i<2*k; i+=2)
{
dp[i] = -prices[0];
}
初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
for(int i= 1; i<2*k; i++)
{
dp[i] = -prices[0];
}
[代码]
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector<int> dp(k*2+1, 0);
for(int i= 1; i<2*k; i+=2)
{
dp[i] = -prices[0];
}
for(int i=1; i<prices.size(); i++)
{
for(int j=1; j<=2*k; j++)
{
if(j%2!=0) dp[j] = max(dp[j], dp[j-1] - prices[i]);
else dp[j] = max(dp[j], dp[j-1]+prices[i]);
}
}
return dp[2*k];
}
};
最佳买卖股票时机Ⅴ时期含冷冻期
[题]
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
1 <= prices.length <= 5000
0 <= prices[i] <= 1000
[例]
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
[思路]
本题增加了一个条件:若本次卖出了股票,下一次不能立即购买股票。所以本题实际上有三种状态:
- 持有股票;
- 不持有股票,是之前卖的,下次可以买;
- 不持有股票,就是本次卖的,下次不可以买啦;
dp数组设计为:vector<int> dp(3,0)
.
状态转移方程:
dp[0] = max(dp[0], dp[1]-prices[i]);
dp[1] = max(dp[1], dp[2]);
dp[2] = dp[0]+prices[i];
初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
dp[0] = -prices[0];
dp[1] = 0;
dp[2] = 0;
[代码]
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> dp(3,0);
dp[0] = -prices[0];
dp[1] = 0;
dp[2] = 0;
for(int i =1; i<prices.size(); i++)
{
dp[0] = max(dp[0], dp[1]-prices[i]);
dp[1] = max(dp[1], dp[2]);
dp[2] = dp[0]+prices[i];
}
return max(dp[2],dp[1]);
}
};
买卖股票最佳时期含手续费
[题]
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费
1 <= prices.length <= 5 * 104
1 <= prices[i] < 5 * 104
0 <= fee < 5 * 104
[例]
输入: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
[思路]
此题和买卖股票的最佳时机Ⅱ类似,但是多了一个条件;每次卖出都要收一笔手续费。所以状态转移方程就要相对的做出改变。
dp数组设计为:
vector<int> dp(2,0)
.本题依旧只有2种状态,所以dp数组定义不变。状态转移方程: 注意dp[0]改变了,因为每次卖出股票还要交手续费。
dp[0] = max(dp[0], dp[1]+prices[i]-fee)
当前不持有股票可获得的最大价值 = max(之前买卖(不持有)股票可获得最大价值, 有股票但在当前卖出股票除去手续费可获得的最大价值)
dp[1] = max(dp[1], p[0]-prices[i])
当前持有股票可获得的最大价值 = max(之前持有股票可获得的最大值,之前不持有股票但买下当前股票获得的价值)初始化: 从dp数组的状态转移方程可知,当前股票的状态与上一次的股票状态相关,所以要初始化一个股票的dp数组。
dp[0] = 0;
dp[1] = -prices[0];
[代码]
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int dp[2] = {0};
dp[1] = -prices[0];
dp[0] = 0;
for(int i=1; i<prices.size(); i++)
{
dp[1] = max(dp[0], dp[1]-prices[i]);
dp[0] = max(dp[1], dp[0]+prices[i]-fee);
}
return dp[0];
}
};