这里是力扣回顾的第三题,这道题属于121,122的进阶题,在力扣难度为“困难”,这道题我主要花了很多时间去理解动态规划的内容,所以我这里也只贴动态规划的代码。
这道题跟前两题不一样,画图可以帮助我们理解,但是对解题没有实质的帮助。
原题:点击此处
题解借鉴:点击这里
什么是动态规划?
一种说法是:把大问题分类讨论成几个小问题,小问题的最优解构成了大问题的当前问题的最优解。
其实我更喜欢另一种说法:就是打表格。
动态规划分为五个步骤:
1.思考问题是否能状态化
2.列出状态转移方程
3.状态初始化
4.输出
5.进行优化与压缩
1.思考问题是否能状态化
这道题要求一段有升有降的股票数组,要找出只买入卖出两次,利润达到最高,并记录此利润。
思考方向1 → 能否写成状态dp[i],代表在前0~i天,买入卖出两次的最高利润?
读者可以自己在草稿画一下想一下,这里的答案是不可以。
思考方向2 → 能否写成状态dp[i][k]?
代表的是在前 0~i 天,买卖 k 次,达到的最高利润。
题目也就只有这两个变量,所以设计一个三维状态不现实,所以第一步完成。
2.列出状态转移方程
列出状态是 dp[i][k] 后,我们思考该如何写状态转移方程。
这是动态规划最关键的一步
只要列出方程,代码可以写得非常简单。
我们可以想到,对于第 i 天,如果这一天是股价下降的,我们肯定不会选择在这一点卖出,所以对利润没有任何的影响。
这是第一种情况:
dp[i][k] = dp[i-1][k];
假如股价在这一点在上升,我们可以知道在这一点上有可能利润发生变化。
如何计算在这一点的利润?
我们可以用下面这条式子来表达:
dp[i][k] = price[i] - price[j] + dp[j-1][k-1]
这条式子我们用实体例子来讲可能会更好理解一点:
我们假设 i = 4 ,k =2 即第五天,买卖两次的利润;
j < i , 从 0 开始;
我们可以列出下面的情况:
- dp[4][2] = prices[4] - prices[0] ;
- dp[4][2] = prices[4] - prices[1] + dp[0][1];
- dp[4][2] = prices[4] - prices[2] + dp[1][1];
- dp[4][2] = prices[4] - prices[3] + dp[2][1];
其实到这里,我们已经可以写代码了。状态转移方程为:
为了让代码更加简洁,我们可以想。
假设 J 点就是我们要找的低谷点,那么prices[j-1] 肯定是一个比J点要高的点,因此dp[j-1][k-1]肯定与dp[j][k-1]是一致的。
因此我们可以把状态转移方程换成:
状态初始化
当 i = 0 时,dp[0][X] 全为0;
当 k = 0时,dp[X][0]全为0;
输出
下面是第一版代码:
class Solution {
public int maxProfit(int[] prices) {
int K = 2;
int length = prices.length;
if(length == 0){
return 0;
}
int[][] dp = new int[length][K+1];
// 一行一行地打表
for(int i =1 ; i<length ;i++){
for(int k = 1; k <= K ; k++){
int min = Integer.MAX_VALUE;
for(int j = 0; j<= i-1 ; j++){
min = Math.min(min,prices[j]-dp[j][k-1]);
}
dp[i][k] = Math.max(prices[i] - min,dp[i-1][k]);
}
}
return dp[length-1][K];
}
}
这是一个最简单,最朴实无华的一版动态规划代码,无论是一行一行更新,还是一列一列更新都可以。
同时我们把
求prices[i] - prices[j] + dp[j][k-1]的最大值 转换为 求:
prices[j] - dp[j][k-1] 的最小值。
如果到这里,你还是搞不明白为什么这段动态规划代码能work,我的建议是可以举一个实例,从头到尾列一次表格。理解到位后,再进行下一步的动态规划代码压缩;
在这里由于博主的懒惰+不会制表,就不把表格打出来了。(哈哈,同时锻炼一下你们画表)
代码的优化与压缩
看到这里,你要知道上面的代码是怎么运行的,这样你才能更好地了解到下面的优化。
观察上面第一版代码。
for(int j = 0; j<= i-1 ; j++){
min = Math.min(min,prices[j]-dp[j][k-1]);
}
我们发现这一段反反复复在计算,算出来的答案都是一模一样的。
因此我们能用一个min变量,用于记录最小的值,这样每次减少了一次for循环,因此代码可以改成如下:
class Solution {
public int maxProfit(int[] prices) {
int K = 2;
int length = prices.length;
if(length == 0){
return 0;
}
int[][] dp = new int[length][K+1];
// 一列一列地打表
for(int k = 1; k <= K ; k++){
int min = prices[0];
for(int i =1 ; i<length ;i++){
min = Math.min(min,prices[i-1]-dp[i-1][k-1]);
dp[i][k] = Math.max(prices[i] - min,dp[i-1][k]);
}
}
return dp[length-1][K];
}
}
请注意!这里从一开始的一行一行打表,转变成了一列一列打表!
要一行一行打表也非常简单,怎么做呢?(大家可以在这里思考一下,因为这里真的很坑!)
class Solution {
public int maxProfit(int[] prices) {
int K = 2;
int length = prices.length;
if(length == 0){
return 0;
}
int[][] dp = new int[length][K+1];
int[] min = new int[K+1];
for(int i =0 ; i < K+1 ; i++){
min[i] = prices[0];
}
// 一行一行地打表
for(int i =1 ; i<length ;i++){
for(int k = 1; k <= K ; k++){
min[k] = Math.min(min[k],prices[i-1]-dp[i-1][k-1]);
dp[i][k] = Math.max(prices[i] - min[k],dp[i-1][k]);
}
}
return dp[length-1][K];
}
}
这里的区别相当于加了个一个min数组,可以记录来自两列最小值。
接下来我们发现当前行的dp数组,都是可以由上一行得到的,跟下一行没有任何关系,因此我们可以把dp数组压缩为一个一维数组。
class Solution {
public int maxProfit(int[] prices) {
int K = 2;
int length = prices.length;
if(length == 0){
return 0;
}
int[] dp = new int[K+1];
int[] min = new int[K+1];
for(int i =0 ; i < K+1 ; i++){
min[i] = prices[0];
}
// 一行一行地打表
for(int i =1 ; i<length ;i++){
for(int k = 1; k <= K ; k++){
min[k] = Math.min(min[k],prices[i]-dp[k-1]);
dp[k] = Math.max(prices[i] - min[k],dp[k]);
}
}
return dp[K];
}
}
最后是究极版:
由于一维数组只有三个数,我们甚至可以压缩成非数组的普通变量。
class Solution {
public int maxProfit(int[] prices) {
int K = 2;
int length = prices.length;
if(length == 0){
return 0;
}
int temp1 = 0;
int temp2 = 0;
int min1 = prices[0];
int min2 = prices[0];
for(int i = 1; i< length; i++){
min1 = Math.min(min1,prices[i]-0);
temp1 =Math.max(temp1,prices[i] - min1) ;
min2 = Math.min(min2,prices[i] - temp1);
temp2 = Math.max(temp2,prices[i]-min2);
}
return temp2;
}
}