动态规划,股票至多买卖两次,怎么求? | LeetCode:123.买卖股票最佳时机III_哔哩哔哩_bilibili动态规划来决定最佳时机,至多可以买卖K次!| LeetCode:188.买卖股票最佳时机4_哔哩哔哩_bilibili
123.买卖股票的最佳时机III
思路:首先我给出 121 和 122 两题中使用dp策略时进行递推的循环。
// 121 买卖股票最佳时机|
dp[0][1] = -prices[0];
dp[0][0] = 0;
for(int i=1; i<n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
// 122 买卖股票最佳时机||
dp[0][0] = 0;
dp[0][1] = -prices[0]; // 由于可以多次买入,所以这里可以直接赋值为-prices[0]
for(int i=1; i<n; i++){
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
}
从中我们可以看到不持有状态的dp[i][0]总是依赖dp[i-1][1],同理dp[i][1]也是依赖dp[i-1][0]。但将122的递推公式直接使用至121中,就会报错,原先我所理解的就是在121中,-prices[i]前省略了0;而122中因为是可多次购买,所以只需要站在前一天(或者前一次)不持有的基础上进行买入即可。(记住这一点,非常的重要!!!)
// 时间复杂度O(n)
// 空间复杂度O(n*2*3)
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][][] dp = new int[n][2][3];
// 初始化
// 第一次不持有股票
dp[0][0][0] = 0;
dp[0][1][1] = -prices[0];
dp[0][0][1] = 0; // 买了又卖
dp[0][1][2] = -prices[0];
dp[0][0][2] = 0; // 买了又卖两次
for(int i=1; i<prices.length; i++){
dp[i][0][1] = Math.max(dp[i-1][0][1], dp[i-1][1][1]+prices[i]); // sell_1
dp[i][1][1] = Math.max(dp[i-1][1][1], -prices[i]); // buy_1
dp[i][0][2] = Math.max(dp[i-1][0][2], dp[i-1][1][2]+prices[i]); // sell_2
dp[i][1][2] = Math.max(dp[i-1][1][2], dp[i-1][0][1]-prices[i]); // buy_2
}
return Math.max(0, Math.max(dp[n-1][0][2], dp[n-1][0][1]));
}
}
带着121,122的求解思路,我们来看123,最多购买两次,也就可以可以购买0次,1次和2次。这里我想到既然可以使用增加一维来表征不同操作的状态,那么是否再增加一维来表示交易的次数。显然是可以的。并且因为是两次,所以我直接的对递推过程的四个状态进行了罗列。显然,求解123我们需要明确一点,即
- 交易一次 ← 0次买入
- 交易两次 ← 1次买入
所以我们将买入的次数可以等效为交易的次数。而结合每天有两种状态——{持有,不持有},那么共有四种状态。
- 买入1次,卖出0次 ← 买入0次,卖出0次 ;
- 买入1次,卖出1次 ← 买入1次,卖出0次 ;
- 买入2次,卖出1次 ← 买入1次,卖出1次 ;
- 买入2次,卖出2次 ← 买入2次,卖出1次 ;
所以我们增加一个维度模拟的交易的次数完全可以解题。现在我们清楚了递归过程有几种状态之间的转换,但是我们还没有明确各自是怎么进行公式化。
我们设定dp[i][0][k]为第i天交易k次不持有股票的最大收益,这是dp数组的含义。 现在这里有一点非常的关键,就是在dp声明的时候,dp的维度表示的为dp[n][2][k+1]第三个维度是k+1.
为什么?因为我们所有的四种状态开始递推前,需要依赖一种 “买入0次,卖出0次”,即交易了0次。而交易了0次时的收益就是0,所以在121中,dp[i-1][1]在更新时的第二项,省略了一个0;而在122中,是由Java自带的为int类型得到值初始化为0,并且我们不清楚到底需要进行多少次交易,所以 dp[i-1][0]-prices[i] 是默认省略了第三个操作的维度,如果还原,应该是dp[i-1][0][k-1]-prices[i],用来进入一次新的交易。因此,在这里,不管是121,122,123,188 递推公式执行的时候我都将持有状态先更新,不持有状态后更新,这样彼此之间的依赖关系可以更加的清晰。
剩下的关键操作就是初始化,题目中给出一定要先买入,再卖出;不可以同时进行多次交易,其实后者可以忽略。我们始终可以坚持就是先买入再卖出,所以不过是交易了多少次,在第1天,不买入的收益就是0,即dp[0][0][?] = 0(这个0也可以认为是买入了再卖出了,所以什么也没赚也没亏损); 而第一天也可以买入,那么收益就是dp[0][1][?]=-prices[0];然后开始遍历,递推即可,至于返回结果时,返回dp[n-1][0][k]即可,也可以按照题目的意思,将k=0,k=1, …… 对应的dp[n-1][0][?]比较取最大值。
188.买卖股票的最佳时机IV
思路:可能有的朋友是先完成IV,形成处理最多k次交易的思路后,完成III中的最多处理两次的操作。我个人是先处理的III,IV也是自然的可以得出,就是将原先的2修改为k,即可求解。
// 时间复杂度O(kn)
// 空间复杂度O(n*2*(k+1))
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
int[][][] dp = new int[n][2][k+1];
// 初始化
dp[0][0][0] = 0;
for(int t=1; t<=k; t++){
dp[0][1][t] = -prices[0];
dp[0][0][t] = 0;
}
// 开始遍历
for(int i=1; i<prices.length; i++){
for(int t=1; t<=k; t++){
dp[i][1][t] = Math.max(dp[i-1][1][t], dp[i-1][0][t-1]-prices[i]); // 持有
dp[i][0][t] = Math.max(dp[i-1][0][t], dp[i-1][1][t]+prices[i]); // 不持有
}
}
// int res = Integer.MIN_VALUE;
// for(int t=1; t<=k; t++)
// res = Math.max(dp[n-1][0][t], res);
// return Math.max(0, res);
return dp[n-1][0][k];
}
}
总结:所以针对买入股票这类题目,以及先前的打家劫舍III 那道在二叉树上进行状态传递的题目,都是引入了一种思想就是在dp数组中增加一个维度来表示不同操作后所形成的状态;而各个操作之间存在直接的因果关系,这也就是状态转移的依据。
股票问题中,我们学习到了
- 不同操作的状态的在线性dp上的使用,在滚动的dp上增加一维状态位来表示;
- 其次在求解前,①我们必须理清到底有几种状态,不同操作是引起状态转移的关键,那么不同的操作间是怎样的一个进行过程,买入股票中就是持有卖出才到不持有,不持有买入才到持有,这是每一次交易中0,1状态更新的关键。② 持有的买入关联的是上一次交易的结果,不持有的卖出就是针对当前的买入的收益加上prices[i]的结果,这一点,千万不要被121和122的递推公式带偏了,因此里面忽略了具体交易的次数。③交易次数千万不要忽略0次,也就是未买入,未卖出的初始状态;这点在121,122中也体现不出来。
- 另外重要一点就是初始化,初始化一定赋值的就是dp[0][0][?]和dp[0][1][?] 这些项,在121和122中初始化两项;在123中初始化五项,在188中初始化2*k+1项,dp[0][0][0]就是为首次买入而额外设置的一个赋值量。
- 然后递推递推公式,就是(先持有,再不持有,这点我在初始化上也进行了配套,更加清晰好理解)
dp[i][1][t] = Math.max(dp[i-1][1][t], dp[i-1][0][t-1]-prices[i]); // 持有
dp[i][0][t] = Math.max(dp[i-1][0][t], dp[i-1][1][t]+prices[i]); // 不持有
- 返回结果,因为dp之间的状态转移和继承的,所以直接返回dp[n-1][0][k]即可。