股票系列问题最简通解(非lc官方版本),详细注释,超好记

特别声明

本文的题解都进行了空间优化,学习本文前,请你先理解不优化空间的写法,好像也就只是加个 [i-1] 哈哈,推荐代码随想录

绝对是最好记忆的写法了(能不能理解看个人)。

站在巨人的肩膀上做题啊,太爽了

核心思路

dp[0] 表示持有,dp[1]表示卖出。

那么 dp[2] , dp[3] , 一直到 dp[n] 呢?
下标为偶数代表持有(包括 0 ),下标为奇数的代表卖出

官方术语叫状态机,本文题解也是基于状态机,空间优化前,dp 数组存储了 dp.length 天的所有交易状态,空间优化后,只用存储前一天之内所有的交易状态

补充说明

  1. 为什么从 prices 数组中取值,是用 i-1 呢?
    因为这个 i 是用来遍历 dp 数组的,并不是用来遍历 prices 数组的;如果不是压缩空间,我会写成 <dp.length 的。

  2. 如果不压缩空间,dp 数组如何定义呢?
    int[][] dp = new int[prices.length + 1][2],我比较喜欢 + 1 的这种方式(在我的对动态规划的一点总结和思考中说明了,一维的两点一样,那么股票系列问题其实也是一维的)
    就算长度取为prices.length也是能做出来的,取为[nums.length + 1],其实就是dp[0][0]dp[0][1]不用,这样dp[i][j]的下标 i 的值,就能与天数对应起来了,i 就代表第几天,dp[i][0]就代表了第 i 天持有状态的利润,dp[i][1]就代表了第 i 天卖出状态的利润。

121. 买卖股票的最佳时机

class Solution {
    
    
  public int maxProfit(int[] prices) {
    
    
    int[] dp = new int[2];
    // 记录一次交易
    dp[0] = -prices[0];
    dp[1] = 0;
    // 可以参考斐波那契问题的优化方式
    // 我们从 i=1 开始遍历数组,一共有 prices.length 天,
    // 所以是 i<=prices.length
    for (int i = 1; i <= prices.length; i++) {
    
    
      // 前一天持有;或当天买入
      dp[0] = Math.max(dp[0], -prices[i - 1]);
      // 前一天卖出; 或当天卖出, 当天要卖出,得前一天持有才行
      dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);
    }
    return dp[1];
  }
}

122. 买卖股票的最佳时机 II

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int[] dp = new int[2];
        // 0表示持有,1表示卖出
        dp[0] = -prices[0];
        dp[1] = 0;
        for(int i = 1; i <= prices.length; i++){
    
    
            // 既然不限制交易次数,那么再次买股票时,要加上之前的收益
            dp[0] = Math.max(dp[0], dp[1] - prices[i-1]);
            // 前一天卖出; 或当天卖出,当天卖出,得先持有
            dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
        }
        return dp[1];
    }
}

理解了123题后,你会发现这道题还能这样写。

既然可以进行 ∞ 次交易,那么我直接存储下这一天的所有状态就行了。

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int[] dp = new int[]; 
        // 存储∞次交易的状态就行了
        // dp[0]代表第一次买入
        dp[0] = -prices[0];
        // dp[1]代表第一次卖出
        dp[1] = 0;
        // dp[2]代表第二次买入
        dp[2] = -prices[0];
        // dp[3]代表第二次卖出
        dp[3] = 0;
		// dp[4]代表第三次买入        
        dp[4] = -prices[0];
        // dp[5]代表第三次卖出
        dp[5] = 0;
        // dp[6]代表第四次买入
        ...
        ...
        for(int i = 1; i <= prices.length; i++){
    
    
            // 要么保持不变,要么没有就买,有了就卖
            dp[0] = Math.max(dp[0], -prices[i-1]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
            // 这已经是第二次交易了,所以得加上前一次交易卖出去的利润
            dp[2] = Math.max(dp[2], dp[1] - prices[i-1]);
            dp[3] = Math.max(dp[3], dp[2] + prices[i-1]);
            // 这已经是第三次交易了,所以得加上前一次交易卖出去的利润
            dp[4] = Math.max(dp[4], dp[3] - prices[i-1]);
            dp[5] = Math.max(dp[5], dp[4] + prices[i-1]);
            // 这已经是第四次交易了,所以得加上前一次交易卖出去的利润
            ...
            ...
        }
        return dp[];
    }
}

存储下一天之内交易 ∞ 次的状态,肯定是做不到的。

那么我们购买股票的时候直接加上前一天的收益就行了(空间没优化前dp[0] = Math.max(dp[0], dp[1] - prices[i-1]) 对应的就是 dp[i][0] = Math.max(dp[i-1][0], dp[i - 1][1] - prices[i-1])

只需要知道结果,不需要知道过程。

123. 买卖股票的最佳时机 III

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int[] dp = new int[4]; 
        // 存储两次交易的状态就行了
        // dp[0]代表第一次买入
        dp[0] = -prices[0];
        // dp[1]代表第一次卖出
        dp[1] = 0;
        // dp[2]代表第二次买入
        dp[2] = -prices[0];
        // dp[3]代表第二次卖出
        dp[3] = 0;
        for(int i = 1; i <= prices.length; i++){
    
    
            // 要么保持不变,要么没有就买,有了就卖
            dp[0] = Math.max(dp[0], -prices[i-1]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i-1]);
            // 这已经是第二次交易了,所以得加上前一次交易卖出去的利润
            dp[2] = Math.max(dp[2], dp[1] - prices[i-1]);
            dp[3] = Math.max(dp[3], dp[2] + prices[i-1]);
        }
        return dp[3];
    }
}

