股票交易系列:贪心思想和动态规划

题目汇总
股票系列一共 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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值