**
通用动态规划状态分解法方法解决股票交易最大利润问题
1. 算法框架
状态的三个维度:
天数:[0, 1,…,N - 1]
交易次数(一次交易对应买+卖两个操作): [0, 1,…,k]
手上是否还持有股票:[0,1]
通用要求解答案:max(dp[N ][_][0] for _ in range(k + 1))
动作集合:买入(交易次数+1),卖出,无操作
状态转移方程:
注:交易次数按照一次买入时增加,因为买入和卖出是成对出现,要先买才能卖
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + price[i])
#第i天未持有股票的最大利润 = max(第i-1天未持有股票的最大利润, 第i-1天持有股票的最大利润 + 在第i天卖出股票得到的钱)
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - price[i])
#第i天持有股票的最大利润 = max(第i-1天持有股票的最大利润, 第i-1天未持有股票的最大利润 - 在第i天买入股票花的钱)
状态初始化:
# j=0 交易次数为0
for i in range(n):
dp[i][0][0] = 0 # 没有买入过股票,且手头没有持股,则获取的利润为0
dp[i][0][1] = float('-inf') # 没有买入过股票,不可能持股,用利润负无穷表示这种不可能
# i=0 天数为0
#正确做法
for j in range(1, k+1):
# i = 0 时,除了[0][1][1]其他状态都不存在,交易次数不可能大于1
dp[0][k][0] = float('-inf')
dp[0][k][1] = float('-inf')
dp[0][1][1] = - prices[0]
#看到leetcode上有些小伙伴的做法是为了缩减状态会将i=0时的情况写成如下:
for j in range(1, k+1):
dp[i][j][1] = - prices[i]
dp[i][j][0] = 0
#不会影响最终结果,但是仔细研究的话中间会有状态的值是错误的
2. 具体解法
股票系列一共 6 道题均可以利用上面的通用方法进行求解,只是细节上有一些变化:
- LeetCode 121:最多进行 1 笔交易(k=1)【贪心】
- LeetCode 122:不限交易次数(k=+inf)【二维 DP】
LeetCode 309:不限交易次数(k=+inf),但有「冷冻期」的额外条件
LeetCode 714:不限交易次数(k=+inf),但有「手续费」的额外条件 - LeetCode 188:最多进行 k 次交易 【三维 DP】
LeetCode 123:最多进行 2 笔交易(k=2)
LeetCode 121类:最多进行 1 笔交易【贪心】
我们只要记录当前交易日前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,决定是否更新当前的最大收益。注意维护两个变量:前面的最小价格 和 当前的最大收益。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices)<=1:
return 0
buy, profit = float('inf'), 0 # 买入值 和 利润
for curr in prices:
profit = max(profit, curr-buy)
buy = min(curr, buy)
return profit
LeetCode 122类:不限交易次数(k=+inf)【二维 DP】
方法1:贪心
找到股票每一次升落都买入卖出
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices)<=1:
return 0
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
方法2:约简通用dp算法
由于交易次数为无穷不能再影响我们的dp,所以将其从状态方程中约去
dp[i]0] = max(dp[i - 1][0], dp[i - 1][1] + price[i])
#第i天未持有股票的最大利润 = max(第i-1天未持有股票的最大利润, 第i-1天持有股票的最大利润 + 在第i天卖出股票得到的钱)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - price[i])
#第i天持有股票的最大利润 = max(第i-1天持有股票的最大利润, 第i-1天未持有股票的最大利润 - 在第i天买入股票花的钱)
边界状态约简为:
# i=0 天数为0
dp[0][0] = 0
dp[0][1] = - prices[0]
代码:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices)<=1:
return 0
dp = [[0, 0] for _ 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[-1][0]
引申: LeetCode 309:增加「冷冻期」的额外条件
状态方程改变买入股票时候要考虑冷冻期
dp[i]0] = max(dp[i - 1][0], dp[i - 1][1] + price[i])
#第i天未持有股票的最大利润 = max(第i-1天未持有股票的最大利润, 第i-1天持有股票的最大利润 + 在第i天卖出股票得到的钱)
dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - price[i])
#第i天持有股票的最大利润 = max(第i-1天持有股票的最大利润, 第i-2天未持有股票的最大利润 - 在第i天买入股票花的钱)
边界状态约简为:
# i=0 天数为0
dp[0][0] = 0
dp[0][1] = - prices[0]
dp[1][0] = max( - prices[0] + prices[1], 0)# 第0天持有股票在第一天卖出或者第0,1天无操作
dp[1][1] = max(- prices[0], - prices[1]) # 第0天持有股票或者在第1天买入
代码:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices<=1:
return 0
dp = [[0, 0] for _ in range(n)]
dp[0][0] = 0
dp[0][1] = -prices[0]
dp[1][0] = max( - prices[0] + prices[1], 0)
dp[1][1] = max(- prices[0], - prices[1])
for i in range(2, 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-2][0]-prices[i])
return dp[-1][0]
引申: LeetCode 714:增加「手续费」的额外条件
状态方程改变买入股票时候要考虑手续费
在卖出时候减手续费初始状态较122不需要改变
但注意若在买入时减fee的话初始状态 dp[0][1] = - prices[0] - fee
dp[i]0] = max(dp[i - 1][0], dp[i - 1][1] + price[i] -fee)
#第i天未持有股票的最大利润 = max(第i-1天未持有股票的最大利润, 第i-1天持有股票的最大利润 + 在第i天卖出股票得到的钱 - fee)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - price[i])
#第i天持有股票的最大利润 = max(第i-1天持有股票的最大利润, 第i-2天未持有股票的最大利润 - 在第i天买入股票花的钱)
边界状态教122未改变:
# i=0 天数为0
dp[0][0] = 0
dp[0][1] = - prices[0]
代码:
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
if len(prices)<=1:
return 0
dp = [[0, 0] for _ 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] -fee)
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
return dp[-1][0] # 返回最后一天且手上没有股票时的获利情况
LeetCode 188,123类:最多进行 k 次交易 【三维 DP】
这两道题解法通用
不同的是对于188要考虑当 k>N/2 时,就退化成 k = inf 时候的情况
class Solution:
def maxProfit_k_inf(self, prices):
if len(prices)<=1:
return 0
dp = [[0, 0] for _ 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[-1][0]
def maxProfit_k(self, k, prices):
dp = [[[0, float('-inf')] for _ in range(k + 1)] for _ in range(len(prices))]
for i in range(len(prices)):
for j in range(1, k + 1):
if i == 0:
if j == 1:
dp[0][1][1] = - prices[i]
else:
dp[i][j][1] = float('-inf')
dp[i][j][0] = float('-inf')
continue
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i])
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i])
return max(dp[len(prices) - 1][_][0] for _ in range(k + 1))
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
if k > n / 2:
return self.maxProfit_k_inf(prices)
else:
return self.maxProfit_k(k, prices)