【算法】—dp动态规划详解1

本文介绍了动态规划的基本概念,包括无后效性、最优子结构和重叠子问题,通过换硬币、不同路径和买卖股票等经典问题阐述了动态规划的求解思路,如状态转移方程、初始化和边界条件。同时,讨论了动态规划在数组空间优化上的方法,如交替数组和滚动数组。文章还探讨了多种买卖股票问题的动态规划解决方案,展示了动态规划在解决此类问题中的灵活性和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划(上)

下一章是dp的深入应用,在学习完本章dp后可以进一步了解~!
【算法】—dp动态规划详解(下)


那我们开始进行dp的学习,简单了解并学会应用这一工具吧~!


一.dp动态规划


①动态规划方法:

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,可以用来优化暴力法

💡 详细:可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果 因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法



②动态规划基本原理:

那么问题来了,什么时候能使用dp呢?

有下列三种典型应用场景:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Imhb2vio-1680691616190)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9f30a87f-31da-4bb6-9f8b-c0bf652f7d20/Untitled.png)]

使用动态规划解决的问题有个明显的特点:

💡 一旦一个子问题的求解得到结果,以后的计算过程就不会修改它


这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图,动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量

动态规划有自底向上和自顶向下两种解决问题的方式:

  1. 自顶向下即记忆化搜索
  2. 自底向上就是递推

③动态规划的性质:

  • 无后效性 :通俗的说就是只要我们得出了当前状态,而不用管这个状态怎么来的,也就是说之前的状态已经用不着了,如果我们抽象出的状态有后效性,很简单,我们只用把这个值加入到状态的表示中。

  • 最优子结构(自下而上):在决策问题中,如果,当前问题可以拆分为多个子问题,并且依赖于这些子问题,那么我们称为此问题符合子结构,而若当前状态可以由某个阶段的某个或某些状态直接得到,那么就符合最优子结构

  • 重叠子问题(自上而下):动态规划算法总是充分利用重叠子问题,通过每个子问题只解一次,把解保存在一个需要时就可以查看的表中,每次查表的时间为常数,如备忘录的递归方法,斐波那契数列的递归就是个很好的例子

  • 状态转移:在抽象出上述两点的的状态表示后,每种状态之间转移时值或者参数的变化。——>通常用数组记录

  • 状态转移方程:当前状态与之前的状态之间满足的关系式,以求解当前状态下的数值


💡 和贪心算法的区别:所以动态规划中每⼀个状态⼀定是由上⼀个状态推导出来的 ,这⼀点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的



④dp求解思路:

引例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYsSFTsy-1680691032105)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 1.png)]

在之前我们提到,这类问题,如果只是用贪心算法的思想,每次都尽可能取最大值,是无法获得最优解的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IiFVFmWd-1680691032105)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 2.png)]

思路分析:

dp问题四大件:

1. 确定状态:

💡 简单的说,解动态规划时需要开一个数组,要确定dp数组,以及下标的含义,如dp[i],dp[i][j],dp[i][j][k]

  1. 最后一步:

    • 虽然我们并不知道最优策略是什么,但最优策略肯定是K枚硬币a1,a2,…ak面值加起来为27
    • 所以一定有最后一枚硬币ak
    • 除掉这枚硬币,其他硬币加起来一定为27-ak

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-35svs1Yi-1680691032106)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 3.png)]

    这里前k-1枚硬币一定是最优策略,加上第k枚硬币才能得到最优解

    💡 定理:最优策略下的子问题也一定是最优

  2. 子问题:

    • 所以我们就要求:最少用多少枚硬币可以拼出27-ak
    • 我们将原问题(最少用多少枚硬币可以拼出27)转化为一个子问题(确定ak后,最少可以用多少枚硬币拼出27-ak),规模在不断变小

    设f(x)=最少用多少枚硬币可以拼出x:


    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jt5wVb0N-1680691032106)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 4.png)]

从而,我们引出递归解法:

