【LeetCode】 买卖股票的最佳时机 I ~ IV

172 篇文章 4 订阅
148 篇文章 3 订阅

I、 121. 买卖股票的最佳时机 (只允许完成一笔交易)

一、题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

二、解题思路 & 代码

2.1 一次遍历

遍历一遍数组,计算每次 到当天为止 的最小股票价格和最大利润。

- [ ] List item

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        minprice = float('inf')
        maxprofit = 0
        for price in prices:
            minprice = min(minprice, price)
            maxprofit = max(maxprofit, price - minprice)
        return maxprofit

复杂度分析

  1. 时间复杂度: O ( n ) O(n) O(n),遍历了一遍数组。
  2. 空间复杂度: O ( 1 ) O(1) O(1),使用了有限的变量。
2.2 动态规划

其实方法一的思路不是凭空想象的,而是由动态规划的思想演变而来。这里介绍一维动态规划思想。

d p [ i ] dp[i] dp[i] 表示前 i i i 天的最大利润,因为我们始终要使利润最大化,则:

d p [ i ] = m a x ( d p [ i − 1 ] , p r i c e s [ i ] − m i n p r i c e ) dp[i]=max(dp[i−1],prices[i]−minprice) dp[i]=max(dp[i1],prices[i]minprice)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if n == 0: return 0 # 边界条件
        dp = [0] * n
        minprice = prices[0] 

        for i in range(1, n):
            minprice = min(minprice, prices[i])
            dp[i] = max(dp[i - 1], prices[i] - minprice)

        return dp[-1]


参考:
  1. LeetCode题解 & 讨论

======================================================

II、 122. 买卖股票的最佳时机 (尽可能地完成更多的交易)

一、题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4

二、解题思路 & 代码

2.1 贪心算法(一次遍历)

“贪心算法” 在每一步总是做出在当前看来最好的选择。

从第 i i i 天(这里 i > = 1 i >= 1 i>=1)开始,与第 i − 1 i - 1 i1 的股价进行比较,如果股价有上升(严格上升),就将升高的股价 ( p r i c e s [ i ] − p r i c e s [ i − 1 ] ) (prices[i] - prices[i- 1]) (prices[i]prices[i1]) 记入总利润,按照这种算法,得到的结果就是符合题意的最大利润。

贪心算法和动态规划相比,它既不看前面(也就是说它不需要从前面的状态转移过来),也不看后面(无后效性,后面的选择不会对前面的选择有影响),因此贪心算法时间复杂度一般是线性的,空间复杂度是常数级别的。

这道题 “贪心” 的地方在于,对于 “今天的股价 - 昨天的股价”,得到的结果有 3 种可能:(1)正数(2)0(3)负数。

贪心算法的决策是:只加正数

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0
        N = len(prices)
        for i in range(N-1):
            diff = prices[i + 1] - prices[i]
            if (diff > 0):
                res += diff
        return res

复杂度分析:

  • 时间复杂度:O(N),这里 N 表示股价数组的长度
  • 空间复杂度:O(1)。
2.2 动态规划

想到动态规划的原因是:可以用贪心算法解决的问题,一般情况下都可以用动态规划

第 1 步:定义状态

状态 d p [ i ] [ j ] dp[i][j] dp[i][j] 定义如下:

  • 第一维 i 表示索引为 i 的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;
  • 第二维 j 表示索引为 i 的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。

第 2 步:思考状态转移方程

  • 状态从持有现金(cash)开始,到最后一天我们关心的状态依然是持有现金(cash);
  • 每一天状态可以转移,也可以不动。状态转移用下图表示:
    在这里插入图片描述

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

说明:

  • 因为不限制交易次数,除了最后一天,每一天的状态可能不变化,也可能转移;
  • 写代码的时候,可以不用对最后一天单独处理,输出最后一天,状态为 0 的时候的值即可。

第 3 步:确定起始
起始的时候:

  • 如果什么都不做, d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0
  • 如果买入股票,当前收益是负数,即 d p [ 0 ] [ 1 ] = − p r i c e s [ i ] dp[0][1] = -prices[i] dp[0][1]=prices[i]

