参考:团灭 LeetCode 股票买卖问题-labuladong
dp[i][k][0]
定义:第i天,手上不持有股票,至今至多进行k次交易
状态转移方程:
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]);
base case
dp[-1][k][0] = 0;
//解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0。
dp[-1][k][1] = Integer.MIN_VALUE;
//解释:还没开始的时候,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
dp[i][0][0] = 0
//解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0。
dp[i][0][1] = -infinity
//解释:不允许交易的情况下,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
// k = 1
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][1][1]+prices[i]);
dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][0]-prices[i])
= Math.max(dp[i-1][1][1],-prices[i]);
//与k无关继续化简
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]);
//base case即 i=0
dp[0][0] = 0;
dp[0][1] = -prices[0];
完整代码
public int maxProfit_k_1(int[] prices){
int n = prices.length;
if (n==0) return 0;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
if (i==0){
dp[0][0] = 0;
dp[0][1] = -prices[0];
continue;
}
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]);
}
return dp[n-1][0];
}
注意到,状态转移⽅程的新状态只和相邻的⼀个状态有关,其实不⽤整个 dp 数组,只需要⼀个变量储存相邻的那个状态就⾜够了,这样可以把空间复杂度降到 O(1):
public int maxProfit_k_1(int[] prices){
int n = prices.length;
if (n==0) return 0;
// dp[-1][0] = 0
int dp0 = 0;
// dp[-1][1] = Integer.MIN_VALUE
int dp1 = Integer.MIN_VALUE;
for (int price : prices) {
dp0 = Math.max(dp0,dp1+price);
dp1 = Math.max(dp1,-price);
}
return dp0;
}
解析: 如果 k 为正⽆穷,那么就可以认为 k 和 k - 1 是⼀样的(与k无关,约掉k)。可以这样改写框架:
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
完整代码:
public int maxProfit_infi(int[] prices){
int n = prices.length;
if (n==0) return 0;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
if (i==0){
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}
解析: 第 i 天选择 buy 的时候,要从 i-2 的状态转移,⽽不是 i-1 。
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i]);
完整代码:
public int maxProfit(int[] prices) {
int n = prices.length;
if (n==0) return 0;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
if (i==0){
dp[0][0] = 0;
dp[0][1] = -prices[0];
continue;
}
if (i==1){
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]);
continue;
}
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i]);
}
return dp[n-1][0];
}
解析: 每次交易要⽀付⼿续费,只要把⼿续费从利润中减去即可。改写⽅程:
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]-fee);
完整代码:
public int maxProfit_fee(int[] prices, int fee) {
int n = prices.length;
if (n== 0 ) return 0;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
if (i==0){
dp[i][0] = 0;
dp[i][1] = -prices[i]-fee;
continue;
}
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]-fee);
}
return dp[n-1][0];
}
解析:
k = 2 和前⾯题⽬的情况稍微不同,因为上⾯的情况都和 k 的关系不太⼤。
要么 k 是正⽆穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 basecase 挨得近,最后也没有存在感。
所以我们必须穷举所有状态
dp[i][2][0] = Math.max(dp[i-1][2][0],dp[i-1][2][1]+prices[i]);
dp[i][2][1] = Math.max(dp[i-1][2][1],dp[i-1][1][0]-prices[i]);
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][1][1]+prices[i]);
dp[i][1][1] = Math.max(dp[i-1][1][1],-prices[i]);
完整代码
public int maxProfit_k_2(int[] prices){
int n = prices.length;
if (n==0) return 0;
int[][][] dp = new int[n][2+1][2];
for (int i = 0; i < n; i++) {
if (i==0){
dp[i][2][0] = 0;
dp[i][2][1] = -prices[i];
dp[i][1][0] = 0;
dp[i][1][1] = -prices[i];
continue;
}
dp[i][2][0] = Math.max(dp[i-1][2][0],dp[i-1][2][1]+prices[i]);
dp[i][2][1] = Math.max(dp[i-1][2][1],dp[i-1][1][0]-prices[i]);
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][1][1]+prices[i]);
dp[i][1][1] = Math.max(dp[i-1][1][1],-prices[i]);
}
return dp[n-1][2][0];
}
可降低空间复杂度
public int maxProfit_k_2(int[] prices) {
int dp_i21 = Integer.MIN_VALUE;
int dp_i20 = 0;
int dp_i11 = Integer.MIN_VALUE;
int dp_i10 = 0;
for (int price : prices) {
dp_i20 = Math.max(dp_i20,dp_i21+price);
dp_i21 = Math.max(dp_i21,dp_i10-price);
dp_i10 = Math.max(dp_i10,dp_i11+price);
dp_i11 = Math.max(dp_i11,-price);
}
return dp_i20;
}
**解析:**有了上⼀题 k = 2 的铺垫,这题应该和上⼀题的第⼀个解法没啥区别。但是出现了⼀个超内存的错误,原来是传⼊的 k 值会⾮常⼤,dp 数组太⼤了。现在想想,交易次数 k 最多有多⼤呢?
⼀次交易由买⼊和卖出构成,⾄少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作⽤了,相当于 k = +infinity。这种情况是之前解决过的。
直接把之前的代码重⽤:
public int maxProfit(int k_max, int[] prices) {
int n = prices.length;
if (n==0) return 0;
if (k_max>n/2) return maxProfit_infi(prices);
int[][][] dp = new int[n][k_max+1][2];
for (int i = 0; i < n; i++) {
for (int k = 0; k < k_max+1; k++) {
if (i==0){
dp[0][k][0] = 0;
dp[0][k][1] = -prices[0];
continue;
}
if (k==0){
dp[i][k][0] = 0;
dp[i][k][1] = -prices[i];
continue;
}
dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]);
}
}
return dp[n-1][k_max][0];
}
public int maxProfit_infi(int[] prices) {
int[] dp = new int[2];
dp[0] = 0;
dp[1] = -prices[0];
for (int price : prices) {
dp[0] = Math.max(dp[0],dp[1]+price);
dp[1] = Math.max(dp[1],dp[0]-price);
}
return dp[0];
}
⽤⼀个状态转移⽅程即可秒杀 6 道股票买卖问题
参考:团灭 LeetCode 股票买卖问题-labuladong