def f(x):
    if x==0:
        return 0 # 0元钱只需要0枚硬币
    res=float('inf')
    if x>=2: # 当前,最后一枚硬币是2
        res=min(res,f(x-2)+1)
    if x>=5: # 当前,最后一枚硬币是5
        res=min(res,f(x-5)+1)
    if x>=7: # 当前,最后一枚硬币是7
        res=min(res,f(x-7)+1)
    return res
  • 递归算法的问题:

    根据递归生成的树可知,在递归中,存在多次重复的元素计算,当数据量大时,时间复杂度非常高

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-seK2fA9i-1680691032107)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 5.png)]

那么,应该如何解决呢?

2. 状态转移方程:

即上一层和当前层的动态变换方程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZoCkyVn-1680691032107)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 6.png)]

3. 初始条件和边界情况:

💡 初始条件:用转移方程无法计算,但又用其进行后续计算

💡 边界情况:不能让数组越界

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wTzsLUSQ-1680691032107)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 7.png)]

如:这里的f[0]需要f[-2],f[-5],f[-7]进行计算,但由定义的边界条件算出来为正无穷,则f[0]算不出,后续f[2],f[5],f[7]也算不出来,所以需要手动初始化f[0]


4. 确定计算顺序:

我们初始化了f[0]=0,则继续计算f[1],f[2],…,f[27],那么,我们在计算f[x]时,一定是在f[x-2],f[x-5],f[x-7]都已经得到结果的情况下进行的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22A3VLwH-1680691032107)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 8.png)]



二.dp数组

空间优化:滚动数组

由于dp[i][]是从上一行dp[i-1][]算出来的,因此第i行只与第i-1行有关系,跟前面的行均(因为前面的子问题已经被解决,为最优子结构)没有关系,则有状态转移方程

       dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]] + w[i])

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bonh1cCd-1680691032108)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled.jpeg)]

✅ dp(i-1,j):不用第i个数字,和为j

dp(i-1, j -w):用第i个数字,且做减法,等价于用i-1个数字实现 j -wi

dp(i-1,j+w):用第i个数字,且做加法,等价于用i-1个数字实现 j+wi


1.交替数组

二维数组:

交替数组即将问题中的所有状态分别视为一个维数,构造为二维数组,最终递推得到结果

两层循环:

  • 即对自底向上的情况逐渐递推,直至到达最顶层
  • 又在每一层中讨论不同的选取方式,找到最大/最小的情况——最优策略,继续下一层选取
N = 3011
dp = [[0 for i in range(N)] for j in range(2)]    #注意先后
w = [0 for i in range(N)] # 表示val价值
c = [0 for i in range(N)] # 表示c容量

def solve(n,C):
    now = 0 # 表示现在行
    old =1 # 表示前一行
    # 主体
		for i in range(1,n+1):
        old,now = now,old            #交换
        for j in range (0,C+1,1):
            if c[i] > j:   **# 如果要加入的物品比当前总容量还大**
                dp[now][j]=dp[old][j]  **# 不加入 直接继承上一行的数据**
            else:
								**# 如果能加入 则选取加入与不加入时价值最大值**
                dp[now][j] = max(dp[old][j], dp[old][j-c[i]]+w[i])
    
		return dp[now][C]   

n, C = map(int, input().split())  # 总的物品数和总容量
for i in range(1, n+1):  
    c[i], w[i] = map(int, input().split())  # 初始化
print(solve(n, C))


2.滚动数组

滚动一维数组:

将多个状态压缩,因为只与前一个状态有关

两层循环:

  • 即对自底向上的情况逐渐递推,直至到达最顶层
  • 又在每一层中讨论不同的选取方式,找到最大/最小的情况——最优策略,继续下一层选取
N=3011
dp = [0 for i in range(N)]
w = [0 for i in range(N)]
c = [0 for i in range(N)]

def solve(n,C):
		# 主体
    for i in range(1,n+1):
        for j in range (C,c[i]-1,-1): **# 变化1:容量由大至小遍历**
            dp[j] = max(dp[j], dp[j-c[i]]+w[i]) **# 变化2:一维数组自己滚动** 
    return dp[C]   

