题目
309. Best Time to Buy and Sell Stock with Cooldown
思路
动态规划,改成最优子结构:
方法一
buyOrSell[i]:表示第i天一定卖的最大利润
递推关系:
for (i = 1...N) {
if i <= 3 {
0...3中只能有一天买,一天卖,枚举最大max
} else {
1、中间可能有多天买卖,如只有第一天卖掉:
buyOrSell[1] + price[i] - price[3...i-1],取max
for (j = 1...i-3), 分别计算上式,取max
2、还需要计算price[i]-price[0...2],因为之前没有考虑在第0到2天买入,直接在第i天卖出,取max
}
这样就算出了buyOrSell[i],但是还要循环计算全部0...
}
方法一源码(Leetcode超时)
package cn.mitsuhide.leetcode;
public class Leetcode309 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Leetcode309 lc = new Leetcode309();
int [] prices = {3, 8, 1, 6, 5, 20};
System.out.println(lc.maxProfit(prices));
}
public int maxProfit(int[] prices) {
if (prices.length <= 1) return 0;
int N = prices.length;
int [] buyOrSell = new int [N];
int finalMax = 0;
for (int i = 1; i < N; i++) {
int max = 0;
if (i <= 3) {
for (int j = 0; j < i; j++) {
max = Math.max(max, prices[i] - prices[j]);
}
} else {
for (int j = 1; j <= i - 3; j++) {
for (int k = j + 2; k <= i - 1; k++) {
int addV = prices[i] > prices[k] ? prices[i] - prices[k] : 0;
max = Math.max(buyOrSell[j] + addV, max);
}
}
for (int j = 0; j <= 2; j++) {
max = Math.max(prices[i] - prices[j], max);
}
}
buyOrSell[i] = max;
finalMax = Math.max(finalMax, max);
}
return finalMax;
}
}
因为方法一超时,所以需要改进:
方法二(超时)
public int maxProfit(int[] prices) {
if (prices.length <= 1) return 0;
int N = prices.length;
int [][] buyOrSell = new int [N][N];
for (int j = 1; j < N; j++) {
for (int i = 0; i + j < N; i++) {
int max = 0;
if (j <= 3) {
for (int k = 0; k < j; k++) {
max = Math.max(max, prices[i + j] - prices[i + k]);
}
} else {
for (int k = i + 1; k <= i + j - 3; k++) {
int addV = prices[k] > prices[i] ? prices[k] - prices[i] : 0;
max = Math.max(addV + buyOrSell[k + 2][i + j], max);
}
max = Math.max(prices[i + j] - prices[i], max);
}
buyOrSell[i][i + j] = max;
}
}
int max = 0;
for (int i = 1; i < N; i++) {
max = Math.max(max, buyOrSell[0][i]);
}
return max;
}
方法三(超时)
public int maxProfit(int[] prices) {
if (prices.length <= 1) return 0;
int N = prices.length;
int [] buyOrSell = new int [N];
int [] min = new int[N];
int finalMax = 0;
min[0] = prices[0];
for (int i = 1; i < N; i++) {
min[i] = prices[i] < min[i - 1] ? prices[i] : min[i - 1];
}
for (int i = 1; i < N ; i++) {
if (i <= 3) {
buyOrSell[i] = prices[i] > min[i-1] ? prices[i] - min[i-1] : 0;
} else {
buyOrSell[i] = 0;
for (int j = 1; j <= i - 3; j++) {
int tMin;
if (min[i] < min[j + 1]) {
tMin = min[i];
} else {
tMin = prices[j + 2];
for (int k = j + 2; k <= i; k++) {
tMin = Math.min(tMin, prices[k]);
}
}
buyOrSell[i] = Math.max(buyOrSell[j] + prices[i] - tMin, buyOrSell[i]);
}
buyOrSell[i] = Math.max(prices[i] - min[i-1], buyOrSell[i]);
}
finalMax = Math.max(finalMax, buyOrSell[i]);
}
return finalMax;
}
Accepted
苦逼的饶了很久,答案是这么写的:
package cn.mitsuhide.leetcode;
public class Leetcode309 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Leetcode309 lc = new Leetcode309();
int [] prices = {1, 2, 4};
System.out.println(lc.maxProfit(prices));
}
public int maxProfit(int[] prices) {
if (prices.length <= 1) return 0;
int N = prices.length;
int [] sell = new int [N];
int [] coolDown = new int[N];
sell[1] = prices[1] - prices[0];
coolDown[0] = coolDown[1] = 0;
for (int i = 2; i < N; i++) {
coolDown[i] = Math.max(sell[i - 1], coolDown[i - 1]);
sell[i] = Math.max(sell[i - 1], coolDown[i - 2]) + prices[i] - prices[i - 1];
}
return Math.max(sell[N - 1], coolDown[N - 1]);
}
}
Accept的思路
假设从0…i-1天已经是买卖完成(正好把买来的卖完,从0一开始也是这样)或者是已买入,等待卖出,考虑的是第i天的状态:
1、第i天是coolDown状态,那么前一天只能是sell、coolDown、buy
2、第i天是sell状态,那么前一天只能是buy、coolDown
这里注意前一天如果是coolDown状态,那么说明再往前一定有最后一次buy状态,还未来得及卖出,正好在第i天卖出。
3、第i天是buy状态,那么前一天只能是coolDown
所以我们用三个数组来计算最大利润:
coolDown[i]:第i天是coolDown状态时的最大利润,
sell[i]:第i天是sell状态时的最大利润,
buy[i]:第i天是buy状态时的最大利润,
由此可以得到状态递推关系:
1、coolDown[i] = max(sell[i - 1],coolDown[i - 1], buy[i - 1]);
2、sell[i] = max(buy[i - 1] + price[i] - price[i - 1], coolDown[i - 1] + price[i] - price[之前最后一次的买入天]);
3、buy[i] = coolDown[i - 1];
将3式子带入1和2,得到:
1、coolDown[i] = max(sell[i - 1], coolDown[i - 1], coolDown[i - 2];
2、sell[i] = max(coolDown[i - 2] + price[i] - price[i - 1], coolDown[i - 1] + price[i] - price[之前最后一次的买入天]);
可以发现,只需要两个数组sell和coolDown就可以了。
但是从2式中看到,还需要记录之前的状态,否则计算不出“之前最后一次的买入天”。
这里也正是accept答案最巧妙的地方,一般人很难注意到2中的下式成立:
coolDown[i - 1] + price[i] - price[之前最后一次的买入天] = sell[i - 1] + price[i] - price[i - 1];
这是整个accept答案中最难想到的递推式。
这样想:
2式的意思是在第i天卖出,能卖出的最大利润是多少???其实将sell[i]和sell[i-1]作比较,会发现:
sell[i - 1]的状态:
0 ... k ... i-1, i
. ... buy ... sell, coolDown
. ... buy ... coolDown, sell
可以发现对于sell[i] 和 sell[i - 1],0…i - 2天的状态只能是完全相同的,不同的只有最后一天卖出的价格,sell[i]是以price[i]卖出,sell[i-1]是以price[i-1]卖出。
所以,sell[i]比sell[i-1]多卖出的价格 = price[i] - price[i-1], 也就是(太关键了!):
sell[i] - sell[i-1] = price[i] - price[i-1]
=>
sell[i] = sell[i-1] + price[i] - price[i-1]
这样,我们的递推式就是:
1、coolDown[i] = max(sell[i - 1], coolDown[i - 1], coolDown[i - 2];
2、sell[i] = max(coolDown[i - 2] + price[i] - price[i - 1], sell[i-1] + price[i] - price[i-1]);
答案中把2式子简化成了(太精辟导致一开始根本看不懂):
sell[i] = max(coolDown[i - 2],sell[i-1]) + price[i] - price[i-1];
在跟上初始化的值:
sell[1] = price[1] - price[0];
coolDown[0] = coolDown[1] = 0;
源码见上文。
总结
虽然这题一眼能看出是用动态规划,但是也不能乱用,一定要想清楚递推关系,找到问题的根本,设计出最简单、最快的算法!