特别声明
本文的题解都进行了空间优化,学习本文前,请你先理解不优化空间的写法,好像也就只是加个 [i-1] 哈哈,推荐代码随想录。
绝对是最好记忆的写法了(能不能理解看个人)。
站在巨人的肩膀上做题啊,太爽了
核心思路
dp[0] 表示持有,dp[1]表示卖出。
那么 dp[2] , dp[3] , 一直到 dp[n] 呢?
下标为偶数代表持有(包括 0 ),下标为奇数的代表卖出
官方术语叫状态机
,本文题解也是基于状态机
,空间优化前,dp 数组存储了 dp.length
天的所有交易状态,空间优化后,只用存储前一天之内所有的交易状态
补充说明
-
为什么从 prices 数组中取值,是用 i-1 呢?
因为这个 i 是用来遍历 dp 数组的,并不是用来遍历 prices 数组的;如果不是压缩空间,我会写成<dp.length
的。 -
如果不压缩空间,dp 数组如何定义呢?
int[][] dp = new int[prices.length + 1][2]
,我比较喜欢 + 1 的这种方式(在我的对动态规划的一点总结和思考中说明了,一维的两点一样,那么股票系列问题其实也是一维的)
就算长度取为prices.length
也是能做出来的,取为[nums.length + 1]
,其实就是dp[0][0]
和dp[0][1]
不用,这样dp[i][j]
的下标 i 的值,就能与天数对应起来了,i 就代表第几天,dp[i][0]
就代表了第 i 天持有状态的利润,dp[i][1]
就代表了第 i 天卖出状态的利润。
121. 买卖股票的最佳时机
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[2];
// 记录一次交易
dp[0] = -prices[0];
dp[1] = 0;
// 可以参考斐波那契问题的优化方式
// 我们从 i=1 开始遍历数组,一共有 prices.length 天,
// 所以是 i<=prices.length
for (int i = 1; i <= prices.length; i++) {
// 前一天持有;或当天买入
dp[0] = Math.max(dp[0], -prices[i - 1]);
// 前一天卖出; 或当天卖出, 当天要卖出,得前一天持有才行
dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);
}
return dp[1];
}
}
122. 买卖股票的最佳时机 II
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[2];
// 0表示持有,1表示卖出
dp[0] = -prices[0];
dp[1] = 0;
for(int i = 1; i <= prices.length; i++){
// 既然不限制交易次数,那么再次买股票时,要加上之前的收益
dp[0] = Math.max(dp[0], dp[1] - prices[i-1]);
// 前一天卖出; 或当天卖出,当天卖出,得先持有
dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
}
return dp[1];
}
}
理解了123题后,你会发现这道题还能这样写。
既然可以进行 ∞ 次交易,那么我直接存储下这一天的所有状态就行了。
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[∞];
// 存储∞次交易的状态就行了
// dp[0]代表第一次买入
dp[0] = -prices[0];
// dp[1]代表第一次卖出
dp[1] = 0;
// dp[2]代表第二次买入
dp[2] = -prices[0];
// dp[3]代表第二次卖出
dp[3] = 0;
// dp[4]代表第三次买入
dp[4] = -prices[0];
// dp[5]代表第三次卖出
dp[5] = 0;
// dp[6]代表第四次买入
...
...
for(int i = 1; i <= prices.length; i++){
// 要么保持不变,要么没有就买,有了就卖
dp[0] = Math.max(dp[0], -prices[i-1]);
dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
// 这已经是第二次交易了,所以得加上前一次交易卖出去的利润
dp[2] = Math.max(dp[2], dp[1] - prices[i-1]);
dp[3] = Math.max(dp[3], dp[2] + prices[i-1]);
// 这已经是第三次交易了,所以得加上前一次交易卖出去的利润
dp[4] = Math.max(dp[4], dp[3] - prices[i-1]);
dp[5] = Math.max(dp[5], dp[4] + prices[i-1]);
// 这已经是第四次交易了,所以得加上前一次交易卖出去的利润
...
...
}
return dp[∞];
}
}
存储下一天之内交易 ∞ 次的状态,肯定是做不到的。
那么我们购买股票的时候直接加上前一天的收益就行了(空间没优化前dp[0] = Math.max(dp[0], dp[1] - prices[i-1])
对应的就是 dp[i][0] = Math.max(dp[i-1][0], dp[i - 1][1] - prices[i-1])
)
只需要知道结果,不需要知道过程。
123. 买卖股票的最佳时机 III
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[4];
// 存储两次交易的状态就行了
// dp[0]代表第一次买入
dp[0] = -prices[0];
// dp[1]代表第一次卖出
dp[1] = 0;
// dp[2]代表第二次买入
dp[2] = -prices[0];
// dp[3]代表第二次卖出
dp[3] = 0;
for(int i = 1; i <= prices.length; i++){
// 要么保持不变,要么没有就买,有了就卖
dp[0] = Math.max(dp[0], -prices[i-1]);
dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
// 这已经是第二次交易了,所以得加上前一次交易卖出去的利润
dp[2] = Math.max(dp[2], dp[1] - prices[i-1]);
dp[3] = Math.max(dp[3], dp[2] + prices[i-1]);
}
return dp[3];
}
}
188. 买卖股票的最佳时机 IV
看懂了 122 题目下交易 ∞ 次的解法,这道题是不是就很好理解了呀?
class Solution {
public int maxProfit(int k, int[] prices) {
if(prices.length == 0){
return 0;
}
if(k == 0){
return 0;
}
// 其实就是123题的扩展,123题只用记录2次交易的状态
// 这里记录k次交易的状态就行了
// 每次交易都有买入,卖出两个状态,所以要乘 2
int[] dp = new int[2 * k];
// 按123题解题格式那样,做一个初始化
for(int i = 0; i < dp.length / 2; i++){
dp[i * 2] = -prices[0];
}
for(int i = 1; i <= prices.length; i++){
dp[0] = Math.max(dp[0], -prices[i - 1]);
dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);
// 还是与123题一样,与123题对照来看
// 就很容易啦
for(int j = 2; j < dp.length; j += 2){
dp[j] = Math.max(dp[j], dp[j - 1] - prices[i-1]);
dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i - 1]);
}
}
// 返回最后一次交易卖出状态的结果就行了
return dp[dp.length - 1];
}
}
309. 最佳买卖股票时机含冷冻期
四种状态:看看carl哥题解
先看题目,题目说尽可能多的交易,那是不是就是无限次交易,
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[4];
dp[0] = -prices[0];
dp[1] = 0;
for(int i = 1; i <= prices.length; i++){
int temp = dp[0];
int temp1 = dp[2];
dp[0] = Math.max(dp[0], Math.max(dp[3], dp[1]) - prices[i-1]);
dp[1] = Math.max(dp[1], dp[3]);
dp[2] = temp + prices[i-1];
dp[3] = temp1;
}
return Math.max(dp[3], Math.max(dp[1], dp[2]));
}
}
更简洁的3种状态
- 持有股票
- 今天不是冷冻期,保持前一天的钱
- 今天是冷冻期,取前一天交易会获得的钱
这道题的 dp[1] 与其他题 dp[1] 代表的含义不一样,dp[1] 就是在今天是否进入冷冻期中取最大利润。
一句话,以今天是否会进入冷冻期,来判断前一天能获得的最大利润。
我循环只能遍历到prices.length
天,那我怎么知道prices.length + 1
天进入冷冻期赚钱多,还是不进入冷冻期赚钱多呢?
所以我们最后返回结果的时候取 max ,就是得等到 prices.length + 1
天的时候,看prices.length + 1
天是否进入冷冻期赚钱多(prices.length
那天交易了(也即dp[2]),那么prices.length + 1
天就是冷冻期了)
class Solution {
public int maxProfit(int[] prices) {
int[] dp=new int[3];
// 0 还是代表的持有
dp[0] = -prices[0];
dp[1] = 0;
// 2 只是用来记录今天交易会获得多少钱
dp[2] = 0;
for(int i = 1; i <= prices.length; i++) {
// 看到不限制交易次数,这个dp[0]是不是就很快写出来了呀
dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]);
dp[1] = Math.max(dp[1], dp[2]);
// 算出今天交易会获得多少钱,
// 要交易得先持有股票, 所以要加上dp[0]
dp[2] = dp[0] + prices[i - 1];
}
return Math.max(dp[1], dp[2]);
}
}
714. 买卖股票的最佳时机含手续费
class Solution {
public int maxProfit(int[] prices, int fee) {
int[] dp = new int[2];
dp[0] = -prices[0];
dp[1] = 0;
for (int i = 1; i <= prices.length; i++) {
// 和122题一样,不限制交易次数
dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]);
// 最后减去手续费就行了
dp[1] = Math.max(dp[1], dp[0] + prices[i - 1] - fee);
}
return dp[1];
}
}
总结
看起来似乎有个规律,能很好地帮助记忆
怎么做不重要,AC最重要!
左边从上到下是 0 开始,右边从上往下也是从 0 开始,只不过右边从第二行开始。
如果是无限次交易,右边第一行添上 dp[1] 就行,冷冻期不适合这个规律,但冷冻期的题解也很容易记住。
值得注意的是,prices[i - 1]
中的这个 i - 1
是会消耗性能的,我这样用是为了方便理解,理解到位之后你可以这样写来提升性能。
// 只用改变for循环条件就行了,就可以取prices[i]了
// 因为此时dp数组长度和prices数组长度一样了
for (int i = 0; i < prices.length; i++) {
}
都一样的,原因,前文已经讲了。
取为
[nums.length + 1]
,其实就是dp[0][0]
和dp[0][1]
不用
我这篇对动态规划的一点总结和思考已经总结了