n, C = map(int, input().split())
for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())
print(solve(n, C))

✅ 注意:使用一维滚动数组时需要从后往前滚动递推,因为当前数据要利用到前一行及其前几列的数据,而滚动数组是一维的,若从前开始改动,则后面数据均出错,因此,要从后向前依次改变

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qQ9rkTRc-1680691032108)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 9.png)]

一定一定注意在使用滚动数组时要逆序遍历!!!



三.dp中的典型问题

1.换硬币

示例:
换硬币

即为引例中的题,直接上dp:
思路分析:

  1. 将dp初始化为∞,因为最后要取min—即要选取最优解

  2. 初始化dp[0]=0

  3. 两层for循环:

    ①要从总额为1不断动态规划至amount

    ②讨论固定总额下的不同选取情况,选取最优解

滚动数组:

class Solution:
    def coinChange(self, coins, amount):
        # write your code here
        INF = float('inf')
        dp = [INF for _ in range(amount + 1)]  # 边界条件
        dp[0] = 0 # 初始化0
        for i in range(1, amount + 1):
            for coin in coins:
                if i >= coin and dp[i - coin] != INF:
                    dp[i] = min(dp[i], dp[i - coin] + 1)

        # 如果不存任意的方案 返回-1
        if dp[amount] == INF:
            return -1
        return dp[amount]

2.不同路径

不同路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rz4W6enX-1680691032109)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 10.png)]

思路分析:

易得,题目求解满足加法原理:

最后一步:

  • 无论机器人用什么方式到达右下角,最后一步总是向下或者向右的
  • 右下角坐标为(m-1,n-1),那么机器人前一步一定在(m-2,n-1)或(m-1,n-2)

子问题:

  • 假如机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)

📌 满足加法原理:①无重复 ②无遗漏

  • 即将问题转化为求有多少种方式从左上角走到(m-1,n-2)和(m-2,n-1),缩小了问题规模

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XqnQXNBP-1680691032109)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 11.png)]

小技巧:如何判断是一维还是二维?即判断子问题所含变量的维数——如本题有x,y变量

二维数组:

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp=[[1]*n for _ in range(m)]
        for i in range(1,m):
            for j in range(1,n):
                dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[m-1][n-1]


3.买卖股票的最好时机(I~VI)

在之前讲解贪心的时候我们了解了买卖股票的贪心做法,但贪心不能解决该类的所有问题,因此,需要用动态规划进行优化,接下来的六种类型将全部用动态规划进行解决:

首先,我们先了解一些概念:

💡 持有:持有不一定是当天买入的,可能是之前买入,持有则表示今天保持拥有该股票的状态


💡持有股票的现金:因为一开始现金为0,所以,在加入第i天买入股票后的现金即为-prices[i],为负数,实际意义则表示买入该股票花费了prices[i]

💡dp数组名:用易于识别的have和no数组,同时两个数组能降低维度


①.买卖股票的最好时机I

买卖股票的最好时机I
①题目分析:

