目录
1. 买卖股票的最佳时机1-只能买卖一次(LeetCode121)
解法1:暴力解法
解法2:贪心算法
解法3:动态规划
2. 买卖股票的最佳时机2-可以买卖多次(LeetCode122)
解法1:贪心算法
解法2:动态规划
3. 买卖股票的最佳时机3-最多买卖两次(LeetCode123)
4. 买卖股票的最佳时机4-最多买卖k次(LeetCode188)
5. 买卖股票的最佳时机含冷冻期-买卖多次,卖出有一天冷冻期(LeetCode309)
6. 买卖股票的最佳时机含手续费-买卖多次,每次有手续费(LeetCode714)
前言:
- 所有的股票问题 都可以使用 动态规划 来求解(动态规划是求解股票问题的正规解法)
- 部分的股票问题 可以使用 贪心算法 来求解(贪心往往比动态规划更巧妙更好用,但是贪心只能解决部分场景)
1. 买卖股票的最佳时机1-只能买卖一次(LeetCode121)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/
解法1:暴力解法
解题思路:
- 第一层for循环:遍历买股票的位置(区间起点)
- 第二层for循环:遍历卖股票的位置(区间终点)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
int result = 0;
// 第一层for循环:遍历买股票的位置(区间起点)
for (int i = 0; i < prices.size(); i++)
{
// 第二层for循环:遍历卖股票的位置(区间终点)
for (int j = i + 1; j < prices.size(); j++)
{
result = max(result, prices[j] - prices[i]); // 打擂法求最大值
}
}
return result;
}
};
int main()
{
//vector<int> prices{ 7,1,5,3,6,4 }; // 5
vector<int> prices{ 7,6,4,3,1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
复杂度分析:
- 时间复杂度:O(n^2) LeetCode提交超时
- 空间复杂度:O(1) 无需额外的辅助空间
解法2:贪心算法
解题思路:由于股票就买卖一次,因此贪心思路为"取最左最小值,取最右最大值",得到的差值就是最大利润。
#include <iostream>
using namespace std;
#include <vector>
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;
}
};
int main()
{
vector<int> prices{ 7,1,5,3,6,4 }; // 5
//vector<int> prices{ 7,6,4,3,1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
复杂度分析:
- 时间复杂度:O(n) 需要从前向后遍历prices数组
- 空间复杂度:O(1) 需要辅助变量low,为常数级的空间
解法3:动态规划
解题思路:动态规划五部曲
- Step1:确定dp数组(一维)/dp表格(二维)、下标的含义
- dp[i][0]:第i天不持有股票,所得最多现金
- 注意:第i天不持有股票,并不表示第i天一定卖出股票,有可能是前面卖出股票,第i天仅仅是维持卖出股票的状态
- dp[i][1]:第i天持有股票,所得最多现金
- 注意:第i天持有股票,并不表示第i天一定买入股票,有可能是前面买入股票,第i天仅仅是维持买入股票的状态
- Step2:确定递推公式
- dp[i][0]:第i天不持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天保持现状 → 所得现金就是昨天不持有股票的所得现金 dp[i - 1][0]
- 第i-1天持有股票,第i天卖出股票 → 所得现金就是按照今天股票价格卖出后所得现金 dp[i - 1][1] + prices[i]
- dp[i][1]:第i天持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天买入股票 → 所得现金就是买入今天的股票后所得现金 dp[i-1][0]-prices[i] → -prices[i]
- 由于本题的限制条件为买卖一次,第i天买入股票表明前面均不能买入,因此前面不会有任何收益,即dp[i-1][0]=0
- 第i-1天持有股票,第i天保持现状 → 所得现金就是昨天持有股票的所得现金 dp[i-1][1]
- Step3:初始化dp数组
- 从递推公式可以看出,i是由i-1推导而来,因此需要初始化第一个元素
- dp[0][0]:第0天不持有股票,所得最多现金为0
- dp[0][1]:第0天持有股票(表明买入股票),所得最多现金为-prices[0]
- Step4:确定遍历顺序
- 从递推公式可以看出,i是由i-1推导而来,因此需要从前向后遍历
- Step5:打印dp数组(调试)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
// Step3:初始化dp数组
vector<vector<int>> dp(prices.size(), vector<int>(2,0));
dp[0][0] = 0;
dp[0][1] = -prices[0];
// Step4:确定遍历顺序
for (int i = 1; i < prices.size(); i++)
{
// Step2:确定递推公式
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(-prices[i], dp[i - 1][1]);
}
return dp[prices.size() - 1][0]; // 不持有股票状态 一定比 持有股票状态 所得金钱多
}
};
int main()
{
//vector<int> prices{ 7,1,5,3,6,4 }; // 5
vector<int> prices{ 7,6,4,3,1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
手动模拟dp数组的执行过程:prices = [ 7,1,5,3,6,4 ]
dp[i][0] dp[i][1]
i=0 0 -7
i=1 0 -1
i=2 4 -1
i=3 4 -1
i=4 5 -1
i=5 5 -1
2. 买卖股票的最佳时机2-可以买卖多次(LeetCode122)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/
题目简述:
- 允许多次买卖
- 任何时候 最多 只能持有 一股 股票(即必须在再次购买前出售掉之前的股票)
解法1:贪心算法
解题思路:
- 收集每天的正利润(局部最优) → 最大利润(全局最优)
- 收集正利润的区间 → 股票买卖的区间(非题目要求,并不关心哪一天买入,哪一天卖出)
#include <iostream>
using namespace std;
#include <vector>
class Solution {
public:
int maxProfit(vector<int>& prices)
{
// Step0:定义变量(准备工作)
int max_profit = 0; // 存储最终结果(最大利润)
int cur_profit = 0; // 存储每天的正利润
for (int i = 1; i < prices.size(); i++)
{
// Step1:计算每天的正利润
cur_profit = prices[i] - prices[i - 1];
// Step2:更新最终结果 三种方式(由简到难)
max_profit += max(cur_profit, 0); // max操作
// max_profit += cur_profit > 0 ? cur_profit : 0; // 三目运算符
// if (cur_profit > 0) max_profit += cur_profit; // if语句
}
return max_profit;
}
};
int main()
{
vector<int> prices{ 7,1,5,3,6,4 };
//vector<int> prices{ 1,2,3,4,5 };
//vector<int> prices{ 7,6,4,3,1 };
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
示例1(非单调): 7 1 5 3 6 4
正利润: -6 4 -2 3 -2 → 最大利润为4+3=7
正利润区间: [1,2] [3,4] → [1,2]表示第1天买入,第2天卖出(天数从0开始计数)
买卖方式:第1天买入,第2天卖出;第3天买入,第4天卖出 此时可以获得最大利润
示例2(单调递增): 1 2 3 4 5
正利润: 1 1 1 1 → 最大利润为1+1+1+1=4
正利润区间: [0,1] [1,2] [2,3] [3,4]
买卖方式1:第0天买入,第1天卖出;第1天买入,第2天卖出;第2天买入,第3天卖出;第3天买入,第4天卖出 此时可以获得最大利润
买卖方式2(合并连续区间):第0天买入,第4天卖出 此时可以获得最大利润
示例3(单调递减): 7 6 4 3 1
正利润: -1 -1 -1 -2 → 最大利润为0
正利润区间:无
买卖方式:不参与交易
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
注意事项:
- 易错点1:max_profit初始化为0?还是初始化为INT_MIN?
- max_profit应该初始化0,此时即便没有参与交易,利润应该为0,而不是负无穷
- 易错点2:i从0开始?还是从1开始?
- i应该从1开始,因为第0天没有利润,至少要第1天才会有利润,正利润的序列比股票序列少一天!
解法2:动态规划
解题思路:
- Step1:确定dp数组(一维)/dp表格(二维)、下标的含义
- dp[i][0]:第i天不持有股票,所得最多现金
- dp[i][1]:第i天持有股票,所得最多现金
- Step2:确定递推公式
- dp[i][0]:第i天不持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天保持现状 → 所得现金就是昨天不持有股票的所得现金 dp[i - 1][0]
- 第i-1天持有股票,第i天卖出股票 → 所得现金就是按照今天股票价格卖出后所得现金 dp[i - 1][1] + prices[i]
- dp[i][1]:第i天持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天买入股票 → 所得现金就是买入今天的股票后所得现金 dp[i-1][0]-prices[i]
- 注意:这是本题与"买卖股票的最佳时机-只能买卖一次"的唯一区别(买入股票时,可能会有之前买卖的利润)
- 由于本题能够买卖多次,尽管第i-1天不持有股票,但是前面仍然可能发生过交易,导致前面可能会产生收益,即dp[i-1][0]不一定等于0
- 第i-1天持有股票,第i天保持现状 → 所得现金就是昨天持有股票的所得现金 dp[i-1][1]
- Step3:初始化dp数组
- 从递推公式可以看出,i是由i-1推导而来,因此需要初始化第一个元素
- dp[0][0]:第0天不持有股票,所得最多现金为0
- dp[0][1]:第0天持有股票(表明买入股票),所得最多现金为-prices[0]
- Step4:确定遍历顺序
- 从递推公式可以看出,i是由i-1推导而来,因此需要从前向后遍历
- Step5:打印dp数组(调试)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < prices.size(); i++)
{
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]); // 唯一区别
}
return dp[prices.size() - 1][0]; // 不持有股票状态 一定比 持有股票状态 所得金钱多
}
};
int main()
{
vector<int> prices{ 1,2,3,4,5 }; // 4
//vector<int> prices{ 7,6,4,3,1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
3. 买卖股票的最佳时机3-最多买卖两次(LeetCode123)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/
解题思路: 动态规划五部曲
- Step1:确定dp数组(一维)/dp表格(二维)、下标的含义
- dp[i][0]:第i天 没有操作,所得最多现金(也可以不设置这个状态)
- dp[i][1]:第i天 第一次持有股票,所得最多现金
- dp[i][2]:第i天 第一次不持有股票,所得最多现金
- dp[i][3]:第i天 第二次持有股票,所得最多现金
- dp[i][4]:第i天 第二次不持有股票,所得最多现金
- Step2:确定递推公式
- dp[i][0]:第i天 没有操作
- 第i-1天 没有操作,第i天保持前一天的状态 → dp[i - 1][0]
- dp[i][1]:第i天 第一次持有股票
- 第i-1天没有操作,第i天第一次买入股票 → dp[i-1][0]-prices[i]
- 第i-1天第一次持有股票,第i天保持前一天的状态 → dp[i-1][1]
- dp[i][2]:第i天 第一次不持有股票
- 第i-1天第一次持有股票,第i天第一次卖出股票 → dp[i-1][1]+prices[i]
- 第i-1天第一次不持有股票,第i天保持前一天的状态 → dp[i-1][2]
- dp[i][3]:第i天 第二次持有股票
- 第i-1天第一次不持有股票,第i天第二次买入股票 → dp[i-1][2]-prices[i]
- 第i-1天第二次持有股票,第i天保持前一天的状态 → dp[i-1][3]
- dp[i][4]:第i天 第二次不持有股票
- 第i-1天第二次持有股票,第i天第二次卖出股票 → dp[i-1][3]+prices[i]
- 第i-1天第二次不持有股票,第i天保持前一天的状态 → dp[i-1][4]
- Step3:初始化dp数组
- 从递推公式可以看出,i是由i-1推导而来,因此需要初始化第一个元素
- dp[0][0]:第0天 没有操作,所得最多现金为0
- dp[0][1]:第0天 第一次持有股票(表明买入股票),所得最多现金为-prices[0]
- dp[0][2]:第0天 第一次不持有股票(表明买入股票再卖出股票),所得最多现金为0(当天买入,当天卖出)
- dp[0][3]:第0天 第二次持有股票(表明买入股票再卖出股票,第二次买入股票),所得最多现金为-prices[0]
- dp[0][4]:第0天 第二次不持有股票(表明买入股票再卖出股票,第二次买入股票再卖出股票),所得最多现金为0
- Step4:确定遍历顺序
- 从递推公式可以看出,i是由i-1推导而来,因此需要从前向后遍历
- Step5:打印dp数组(调试)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for (int i = 1; i < prices.size(); i++)
{
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
dp[i][3] = max(dp[i - 1][2] - prices[i], dp[i - 1][3]);
dp[i][4] = max(dp[i - 1][3] + prices[i], dp[i - 1][4]);
}
return dp[prices.size() - 1][4];
}
};
int main()
{
vector<int> prices{ 3,3,5,0,0,3,1,4 }; // 6
//vector<int> prices{ 1,2,3,4,5 }; // 4
//vector<int> prices{ 7,6,4,3,1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
示例:prices = [ 3,3,5,0,0,3,1,4 ]
不操作 第一次持有 第一次不持有 第二次持有 第二次不持有
0 1 2 3 4
i=0 0 -3 0 -3 0
i=1 0 -3 0 -3 0
i=2 0 -3 2 -3 2
i=3 0 0 2 2 2
i=4 0 0 2 2 2
i=5 0 0 3 2 5
i=6 0 0 3 2 5
i=7 0 0 4 2 6
4. 买卖股票的最佳时机4-最多买卖k次(LeetCode188)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(int k, vector<int>& prices)
{
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 = 1; j <= 2 * k; j += 2)
{
dp[i][j] = max(dp[i - 1][j - 1] - prices[i], dp[i - 1][j]); // 第i天 第j次持有股票
dp[i][j + 1] = max(dp[i - 1][j] + prices[i], dp[i - 1][j + 1]); // 第i天 第j次不持有股票
}
}
return dp[prices.size() - 1][2 * k];
}
};
int main()
{
vector<int> prices{ 2,4,1 }; // 2
int k = 2;
//vector<int> prices{ 3,2,6,5,0,3 }; // 7
//int k = 2;
Solution s;
int ans = s.maxProfit(k, prices);
cout << ans << endl;
system("pause");
return 0;
}
5. 买卖股票的最佳时机含冷冻期-买卖多次,卖出有一天冷冻期(LeetCode309)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/
题目简述:本题 = 买卖多次 + 冷冻期 → 相对于"买卖股票的最佳时机2(买卖多次)",本题加上了一个冷冻期
解题思路:动态规划五部曲
- Step1:确定dp数组(一维)/dp表格(二维)、下标的含义
- dp[i][0]:第i天不持有股票(非冷冻期),所得最多现金
- dp[i][1]:第i天不持有股票(冷冻期),所得最多现金
- dp[i][2]:第i天持有股票,所得最多现金
- 持有股票 → 不持有股票(冷冻期) → 持有股票(非冷冻期) 每个状态只能有自身以及前一个状态推导得到(状态是环形的)
- 在不含冷冻期的买卖股票问题中,冷冻期和非冷冻期的状态 统称为 不持有股票的状态
- Step2:确定递推公式
- dp[i][0]:第i天不持有股票(非冷冻期)
- 第i-1天不持有股票(非冷冻期),第i天维持现状 → 所得现金就是昨天不持有股票(非冷冻期)的所得现金 dp[i - 1][0]
- 第i-1天不持有股票(冷冻期),第i天从冷冻期转变为非冷冻期 → 所得现金就是昨天不持有股票(冷冻期)的所得现金 dp[i - 1][1]
- dp[i][1]:第i天不持有股票(冷冻期)
- 第i-1天持有股票,第i天卖出股票,转变为冷冻期 → 所得现金就是昨天持有股票的所得现金 加上 今天卖出股票所得现金 dp[i - 1][2]+prices[i]
- 注1:第i天的冷冻期 不能由 第i-1天的冷冻期 推导得到,因为冷冻期只能持续一天
- 注2:第i天的冷冻期 只能由 第i-1天持有股票 推导得到
- dp[i][2]:第i天持有股票
- 第i-1天不持有股票(非冷冻期),第i天买入股票 → 所得现金就是昨天不持有股票(非冷冻期)的所得现金 减去 今天买入股票的所花现金 dp[i - 1][0]-prices[i]
- 第i-1天持有股票,第i天维持现状 → 所得现金就是昨天持有股票的所得现金 dp[i-1][2]
- Step3:初始化dp数组
- 从递推公式可以看出,i是由i-1推导而来,因此需要初始化第一个元素
- dp[0][0]:第0天不持有股票(非冷冻期),所得最多现金为0
- dp[0][1]:第0天不持有股票(冷冻期),所得最多现金为0
- dp[0][2]:第0天持有股票(表明买入股票),所得最多现金为-prices[0]
- Step4:确定遍历顺序
- 从递推公式可以看出,i是由i-1推导而来,因此需要从前向后遍历
- Step5:打印dp数组(调试)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
if (prices.size() == 1) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(3, 0));
dp[0][0] = 0;
dp[0][1] = 0;
dp[0][2] = -prices[0];
for (int i = 1; i < prices.size(); i++)
{
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]); // 不持有股票(非冷冻期)
dp[i][1] = dp[i - 1][2] + prices[i]; // 不持有股票(冷冻期)
dp[i][2] = max(dp[i - 1][0] - prices[i], dp[i - 1][2]); // 持有股票
}
return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
}
};
int main()
{
vector<int> prices{ 1,2,3,0,2 }; // 3
// vector<int> prices{ 1 }; // 0
Solution s;
int ans = s.maxProfit(prices);
cout << ans << endl;
system("pause");
return 0;
}
6. 买卖股票的最佳时机含手续费-买卖多次,每次有手续费(LeetCode714)
题目描述:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/
题目简述:本题 = 买卖多次 + 手续费 → 相对于"买卖股票的最佳时机2(买卖多次)",本题加上了一个手续费
解题思路:动态规划五部曲
- Step1:确定dp数组(一维)/dp表格(二维)、下标的含义
- dp[i][0]:第i天不持有股票,所得最多现金
- dp[i][1]:第i天持有股票,所得最多现金
- Step2:确定递推公式
- dp[i][0]:第i天不持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天保持现状 → 所得现金就是昨天不持有股票的所得现金 dp[i - 1][0]
- 第i-1天持有股票,第i天卖出股票 → 所得现金就是按照今天股票价格卖出后所得现金 dp[i - 1][1] + prices[i] - fee
- 注:需要在计算卖出操作的时候减去手续费(本题和"买卖股票的最佳时机2"的唯一区别)
- dp[i][1]:第i天持有股票 可以由两个状态推导出来
- 第i-1天不持有股票,第i天买入股票 → 所得现金就是买入今天的股票后所得现金 dp[i-1][0]-prices[i]
- 第i-1天持有股票,第i天保持现状 → 所得现金就是昨天持有股票的所得现金 dp[i-1][1]
- Step3:初始化dp数组
- 从递推公式可以看出,i是由i-1推导而来,因此需要初始化第一个元素
- dp[0][0]:第0天不持有股票,所得最多现金为0
- dp[0][1]:第0天持有股票(表明买入股票),所得最多现金为-prices[0]
- Step4:确定遍历顺序
- 从递推公式可以看出,i是由i-1推导而来,因此需要从前向后遍历
- Step5:打印dp数组(调试)
#include <iostream>
using namespace std;
#include <vector>
class Solution
{
public:
int maxProfit(vector<int>& prices, int fee)
{
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < prices.size(); i++)
{
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee); // 唯一区别
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
}
return dp[prices.size() - 1][0];
}
};
int main()
{
//vector<int> prices{ 1,3,2,8,4,9 }; // 8
//int fee = 2;
vector<int> prices{ 1,3,7,5,10,3 }; // 6
int fee = 3;
Solution s;
int ans = s.maxProfit(prices, fee);
cout << ans << endl;
system("pause");
return 0;
}