【leetcode-Python】-Dynamic Programming -188. Best Time to Buy and Sell Stock IV

目录

 

题目链接

题目描述

示例

解题思路

Python实现

时间复杂度与空间复杂度

参考


题目链接

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/

题目描述

给定价格数组prices,prices[i]表示一支给定股票第i天的价格。限定只能交易k次(卖掉手里的股票后才能再次购买),设计算法来计算能够获得的最大利润。

示例

输入:k=2,prices=[3,2,6,5,0,3]

输出:7

天数从0开始计数,第1天(价格为2)买入,第2天(价格为6)卖出,得到利润4。然后第4天(价格为0)买入,第5天(价格为3)卖出,得到利润3。最终得到总利润为7。

解题思路

在这个问题里我们给出当最多交易次数为k时,求买卖股票最大利润的解法。

由于新增了约束,在121、122题的基础上,增加状态k,表示至今最多进行了k次交易(已经进行的交易数的允许上限)。因此现在题目中存在三种状态:天数(索引i表示第i天,i从0取值到n-1),当前是否持股(0表示没有持股,1表示持股),至今最多进行了k次交易。我们人为定义,如果当天买入就算进行了一次交易。

因此有

dp[i][0][k]表示第i天结束后没有持股,至今最多进行了k次交易时,手中的最大现金数。

dp[i][1][k]表示第i天结束后持有股票,至今最多进行了k次交易时,手中的最大现金数。

接着,通过分析每天的“选择”(买入、卖出、无操作)确定状态转移方程:

dp[i][0][k]的取值为下面两种可能中的最大值:
(1)第i-1天没有持股,第i天无操作。这种情况下手中的最大现金数为dp[i-1][0][k]。

(2)第i-1天持股,第i天卖出。(注意,在我们的定义中,“卖出”操作不给交易次数‘计数”,因为买入的时候已经计过了)。这种情况下手中的最大现金数为dp[i-1][1][k]+prices[i]。

因此有

dp[i][0][k]  = max(dp[i-1][0][k],dp[i-1][1][k]+prices[i])

dp[i][1][k]的取值为下面两种可能中的最大值:
(1)第i-1天持股,第i天无操作。这种情况下手中的最大现金数为dp[i-1][1][k]。

(2)第i-1天没有持股,第i天买入。由于“买入”操作需要给交易次数计数,因此在i-1天最多进行了k-1次交易,这样到了第i天状态改为“最多进行了k次交易”。这种情况下手中的最大现金数为dp[i-1][1][k-1]-prices[i](第i-1天持股,且最多进行了k-1次交易时手中的现金数,减去买股票需要花的prices[i])。

因此有

dp[i][1][k]  = max(dp[i-1][1][k],dp[i-1][0][k-1]-prices[i])

下面分析base case:

由于变量j只有0、1两种状态表示是否持股,因此只需要考虑i和k这两个变量的边界条件:

i = 0时:
(1)dp[0][0][k]表示第0天不持股,最多进行了k次交易(k>=0),因此有dp[0][0][k]=0。

(2)dp[0][1][k](k>=1)表示第0天持股,最多进行了k次交易(k>=1),因此有dp[0][1][k] = -pirces[i]。

k = 0时:

(3)dp[i][0][0]表示第i天不持股,最多进行了0次交易,因此有dp[i][0][0] = 0。

(4)dp[i][1][0]表示第i天持股,最多进行了0次交易,这种情况不可能存在,因此有dp[i][1][0] = -infinity。

Python实现

在动态规划的模板里要做到对状态的穷举:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n < 1: #写之前需要考虑到prices为空的情况
            return 0
        dp = [[[0 for _ in range(k+1)] for _ in range(2)] for _ in range(n)] #len(prices)*2*k大小的三维数组
        #初始化base case i=0时的情况
        for cur_k in range(1,k+1):
            #dp[0][0][cur_k] = 0 由于初始化dp数组的时候各值已经为0了,因此这里不用再赋值
            dp[0][1][cur_k] = -prices[0]
        #初始化base case k = 0时的情况
        for i in range(n):
            dp[i][1][0] = -float('inf') #会将dp[0][1][0]重置为负无穷
        for i in range(1,n):
            for cur_k in range(1,k+1): #已经进行的交易次数的范围为[0,k]
                dp[i][0][cur_k] = max(dp[i-1][0][cur_k],dp[i-1][1][cur_k]+prices[i])
                dp[i][1][cur_k] = max(dp[i-1][1][cur_k],dp[i-1][0][cur_k-1]-prices[i])
        return dp[n-1][0][k] #返回最后一天无持股时,至今最多进行了k次交易时的最大利润

时间复杂度与空间复杂度

时间复杂度为O(NK),n为价格序列的长度,k为给定的参数。