给定一个数组prices,它在第i个元素prices[i]表示一支给定股票在第i天的价格,只能选择某一天买入这只股票,并选择在未来的某一天不同的日子卖出该股票(只能进行一次交易


②确定dp数组及下标含义:

💡 dp在构造数组时最重要的就是确定状态:

状态如何确定呢?即根据题目条件判断当前时刻可能出现的情况:

  • 一.未进行任何操作
  • 二.进行过一次买入
  • 三.进行了一次买入和卖出

😄 状态定义为:1.当天的天数i; 2.当天是否持有股票——have / no

  1. have[i]:表示第i天持有股票时所获得的最多现金(为负数)
  2. no[i]:表示第i天不持有股票时所获得的最多现金

③确定状态转移方程:

  1. 第i天持有股票—have[i]:

    1. 第i-1天就已经持有该股票了,那么就保持现状(今天不买),所得现金为
      have[i]=have[i-1]
    2. 第i天买入该股票,所得现金就是买入今天得股票时所获得的现金为:0-prices[i]

    而have应该选择所得现金最大的情况,才为最优策略:

    have[i]=max(have[i-1],-prices[i])

    注意:因为手上只能有一只股票,所以若为当天买的,则之前无利润,因此为0-prices[i]

  2. 第i天不持有股票—no[i]:

    1. 第i-1天不持有股票,那么就保持现状(今天也不买),所得现金就是昨天所获得的现金:

      no[i]=no[i-1]

    2. 第i天卖出股票(说明第i-1天是持有股票的,为have[i-1]),所得现金就是按照今天股票价格卖出后所得的现金:

      prices[i]+have[i-1]

    同样,no也应该选择所得现金最大的情况,才为最优策略:

    no[i]=max(no[i-1],prices[i]+have[i-1])


④dp数组如何初始化:

由初始化的原则,当dp的项由前面无法推导时,需要手动初始化

  1. have[0]表示第1天持有股票,此时的持有股票就一定是买入股票了,因为第1天只能选择买或者跳过,因此,这里初始化:

    have[0]=-prices[0]

  2. no[0]表示第1天不持有股票,即没有买入股票,现金为0:

    no[0]=0

⑤最终结果:

最终结果即为no[i]所对应的值,因为第i天不持有股票时的值一定比持有时的多

代码实现:

class Solution:
    def maxProfit(self, prices: list[int]) -> int:
        l=len(prices)
        if l==0:
            return 0
        have=[]*l
        no=[]*l
        have[0]=-prices[0]
        no[0]=0
        for i in range(1,l):
            have[i]=max(have[i-1],-prices[i])
            no[i]=max(no[i-1],prices[i]+have[i-1])
        return no[l-1]

②.买卖股票的最好时机II

买卖股票的最好时机II
①题目分析:

在本题中,可以进行多次交易,从而得到这几天的最大利益,但同样,手里最多也只能持有一只股票,一次只能进行一次交易

②思路分析:

😄 状态定义为:1.当天的天数i; 2.当天是否持有股票——have / no

与前一题基本相同,只是对第i天持有时的情况有所不同:

💡 若第i天买入股票,还需要考虑之前几天赚到的钱

前一题不用考虑是因为只能进行一次交易,因此有如下变化:

have[i]=max(have[i-1],no[i-1]-prices[i])

代码实现:

class Solution:
    def maxProfit(self, prices: list[int]) -> int:
        l=len(prices)
        if l==0:
            return 0
        have=[0]*l
        no=[0]*l
        have[0]=-prices[0]
        no[0]=0
        for i in range(1,l):
            have[i]=max(have[i-1],no[i-1]-prices[i])
            no[i]=max(no[i-1],prices[i]+have[i-1])
        return no[l-1]

😀 总结:II其实是对I的一种延伸,从一笔交易演变到多笔交易,在第i天变化的基础上,还要加上之前可能进行的变化


③.买卖股票的最好时机III

买卖股票的最好时机III
①题目分析:

本题中最多可以完成 两笔 交易,但不能同时参与多笔交易,必须在再次购买前出售掉之前的股票,这里我们设定可以在同一天买入并在当天卖出(这一操作收益为0,不影响结果)

②确定dp数组及下标含义:

这里多了一个限制条件:最多完成2次交易,由于交易次数不确定,因此,需要增加一个状态,但又由于我们可以用命名对状态进行优化:

😄 状态定义为:1.当天的天数i; 2.当天是否持有股票——have / no; 3.交易次数 j

将是否持有股票优化为 buy,sell;

又将交易次数优化为分解为两个数组 buy(1,2),sell(1,2)

  1. 由于我们最多可以完成两笔交易,因此在第i天结束以后,我们会处于五种情况

    1. NULL:未进行任何操作
    2. buy1[i]:只进行过一次买入
    3. sell1[i]:进行了一笔交易,即一次买入和一次卖出
    4. buy2[i]:在完成了一笔交易之后,又进行了一次买入
    5. sell2[i]:进行了两次交易
  2. 由于第一个状态的利润显然未0,因此,我们可以不用将其记录;

    对于剩下几种状态,我们分别将它们的最大利润记为buy1[i],sell1[i],buy2[i],sell2[i]

③确定状态转移方程:

  1. 当处于“进行过一次买入”状态时:

    1. 第i天买入股票了,那么buy1[i]=-prices[i]
    2. 第i天没有操作,而是沿用前一天的买入状态:buy1[i]=buy[i-1]

    最优策略需要选择最大的情况:

    buy1[i]=max(buy1[i-1],-prices[i])

  2. 当处于“进行过一次交易” 状态时:

    1. 第i天卖出股票了,那么sell1[i]=buy1[i-1]+prices[i]
    2. 第i天没有卖出,交易发生在前几天:sell1[i]=sell[i-1]

    最优策略下选择:

    sell1[i]=max(sell[i-1],buy1[i-1]+prices[i])

  3. 当处于“进行过一次交易+一次买入”状态时:

    1. 第i天没有买入股票,是之前买的,那么:buy2[i]=buy2[i-1]

    2. 第i天买入股票了,由于之前已经进行过一次交易,所以

      buy2[i]=sell1[i-1]-prices[i]

    buy2[i]=max(buy2[i-1],sell1[i-1]-prices[i])

  4. 当处于"进行过两次交易”状态时:

    1. 第i天卖出股票了,那么:sell2[i]=buy2[i-1]+prices[i]
    2. 第i天没有卖出股票,交易在之前进行了,那么:sell2[i]=sell2[i-1]

    sell2[i]=max(sell2[i-1],buy2[i-1]+prices[i])


④确定dp数组如何初始化:

📢 隐含条件:无论题目中是否允许在同一天买入并卖出,最终答案都不会受到影响,因为这一操作所带来的收益为0,这是默认可以的

因此,我们对第一天进行 无聊至极的讨论:

  1. buy1[0]:第一天,只能以prices[0]的价格买入:buy1[0]=-prices[0]
  2. sell1[0]:即在第一天买入又卖出:sell1[0]=0
  3. buy2[0]:第一天,买入卖出又买入:buy2[0]=-prices[0]
  4. sell2[0]:即在第一天买入卖出又买入卖出:sell2[0]=0

⑤最终结果:

  1. 在动态规划结束后,由于我们可以进行不超过两笔交易,所以结果为:

    res=max(0,sell1[n-1],sell2[n-1])

  2. 因为对边界进行了维护,即sell1[0],sell2[0]都为0,并且在状态转移过程中维护的是最大值,因此结果一定大于等于0

  3. 最终答案其实只需输出 sell2[n-1]

    💡 即转化为证明sell2[i]=sell1[i](最优时):

如果最优策略是恰好进行一次交易时产生的,假设交易发生在第i-1天之前,则sell1[i-1]为最大值;那么现在来到第i天,则现在的状态为已经进行过一笔交易,则我们设定暂时不卖出,保留之前状态:sell1[i]=sell1[i-1](因为为最大),但又根据隐含条件,我们可以选择在当天买入并卖出(凑一笔交易),则+一次买入(现在的状态为进行过一笔交易):buy2[i]=sell1[i]-prices[i],再卖出(现在的状态为进行过一笔交易+一次买入):则sell2[i]=buy2[i]+prices[i]=sell[i],证毕


代码实现:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        if n==0:
            return 0
        buy1=[0]*n
        buy2=[0]*n
        sell1=[0]*n
        sell2=[0]*n
        buy1[0]=buy2[0]=-prices[0]
        sell1[0]=sell2[0]=0
        for i in range(1,n):
            buy1[i]=max(buy1[i-1],-prices[i])
            sell1[i]=max(buy1[i-1]+prices[i],sell1[i-1])
            buy2[i]=max(buy2[i-1],sell1[i-1]-prices[i])
            sell2[i]=max(sell2[i-1],buy2[i-1]+prices[i])
        return sell2[-1]
  • 这里注意一个小细节,不能用连等号初始化多个数组:buy1=buy2=sell1=sell2=[0]*n

    因为他们指向同一片内存空间

😀 总结:III实际上结合了I和II,在四种情况下,我们分别利用了与I和II的相似的状态转移方程,将交易次数限定在了两次:第一次可以看作是问题I,只能进行一次交易时的最大值;第二次可以看作是问题II,可以进行多次交易时的最大值


④.买卖股票的最好时机IV

买卖股票的最好时机IV
思路分析:

①确定dp数组及下标的含义:

对于dp数组的定义, 我们首先要确定一共有多少个状态:

❓ 状态暂定为:1.当天的天数i; 2.当天是否持有股票——have / no


💡 而本题的特殊之处在于:我们必须还要确定当前进行了几笔交易 j,因为前面的几道题中,k要么是1,2(可以分开为两个构造),要么是正无穷,所以我们也不需要将它单独作为一个状态,但本题就不同了,k可以取任意的值,所以它也必须作为一个状态


所以本题必须用3个状态才能完整表示dp数组, 也就是三维dp数组

但可以通过命名来优化一个状态 :

😄 状态最终定义为:1.当天的天数 i; 2.当天是否持有股票——have / no; 3.交易的次数 j

将是否持有股票优化为 have,no;

所以最终的dp数组就是二维dp数组:

  1. have[i][j]:表示进行恰好 j 笔交易, 并且当前手上持有一支股票, 这种情况下的最大利润
  2. no[i][j]:表示进行恰好 j 笔交易, 并且当前手上不持有股票, 这种情况下的最大利润
  • 我们必须还要明确一个概念:k何时进行变化——这里默认一买一卖才算完整交易(j+1)

②确定状态转移方程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kt7BeC9l-1680691032109)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 12.png)]

