目录
题目链接
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次交易时的最大利润