188. 买卖股票的最佳时机 IV

看懂了 122 题目下交易 ∞ 次的解法,这道题是不是就很好理解了呀?

class Solution {
    
    
    public int maxProfit(int k, int[] prices) {
    
    
        if(prices.length == 0){
    
    
            return 0;
        }
        if(k == 0){
    
    
            return 0;
        }
        // 其实就是123题的扩展,123题只用记录2次交易的状态
        // 这里记录k次交易的状态就行了
        // 每次交易都有买入,卖出两个状态,所以要乘 2
        int[] dp = new int[2 * k];
        // 按123题解题格式那样,做一个初始化
        for(int i = 0; i < dp.length / 2; i++){
    
    
            dp[i * 2] = -prices[0];
        }
        for(int i = 1; i <= prices.length; i++){
    
    
            dp[0] = Math.max(dp[0], -prices[i - 1]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);
            // 还是与123题一样,与123题对照来看
            // 就很容易啦
            for(int j = 2; j < dp.length; j += 2){
    
    
                dp[j] = Math.max(dp[j], dp[j - 1] - prices[i-1]);
                dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i - 1]);
            }
        }
        // 返回最后一次交易卖出状态的结果就行了
        return dp[dp.length - 1];
    }
}

309. 最佳买卖股票时机含冷冻期

四种状态:看看carl哥题解

先看题目,题目说尽可能多的交易,那是不是就是无限次交易,

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int[] dp = new int[4];
        dp[0] = -prices[0];
        dp[1] = 0;
        for(int i = 1; i <= prices.length; i++){
    
    
            int temp = dp[0];
            int temp1 = dp[2];
            dp[0] = Math.max(dp[0], Math.max(dp[3], dp[1]) - prices[i-1]);
            dp[1] = Math.max(dp[1], dp[3]);
            dp[2] = temp + prices[i-1];
            dp[3] = temp1;
        }
        return Math.max(dp[3], Math.max(dp[1], dp[2]));
    }
}

更简洁的3种状态

  1. 持有股票
  2. 今天不是冷冻期,保持前一天的钱
  3. 今天是冷冻期,取前一天交易会获得的钱

这道题的 dp[1] 与其他题 dp[1] 代表的含义不一样,dp[1] 就是在今天是否进入冷冻期中取最大利润。

一句话,以今天是否会进入冷冻期,来判断前一天能获得的最大利润。

我循环只能遍历到prices.length天,那我怎么知道prices.length + 1天进入冷冻期赚钱多,还是不进入冷冻期赚钱多呢?

所以我们最后返回结果的时候取 max ,就是得等到 prices.length + 1 天的时候,看prices.length + 1天是否进入冷冻期赚钱多(prices.length 那天交易了(也即dp[2]),那么prices.length + 1 天就是冷冻期了)

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int[] dp=new int[3];
        // 0 还是代表的持有
        dp[0] = -prices[0];
        dp[1] = 0;
        // 2 只是用来记录今天交易会获得多少钱
        dp[2] = 0;
        for(int i = 1; i <= prices.length; i++) {
    
    
        	// 看到不限制交易次数,这个dp[0]是不是就很快写出来了呀
            dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]);
            dp[1] = Math.max(dp[1], dp[2]);
            // 算出今天交易会获得多少钱,
            // 要交易得先持有股票, 所以要加上dp[0] 
            dp[2] = dp[0] + prices[i - 1];
        }
        return Math.max(dp[1], dp[2]);
    }
}

714. 买卖股票的最佳时机含手续费

class Solution {
    
    
    public int maxProfit(int[] prices, int fee) {
    
    
    int[] dp = new int[2];
    dp[0] = -prices[0];
    dp[1] = 0;
    for (int i = 1; i <= prices.length; i++) {
    
    
      // 和122题一样,不限制交易次数
      dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]);
      // 最后减去手续费就行了
      dp[1] = Math.max(dp[1], dp[0] + prices[i - 1] - fee);
    }
    return dp[1];
    }
}

总结

看起来似乎有个规律,能很好地帮助记忆

怎么做不重要,AC最重要!

左边从上到下是 0 开始,右边从上往下也是从 0 开始,只不过右边从第二行开始。

如果是无限次交易,右边第一行添上 dp[1] 就行,冷冻期不适合这个规律,但冷冻期的题解也很容易记住。
在这里插入图片描述

值得注意的是,prices[i - 1] 中的这个 i - 1是会消耗性能的,我这样用是为了方便理解,理解到位之后你可以这样写来提升性能。

	// 只用改变for循环条件就行了,就可以取prices[i]了
	// 因为此时dp数组长度和prices数组长度一样了
    for (int i = 0; i < prices.length; i++) {
    
    
		
    }

都一样的,原因,前文已经讲了。

取为[nums.length + 1],其实就是dp[0][0]dp[0][1]不用

我这篇对动态规划的一点总结和思考已经总结了

猜你喜欢

转载自blog.csdn.net/lihuabeats/article/details/121509626
今日推荐