注意交易次数:

当前状态为持有时,是不会改变交易次数的,因为当前未卖出

而当前状态为不持有时,说明存在卖出,即递推时要注意交易次数的变化


④dp数组的初始化:

  1. 由于为二维数组,我们先设置边界have[0][0~k]=0,no[0][0~k]=0

  2. 对于have数组:

    ①have[0][1~k]表示在第0天时进行了>=1次交易,显然是不可能的,因此,我们将其初始化为一个极小值,表示不合法:have[0][1~k]=float("-inf")

    ②have[0][0]与前几题类似,初始化为 :have[0][0]= -prices[0]

  3. 对于no数组:

    no[0][0~k]均表示进行了完整的买入卖出cao’zuo,且当前不持有,所以全部初始化为:

    no[0][0~k]=0

⑤最后结果:

首先,手上不持有的一定比手上持有的所得利润高,其次,并不是交易次数越多越好,因为存在交易亏损,所以,应该为:max(no[n-1][0~k])

代码实现:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n=len(prices)
        if n==0:
            return 0
        have=[[0]*(k+1) for i in range(n)]
        no=[[0]*(k+1) for j in range(n)]
        have[0][0]=-prices[0]
        for i in range(1,k+1):
            have[0][i]=float("-inf")

        for i in range(1,n):
            for j in range(k+1):
                have[i][j]=max(have[i-1][j],no[i-1][j]-prices[i])
								**# 注意:这里j要特判,否则j-1会超出边界**
                if j!=0:
                    no[i][j]=max(no[i-1][j],have[i-1][j-1]+prices[i])
                else:
								**# j=0时,保持为0即可,因为没有利润**
                    no[i][j]=0
        return max(no[n-1])

