题目汇总
股票系列一共 6 道题:
LeetCode 121:最多进行 1 笔交易(k=1)【贪心】
LeetCode 122:不限交易次数(k=+inf)【二维 DP】
LeetCode 309:不限交易次数(k=+inf),但有「冷冻期」的额外条件
LeetCode 714:不限交易次数(k=+inf),但有「手续费」的额外条件
LeetCode 123:最多进行 2 笔交易(k=2)【三维 DP】
LeetCode 188:最多进行 k 次交易
贪心
LeetCode 121
只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,决定是否更新当前的最大收益。注意维护两个变量:前面的最小价格 和 当前的最大收益。
python3
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n<=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
股价有升有落,需要找出所有的升区间,计算每个升区间的价格差(峰值减去谷值)作为收益,最后把所有升区间带来的收益求和就可以了。
对于升区间 [a, b, c, d],有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此每当访问到 prices[i] 比前一天价格高,即 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中。
python3
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n<=1: return 0
profit = 0
for i in range(1, n):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
(下面还提供了本题的 DP 解法)
二维 DP
LeetCode 122
每天都有三种动作:买入(buy)、卖出(sell)、无操作(rest)。
因为不限制交易次数,因此交易次数这个因素不影响题目,不必考虑。DP Table 是二维的,两个维度分别是天数(0,1,…,n-1)和是否持有股票(1 表持有,0 表不持有)。
状态转移方程
Case 1,今天我没有股票,有两种可能:
昨天我手上就没有股票,今天不做任何操作(rest);
昨天我手上有一只股票,今天按照时价卖掉了(sell),收获了一笔钱
Case 2,今天持有一只股票,有两种可能:
昨天我手上就有这只股票,今天不做任何操作(rest);
昨天我没有股票,今天按照时价买入一只(sell),花掉了一笔钱
综上,第 i 天的状态转移方程为:
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])
注意上面的转移方程只是对某一天而言的,要求出整个 DP Table 的状态,需要对 i 进行遍历。
边界状态
观察状态转移方程,第 i 天的状态只由第 i-1 天状态推导而来,因此边界状态只需要定义 i=0(也就是第一天)即可:
dp[0][0] = 0 # 第一天没有股票,说明没买没卖,获利为0
dp[0][1] = -prices[0] # 第一天持有股票,说明买入了,花掉一笔钱
python3
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n<=1: return 0
dp = [[None, None] 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
这道题的在 LeetCode 122E 的基础上添加了冷冻期的要求,即每次 sell 之后要等一天才能继续交易。状态转移方程要做修改,如果第 i 天选择买入股票,状态要从第 i-2 的转移,而不是 i-1 (因为第 i-1 天是冷冻期)。另外,由于状态转移方程中出现了 dp[i-2] 推导 dp[i-1],因此状态边界除了考虑 i=0 天,还要加上 i=1 天的状态。Solution 如下:
python
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n<=1: return 0
dp = [[None, None] for _ in range(n)]
dp[0][0] = 0
dp[0][1] = -prices[0]
dp[1][0] = max(dp[0][0], dp[0][1]+prices[1]) # 昨天就没有 或者 昨天买入今天卖出
dp[1][1] = max(dp[0][1], -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
这道题在 LeetCode 122E 的基础上添加了交易费的要求,可以理解为每次 sell 时要缴纳一定的费用。边界状态保持不变,状态转移方程需要做修改。Solution 如下:
python
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
n = len(prices)
if n<=1: return 0
dp = [[None, None] 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]
三维 DP
LeetCode 123
题目约定最多交易次数 k=2,因此交易次数必须作为一个新的维度考虑进 DP Table 里,也就是说,这道题需要三维 DP 来解决。三个维度分别为:天数 i(i=0,1,…,n-1),买入股票的次数 j(j=1,2,…,k)和是否持有股票(1 表持有,0 表不持有). 特别注意买入股票的次数为 j 时,其实隐含了另一个信息,就是卖出股票的次数为 j-1 或 j 次。
状态转移方程
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]) # 右边:今天卖了昨天持有的股票,所以两天买入股票的次数都是j
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]) # 右边:昨天没有持股而今天买入一只,故昨天买入的次数是j-1
注意上面的转移方程只是穷举了第三个维度,要求出整个 DP Table 的状态,需要对 i 和 j 进行遍历。
边界状态
观察状态转移方程知,边界状态需要考虑两个方面:i=0 和 j=0
j=0
for i in range(n):
dp[i][0][0] = 0 # 没有买入过股票,且手头没有持股,则获取的利润为0
dp[i][0][1] = -float(‘inf’) # 没有买入过股票,不可能持股,用利润负无穷表示这种不可能
i=0
for j in range(1, k+1): # 前面j=0已经赋值了,这里j从1开始
dp[0][k][0] = 0
dp[0][k][1] = -prices[0]
特别注意,上述两轮边界定义有交集——dp[0][0][0] 和 dp[0][0][1] ,后者会得到不同的结果,应以 j=0 时赋值结果为准。
python3
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n<=1: return 0
dp = [[[None, None] for _ in range(3)] for _ in range(n)] # (n, k+1, 2)
# 边界状态需要考虑:1.j=0时对i穷举; 2.i=0时对有效的j穷举(j=1,2)
for i in range(n):
dp[i][0][0] = 0
dp[i][0][1] = -float('inf')
for j in range(1, 3):
dp[0][j][0] = 0
dp[0][j][1] = -prices[0]
# 状态转移
for i in range(1, n):
for j in range(1, 3):
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1]+prices[i])
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i])
return dp[-1][-1][0]
LeetCode 188
这道题理论上和 LeetCode 123(交易次数最多为2) 的解法一样,但是直接提交容易出现超内存的错误,是 DP Table 太大导致的。
有效的交易由买入和卖出构成,至少需要两天;反之,当天买入当天卖出则视为一次无效交易。如果题目给定的最大交易次数 k<=n/2,这个 k 是可以有效约束交易次数的;如果给定的 k>n/2 ,那这个 k 实际上起不到约束作用了,可以认为 k=+inf,本题退化为 LeetCode 122(不限交易次数) 。
题目整体思路是判断 k 和 n/2 的大小关系,两个分支分别用 LeetCode 123 和 LeetCode 122 的代码解决,可有效防止内存超出。
python
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
if n <= 1: return 0
if k >= n//2: # 退化为不限制交易次数
profit = 0
for i in range(1, n):
if prices[i] > prices[i - 1]:
profit += prices[i] - prices[i - 1]
return profit
else: # 限制交易次数为k
dp = [[[None, None] for _ in range(k+1)] for _ in range(n)] # (n, k+1, 2)
for i in range(n):
dp[i][0][0] = 0
dp[i][0][1] = -float('inf')
for j in range(1, k+1):
dp[0][j][0] = 0
dp[0][j][1] = -prices[0]
for i in range(1, n):
for j in range(1, k+1):
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
return dp[-1][-1][0]
作者:cheng-cheng-16
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/gu-piao-jiao-yi-xi-lie-cong-tan-xin-dao-dong-tai-g/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。