第 4 步:确定终止
终止的时候,上面也分析了,输出 d p [ l e n − 1 ] [ 0 ] dp[len - 1][0] dp[len1][0],因为一定有:
d p [ l e n − 1 ] [ 0 ] > d p [ l e n − 1 ] [ 1 ] dp[len - 1][0] > dp[len - 1][1] dp[len1][0]>dp[len1][1]

二维 dp

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        N = len(prices)
        if (N < 2):
            return 0

        # 0:持有现金
        # 1:持有股票
        # 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
        dp = [[0] * 2 for i in range(N)]

        dp[0][0] = 0
        dp[0][1] = -prices[0]

        for i in range(1, N):
            # 这两行调换顺序也是可以的
            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 dp[N - 1][0]

复杂度分析:

  • 时间复杂度:O(N),这里 N 表示股价数组的长度。
  • 空间复杂度:O(N),虽然是二维数组,但是第二维是常数,与问题规模无关。

也可以将状态数组分开设置,使得语义更明确:

二维改进版 dp

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        N = len(prices)
        if (N < 2):
            return 0

        # cash:持有现金
        # hold:持有股票
        # 状态数组
        # 状态转移:cash → hold → cash → hold → cash → hold → cash
        cash = [0] * N
        hold = [0] * N

        cash[0] = 0
        hold[0] = -prices[0]

        for i in range(1, N):
            # 这两行调换顺序也是可以的
            cash[i] = max(cash[i - 1], hold[i - 1] + prices[i])
            hold[i] = max(hold[i - 1], cash[i - 1] - prices[i])

        return cash[N - 1]

一维 dp

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        N = len(prices)
        if (N < 2):
            return 0

        # cash:持有现金
        # hold:持有股票
        # 状态转移:cash → hold → cash → hold → cash → hold → cash

        cash = 0
        hold = -prices[0]

        preCash = cash
        preHold = hold
        for i in range(1, N):
            cash = max(preCash, preHold + prices[i])
            hold = max(preHold, preCash - prices[i])

            preCash = cash
            preHold = hold

        return cash

复杂度分析:

  • 时间复杂度:O(N),这里 NN 表示股价数组的长度。
  • 空间复杂度:O(1),分别使用两个滚动变量,将一维数组状态压缩到常数。
参考:
  1. LeetCode题解1

======================================================

III、 123. 买卖股票的最佳时机 (最多可以完成 两笔 交易)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1] 
输出: 0 
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

二、解题思路 & 代码

状态定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在 [ 0 , i ] [0, i] [0,i] 区间里(这个状态依然是前缀性质的),状态为 j j j 的最大收益。 j j j 的含义如下:

  • j = 0:还未开始交易;
  • j = 1:第 1 次买入一支股票;
  • j = 2:第 1 次卖出一支股票;
  • j = 3:第 2 次买入一支股票;
  • j = 4:第 2 次卖出一支股票。

二维 dp

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        N = len(prices)
        if (N < 2):
            return 0

        # dp[i][j] ,表示 [0, i] 区间里,状态为 j 的最大收益
        # j = 0:什么都不操作
        # j = 1:第 1 次买入一支股票
        # j = 2:第 1 次卖出一支股票
        # j = 3:第 2 次买入一支股票
        # j = 4:第 2 次卖出一支股票

        # 初始化
        dp = [[0] * 5 for i in range(N)]
        dp[0][0] = 0
        dp[0][1] = -prices[0]
		
        # 3 状态都还没有发生,因此应该赋值为一个不可能的数
        #因为在更新dp[i][3]的时候可能会出现比0小的数,因为dp[i-1][1]-prices[i]
        #所以这个时候dp[i][3]不能默认是0,相反dp[i][4]就可以默认是0
        for i in range(N):
            dp[i][3] = float("-inf")

        # 状态转移只有 2 种情况:
        # 情况 1:什么都不做
        # 情况 2:由上一个状态转移过来
        for i in range(1, N):
            # j = 0 的值永远是 0
            dp[i][0] = 0
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i])
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])

        # 最大值只发生在不持股的时候,因此来源有 3 个:j = 0 ,j = 2, j = 4
        return max(0, max(dp[N - 1][2], dp[N - 1][4]))