💡 总结:本题是所有股票问题的通解问题,其他的股票问题都是建立在k的取值上进行改变的, 它们的思想都可以用本题的思想来解决


⑤.买卖股票的最好时机V

买卖股票的最好时机V
思路分析:

①确定dp数组及下标的含义:

根据前几题的经验,我们可以快速地定义两个状态:持有have和不持有no

但此时,题目中添加了冷冻期这一新的状态,由于其不确定性,我们将其引入为第三个状态

😄 状态定义为:1.当天的天数i; 2.当天是否持有股票——have / no ;3.冷冻期


我们观察冷冻期的概念,只有在股票卖出之后才会出现冷冻期,所以,处于冷冻期时一定是不持有股票的,可以将它合并到状态no[i]中,这样就将三个状态成功转化为两个状态了

  1. have[i]:表示第i天持有股票时所获得的最多现金
  2. no[i]:表示第i天不持有股票时所获得的最多现金

②确定状态转移方程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wHPNOyIA-1680691032109)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 13.png)]

冷冻期只对第i天买入时的情况有影响:

当第i天买入股票时, 那第i-1天必定是不持有股票且不能是冷冻期, 所以要采用no[i-2]这个状态

而no[i-2]由两种情况组成:

  • 如果no[i-2]是在第i-2天卖出股票,那第i-1天就是冷冻期, 那第i天就解冻了
  • 如果no[i-2]是延续了前一天i-3天不持有股票的状态,那么在第i-1天就不会有股票被卖出, 那么第i天也不会是冷冻期

