力扣周末回顾3-123题买卖股票的最佳时机 III

这里是力扣回顾的第三题,这道题属于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 开始;
我们可以列出下面的情况:

  1. dp[4][2] = prices[4] - prices[0] ;
  2. dp[4][2] = prices[4] - prices[1] + dp[0][1];
  3. dp[4][2] = prices[4] - prices[2] + dp[1][1];
  4. 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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值