空间复杂度为O(NK)。

接下来进行优化:

1、给定的参数k表示最多能够进行的交易数,虽然我们仅在“买入”操作时给交易次数计数,但是在获得最大利润的计算过程中,有“买入”一定会伴随着之后的“卖出”,且买入价格低于卖出价格,因此一次交易实际上会占用两天。那么如果股票价格数组长度为n,k的限制要想有效,需要满足2*k(买入、卖出的天数)<= n。因此如果给定的最多交易次数限制k>=n/2,就相当于没有限制,即等价于k等于正无穷的情况:【leetcode-Python】-Dynamic Programming -122. Best Time to Buy and Sell Stock II,因此代码可以进一步优化为:

class Solution:
    #贪心法
    def maxProfit_kinfinity(self,prices):
        result = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                result += (prices[i]-prices[i-1])
        return result 
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n < 1: #写之前需要考虑到prices为空的情况
            return 0
        if k >= n // 2: #如果k >= n//2,那么k相当于无穷大
            return self.maxProfit_kinfinity(prices)
        dp = [[[0 for _ in range(k+1)] for _ in range(2)] for _ in range(n)] #len(prices)*2*k大小的三维数组
        #初始化base case i=0时的情况
        for cur_k in range(1,k+1):
            #dp[0][0][cur_k] = 0 由于初始化dp数组的时候各值已经为0了,因此这里不用再赋值
            dp[0][1][cur_k] = -prices[0]
        #初始化base case k = 0时的情况
        for i in range(n):
            dp[i][1][0] = -float('inf') #会将dp[0][1][0]重置为负无穷
        for i in range(1,n):
            for cur_k in range(1,k+1): #已经进行的交易次数的范围为[0,k]
                dp[i][0][cur_k] = max(dp[i-1][0][cur_k],dp[i-1][1][cur_k]+prices[i])
                dp[i][1][cur_k] = max(dp[i-1][1][cur_k],dp[i-1][0][cur_k-1]-prices[i])
        return dp[n-1][0][k] #返回最后一天无持股时,至今最多进行了k次交易时的最大利润

        
        

2、由于第i天的状态仅和第i-1天的状态有关,因此可以去掉一维,用二维数组来保存并更新状态,能够进一步将空间复杂度降到O(K)。

注意在更新数组的双重循环中,交易次数cur_k需要逆序,因为在去掉“天数“这个维度之前,dp[i][1][cur_k]的更新需要用到dp[i-1][0][cur_k-1],去掉第一维后,dp[i-1][0][cur_k-1]为上一轮更新的dp[0][cur_k-1],即当前待更新的dp[1][cur_k]会用到上一次循环中被更新的dp[0][cur_k-1](因此dp[0][cur_k-1]的更新应该排在dp[1][cur_k]的后面)

同时注意cur_k的范围为(0,k],cur_k = 0是边界条件。dp[0][0] = 0,dp[1][0] = -inf。

class Solution:
    #贪心法
    def maxProfit_kinfinity(self,prices):
        result = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                result += (prices[i]-prices[i-1])
        return result 
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n < 1: #写之前需要考虑到prices为空的情况
            return 0
        if k >= n // 2:
            return self.maxProfit_kinfinity(prices)
        dp = [[0 for _ in range(k+1)] for _ in range(2)] #2*k大小的二维数组
        #初始化二维数组,表示第0天的情况
        for cur_k in range(1,k+1):
            #dp[0][cur_k] = 0 由于初始化dp数组的时候各值已经为0了,因此这里不用再赋值
            dp[1][cur_k] = -prices[0]
        #第0天持股,且最多进行了的交易数k = 0这种情况不可能存在,因此初始化为负无穷
            dp[1][0] = -float('inf') 
        for i in range(1,n):
            #这里cur_k需要逆序循环,因为dp[i][1][cur_k]的更新需要用到dp[i-1][0][cur_k-1],去掉第一维后,即当前待更新的dp[1][cur_k]会用到上一次循环中被更新的dp[0][cur_k-1](因此dp[0][cur_k-1]的更新应该排在dp[1][cur_k]的后面)
            for cur_k in range(k,0,-1): #已经进行的交易次数的范围为(0,k]
                dp[0][cur_k] = max(dp[0][cur_k],dp[1][cur_k]+prices[i])
                dp[1][cur_k] = max(dp[1][cur_k],dp[0][cur_k-1]-prices[i])
        return dp[0][k] #返回最后一天无持股时,至今最多进行了k次交易时的最大利润

        

参考

https://leetcode-cn.com/circle/article/qiAgHn/

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems

https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/qi-ta-suan-fa-wen-ti/tuan-mie-gu-piao-wen-ti

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值