一维 dp

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        N = len(prices)
        if N < 2:
            return 0

        dp = [0 for _ in range(5)]
        dp[0] = 0
        dp[1] = -prices[0]

        dp[3] = float('-inf')

        # dp[j]:i 表示 [0, i] 区间里,状态为 j 的最大收益
        # j = 0:什么都不操作
        # j = 1:第 1 次买入一支股票
        # j = 2:第 1 次卖出一支股票
        # j = 3:第 2 次买入一支股票
        # j = 4:第 2 次卖出一支股票

        for i in range(1, N):
            dp[0] = 0
            dp[1] = max(dp[1], - prices[i])
            dp[2] = max(dp[2], dp[1] + prices[i])
            dp[3] = max(dp[3], dp[2] - prices[i])
            dp[4] = max(dp[4], dp[3] + prices[i])
        return max(0, dp[2], dp[4])

参考:
  1. LeetCode题解

======================================================

IV、 188. 买卖股票的最佳时机 IV(最多可以完成 k k k 笔交易)

一、题目描述

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

二、解题思路 & 代码

d p [ n ] [ k ] [ 2 ] dp[n][k][2] dp[n][k][2] 这里的n表示天数

  • d p [ i ] [ j ] [ 0 ] dp[i][j][0] dp[i][j][0]:表示第 i i i 天交易了 j j j 次时卖出后的累计最大利润
  • d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1]:表示第 i i i 天交易了 j j j 次时买入后的累计最大利润

注意,当 j = = k j==k j==k 时, d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1] 已经没有意义了,因为交易了 k k k 次之后,就不能再买入了。

三维 dp

class Solution(object):
    def maxProfit(self, k, prices):	
        if not prices:
            return 0
        n = len(prices)
        # 当k非常大时转为无限次交易
        if k>n//2:
            dp0,dp1 = 0,-prices[0]
            for i in range(1,n):
                tmp = dp0
                dp0 = max(dp0,dp1+prices[i])
                dp1 = max(dp1,tmp-prices[i])
            return max(dp0,dp1)  
        # 定义三维数组,第i天、交易了多少次、当前的买卖状态    
        dp = [[[0,0] for _ in range(k+1)] for _ in range(n)]
        # 初始化第一天,这里的dp[0][k][1]可以不用管,后面也不会用到
        for i in range(k+1):
            dp[0][i][0] = 0
            dp[0][i][1] = -prices[0]
        for i in range(1,n):
            for j in range(1,k+1):
                # 处理第k次买入
                dp[i][j-1][1] = max(dp[i-1][j-1][1],dp[i-1][j-1][0]-prices[i])
                # 处理第k次卖出
                dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j-1][1]+prices[i])  
        return dp[-1][k][0]

对三维数组进行压缩,去掉最高维度的天数 n,用二维数组 d p [ k ] [ 2 ] dp[k][2] dp[k][2] 来代替。
这里的 k k k 就是交易了多少次, 2 2 2 表示买卖状态。

由于对 dp 数组进行了压缩,所以内层循环采用的是逆序遍历。
这是因为状态压缩之后,由于少了一个维度,正序遍历会出现状态覆盖的情况,所以改成了逆序遍历。

这种情况在很多dp压缩的时候都会出现,比如背包类型的dp,如果做了状态压缩,也是需要逆序遍历。

二维 dp

class Solution(object):
    def maxProfit(self, k, prices):	
        if not prices:
            return 0
        n = len(prices)
        # 当k非常大时转为无限次交易
        if k>n//2:
            dp0,dp1 = 0,-prices[0]
            for i in range(1,n):
                tmp = dp0
                dp0 = max(dp0,dp1+prices[i])
                dp1 = max(dp1,dp0-prices[i])
            return max(dp0,dp1)
        # 定义二维数组,交易了多少次、当前的买卖状态    
        dp = [[0,0] for _ in range(k+1)]
        for i in range(k+1):
            dp[i][1] = -prices[0]
        for i in range(1,n):
            for j in range(k,0,-1):
                # 处理第k次买入
                dp[j-1][1] = max(dp[j-1][1],dp[j-1][0]-prices[i])
                # 处理第k次卖出
                dp[j][0] = max(dp[j][0],dp[j-1][1]+prices[i])
        return dp[-1][0]

参考:

  1. LeetCode题解
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值