所以,no[i-2]卖出时,第i天一定不是冷冻期


③dp数组的初始化:

之前初始化只是对have[0]和no[0]进行:

💡 本题由于使用到了no[i-2]这个状态, 在i=1时是不合法的, 所以本题还要初始化have[1]和no[1]两个状态

因此,初始化四个值:

  1. have[0]=-prices[0]; no[0]=0
  2. have[1]=max(have[0],-prices[1]); no[1]=max(no[0],have[0]+prices[1])

④最后结果:

最后不持有时的利润一定比持有时的多,所以为: res=no[n-1]

代码实现:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        if n<=1:
            return 0
        have=[0]*n
        no=[0]*n
        have[0]=-prices[0]
        no[0]=0
        have[1]=max(have[0],-prices[1])
        no[1]=max(no[0],have[0]+prices[1])
        for i in range(2,n):
            no[i]=max(no[i-1],prices[i]+have[i-1])
            have[i]=max(have[i-1],no[i-2]-prices[i])
        return no[n-1]


⑥.买卖股票的最好时机VI

买卖股票的最好时机VI


思路分析:

这道题实际上就是II的一个变式,在卖出时添加了加上手续费的条件,因此对卖出时的情况更新处理即可:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZqCZzchg-1680691032110)(C:\Users\DAY 1\AppData\Local\Temp\360zip$Temp\360$0\动态规划 8cfb4e4615f941758203ae069b4388d0\Untitled 14.png)]

代码实现:

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n=len(prices)
        if n==0:
            return 0
        have=[0]*n
        no=[0]*n
        no[0]=0
        have[0]=-prices[0]
        for i in range(1,n):
            have[i]=max(have[i-1],no[i-1]-prices[i])
            no[i]=max(no[i-1],have[i-1]+prices[i]-fee)
        return no[n-1]

⑦.买卖股票的最佳时机问题总结:

🧐 总结:当dp数组为多个状态时,我们可以用命名方式(:开辟多个数组分散表示多种情况)降维,确定每一种情况对应的方程,最后组成状态转移方程,用循环方式进行递推


其实这个方法有一个好处,之前没有提及,我们将二维数组转为一维数组已经是一个空间优化了,其实在该类问题下,当前状态只由前一天的状态递推而来,所以,只与两个数组的前一个值有关,因此,我们还可以将一维数组直接转化为一个变量储存,以
买卖股票的最好时机VI 为例

可以进行空间优化:

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n=len(prices)
        if n==0:
            return 0
        no=0
        have=-prices[0]
        for i in range(1,n):
            have=max(have,no-prices[i])
            no=max(no,have+prices[i]-fee)
        return no

四.总结

 本篇引入了动态规划的入门部分,这是一个很重要的算法思想,不像其他算法一样会有固定的模板,所以实际问题中往往很复杂,下期将会深入dp,研究更多相关问题,如有错误,欢迎指正~~,感谢!!


觉得本篇有帮助的话,就赏个三连吧~

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DAY Ⅰ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值