代码随想录算法训练营day54 || 123.买卖股票的最佳时机III,188.买卖股票的最佳时机IV

动态规划,股票至多买卖两次,怎么求? | 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我们需要明确一点,即

  1. 交易一次 ← 0次买入
  2. 交易两次 ← 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]即可。
  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值