代码随想录算法训练营Day49 | 121. 买卖股票的最佳时机 | 122. 买卖股票的最佳时机II

121. 买卖股票的最佳时机

题目链接 | 理论基础

贪心算法

本题只能买卖一次股票,也就是找最大的前后差:找到左边最小的,找到右边最大的,得到的最大非负差值就是最大利润。
不过虽然本题的贪心算法非常直白,要想写出 O ( n ) O(n) O(n) 的正确贪心解也不容易,具体思路有点像双指针。(即使知道是贪心也不好写啊!太笨了)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        left_lowest = float('inf')
        result = 0

        for i in range(len(prices)):
            left_lowest = min(left_lowest, prices[i])
            result = max(result, prices[i] - left_lowest)

        return result

dp 数组

贪心虽好,可不要贪心呦。对于这种题目,dp 才是无往不利的王道。

相比于之前的打家劫舍中的一维数组,股票类问题需要记录之前一步的状态。因为在打家劫舍中,当前 i 的状态可以确定地从之前的某个状态中推导得到(抢 i 那么一定不会抢 i-1,不抢 i 那么就和 i-1 的结果相同)。然而,股票问题的状态是不能通过之前的状态确定推导的:当前能否卖出股票取决于之前是否持有股票,只记录之前得到的最大收益就无从得知对于股票的持有状态。所以,本题需要靠一个二维数组来同时记录(状态,当前收益)。

  1. dp 数组的下标含义:注意这里所代表的是最大净利润,当然也可能是负数

    • dp[i][0]:如果第 i 天结束的时候不持有股票,能够得到的最大利润
    • dp[i][1]:如果第 i 天结束的时候持有股票,能够得到的最大利润
  2. dp 递推公式:

    • dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]),如果第 i 天结束时不持有股票,可能的情况有两种:
      • 前一天就没有持有股票,所以和第 i-1 天的情况完全相同,dp[i-1][0]
      • 前一天持有股票,在第 i 天内将股票卖出,dp[i-1][1] + prices[i]
    • dp[i][1] = max(dp[i-1][1], - prices[i]),如果第 i 天结束时持有股票,可能的情况有两种:
      • 前一天就持有股票,所以和第 i-1 天的情况完全相同,dp[i-1][1]
      • 前一天还未持有股票,在第 i 天内买入股票,由于本题只允许进行一次买入,那么当前的净利润必然是 - prices[i]
  3. dp 数组的初始化:从递推公式可知,我们需要初始化第一天的 dp 数组,其余值设为 0 即可(可以处理股票价格一直下跌的状态)

    • dp[0][0]:第一天开始时没有股票,第一天结束时没有股票,那么净利润还是 0
    • dp[0][1]:第一天开始时没有股票,第一天结束时持有股票,那么净利润是 -prices[0]
  4. dp 的遍历顺序:终于没有背包问题的复杂顺序哲学了,明显的从小到大遍历

  5. 举例推导:

    idp[i][0]dp[i][1]
    70-7
    10-1
    54-1
    34-1
    65-1
    45-1

值得一提的是,一般来说完成 dp 数组的遍历后,应该返回 max(dp[-1][0], dp[-1][1])。但根据简单的逻辑,到了最后一天无论如何都是手中没有股票的净利润更高,毕竟不卖股票的话肯定赚不了钱,所以返回 dp[-1][0] 即可。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # dp[i][0] represents the max profit without holding a stock at hand on day i
        # dp[i][1] represents the max profit while holding a stock at hand on day i
        dp = [[0, 0] for _ in range(len(prices))]
        # initialization
        dp[0] = [0, -prices[0]]
    
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp[i][1] = max(dp[i-1][1], - prices[i])
        
        return max(dp[-1][0], dp[-1][1])

本题的时间复杂度自然是 O ( n ) O(n) O(n),空间复杂度也是 O ( n ) O(n) O(n)

观察我们在遍历中经历的递推,很容易发现 dp[i] 只依赖于 dp[i-1],所以并不需要维护整个 n × 2 n\times2 n×2 的数组,只需要一个 2 × 2 2\times2 2×2 的滚动数组即可,依靠天数 i 对 2 的取余即可。
以下代码直接复制自代码随想录

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        length = len(prices)
        dp = [[0] * 2 for _ in range(2)] #注意这里只开辟了一个2 * 2大小的二维数组
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1, length):
            dp[i % 2][0] = max(dp[(i-1) % 2][0], -prices[i])
            dp[i % 2][1] = max(dp[(i-1) % 2][1], prices[i] + dp[(i-1) % 2][0])
        return dp[(length-1) % 2][1]

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

题目链接 | 理论基础

本题的条件和上一唯一的不同在于股票可以多次买卖了(但是同时只能持有一支股票),其余条件都一样,甚至于本题也有一种贪心解法。
这个条件上的区别导致的解法的变化在于 dp 的递推公式:dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

  • 在上一题中,只能买卖一次股票,如果第 i-1 天结束时没有持有股票,但是第 i 天结束时持有股票,那么必然是第一次购买股票,之前的利润确定是 0,此时的净利润可以确定是 -prices[i]
  • 在本题中,由于股票可以多次买卖,如果第 i-1 天结束时没有持有股票,但是第 i 天结束时持有股票,只能知道相比于之前未持有股票的时候净利润下降了 prices[i],但却不知道之前未持有股票的利润是多少,所以只能知道当前的净利润是 dp[i-1][0] - prices[i]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # dp[i][0] represents the max profit without holding the stock on day i
        # dp[i][1] represents the max profit while holding the stock on day i
        dp = [[0, 0] for _ in range(len(prices))]
        dp[0] = [0, -prices[0]]

        # dp formula
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        
        return max(dp[-1][0], dp[-1][1])
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值