当前位置: 首页 > news >正文

动态规划 Dynamic programming

动态规划

动态规划是一种从底至顶的方法,从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解。其核心特征包括:

  • 最优子结构:原问题的最优解是从子问题的最优解构建得来的;
  • 无后效性:已经求解的子问题,不会再受到后续决策的影响。

为此,采用数组(称为 dp 数组)来存储各子问题的最优解,其每个元素对应一个子问题的最优解。通过定义状态转移方程明确子问题间的关系,并按顺序依次求解各个阶段的问题。

例题
70. 爬楼梯 :给定一个共有 nnn 阶的楼梯,你每步可以上 111 阶或者 222 阶,请问有多少种方案可以爬到楼顶?

要到达第 iii 个台阶,有两种方式:从第 i−1i-1i1 个台阶踏一步上来,或者从第 i−2i-2i2 个台阶踏两步上来。因此到达第 iii 个台阶的总方案数等于前两个台阶方案数之和。

为了存储每个问题的解,定义一个 dp 数组,其中 dp[i]dp[i]dp[i] 表示爬到第 iii 台阶的方案数。根据以上的分析,可得到状态转移方程: dp[i]=dp[i−1]+dp[i−2]dp[i] = dp[i-1] + dp[i-2]dp[i]=dp[i1]+dp[i2]。这表明爬楼梯问题具有最优子结构特性,问题的最终解可以通过子问题的解递推得到。

此外,需要初始化边界,设置最小子问题的解,即 dp[0]=1dp[0]=1dp[0]=1dp[1]=1dp[1]=1dp[1]=1。代码如下所示:

def climbing_stairs_dp(n: int) -> int:"""爬楼梯:动态规划"""if n == 1 or n == 2:return n# 初始化 dp 表,用于存储子问题的解dp = [1] * (n + 1)# 状态转移:从较小子问题逐步求解较大子问题for i in range(2, n + 1):dp[i] = dp[i - 1] + dp[i - 2]return dp[n]

由于 dp[i]dp[i]dp[i] 只与 dp[i−1]dp[i-1]dp[i1]dp[i−2]dp[i-2]dp[i2] 相关,因此无须使用一个数组 dp 来存储所有子问题的解,而只需两个变量滚动前进即可。代码如下所示:

def climbing_stairs_dp_comp(n: int) -> int:"""爬楼梯:空间优化后的动态规划"""if n == 1 or n == 2:return na, b = 1, 1for _ in range(2, n + 1):a, b = b, a + breturn b

0-1 背包

nnn 件物品和一个容量为 www 的背包,其中第 iii 件物品的重量和价值分别为 wiw_iwiviv_ivi。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

上述问题中,由于每个物体只有两种状态(或者不取),对应二进制中的 000111,因此这类问题便被称为 0-1 背包问题。

分割等和子集

题目含义:给定一个长度为 nnn 且只包含正整数的非空数组 numsnumsnums,求这个数组是否可以分割成两个子集,使得两个子集的元素和相等。
例如,[1,5,11,5][1,5,11,5][1,5,11,5] 可以分割为 [1,5,5][1,5,5][1,5,5][11][11][11],而 [1,2,3,5][1,2,3,5][1,2,3,5] 不能分割成两个元素和相等的子集。

转换思路:根据以上的例子可得,s=sum(nums)s = sum(nums)s=sum(nums) 必须是偶数才能分割出两个子集,且分割后的子集总和为 a=s2a = \frac{s}{2}a=2s。那么当前问题就转化为 0-1 背包,即在数组的每个元素均有限的条件下,容量为 aaa 的背包可以放入物品的最大价值/总和。

动态规划
1、定义 dp 数组:dp[i][j]dp[i][j]dp[i][j] 表示前 i−1i-1i1 个元素(长度为 iii)在容量为 jjj 的背包中的最大价值,维度为 (n+1)×(a+1)(n+1) \times (a + 1)(n+1)×(a+1)

2、状态转移方程:每个物品都有不放入和放入这两个选择:如果放入物品则背包容量减少,如果不放入物品则背包容量不变。确定当前物品 nums[i−1]nums[i-1]nums[i1] 的决策后,转而考虑前 i−2i-2i2 个物品最大价值的子问题:
  a、当元素 nums[i−1]nums[i-1]nums[i1] 超出背包容量 jjj 时,物品不能放入背包,因此 dp[i][j] = dp[i-1][j]
  b、当 nums[i−1]nums[i-1]nums[i1] 小于等于容量 jjj 时,需要从不放入物品和不放入物品两种选择最大的价值,即 dp[i] = max(dp[i-1][j], dp[i-1][j-nums[i-1]] + nums[i-1])

3、确定边界条件:

  • dp[i][0],i>0dp[i][0], i > 0dp[i][0],i>0 表示前 iii 个物品装入容量为 000 的背包,这种情况无解,故设为 000
  • dp[0][j],j>0dp[0][j], j > 0dp[0][j],j>0 表示用零个物品填满容量为 jjj 的背包,同样无解,设为 000
  • dp[0][0]dp[0][0]dp[0][0] 表示从零个物品放入容量为 000 的背包,合理的且最大价值为 000

空间和时间复杂度:空间复杂度和时间复杂度均为 O(n×amt)O(n \times amt)O(n×amt)

class Solution:def canPartition(self, nums: List[int]) -> bool:def fn(nums, target):n = len(nums)dp = [[0] * (target+1) for _ in range(n+1)]for i in range(1, n+1):for j in range(1, target + 1):if j < nums[i-1]:dp[i][j] = dp[i-1][j]else:dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i-1]] + nums[i-1])return dp[n][target] == targets = sum(nums)if s % 2 != 0:return Falsereturn fn(nums, s//2)

空间优化的一维DP解法
根据上述分析,dp[i][j]dp[i][j]dp[i][j] 的状态转移仅与 dp[i−1][j]dp[i-1][j]dp[i1][j]dp[i−1][j−nums[i−1]]dp[i-1][j-nums[i-1]]dp[i1][jnums[i1]] 相关,而与之前各行无关,因此可将二维数组优化为一维数组,即在遍历到第 iii 元素时,该数组存储的是第 i−1i-1i1 行的状态。

注意,利用第 i−1i-1i1 行来更新第 iii 行的信息,如果采用正序遍历,则 dp[j]dp[j]dp[j] 所依赖的数据 dp[j−nums[i]]dp[j-nums[i]]dp[jnums[i]] 会被新的值覆盖,导致状态转移错误,为此必须采用倒序遍历。

class Solution:def canPartition(self, nums: List[int]) -> bool:def fn(t):dp = [0] * (t+1)for x in nums:for j in range(t, -1, -1):if x <= j:dp[j] = max(dp[j], dp[j-x] + x)return dp[t] == ts = sum(nums)if s % 2 != 0:return Falsereturn fn(s // 2)# 上方一致,可少一些判断
class Solution:def canPartition(self, nums: List[int]) -> bool:def fn(t):dp = [0] * (t+1)for x in nums:for j in range(t, x-1, -1):dp[j] = max(dp[j], dp[j-x] + x)return dp[t] == ts = sum(nums)if s % 2 != 0:return Falsereturn fn(s // 2)

完全背包

完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

零钱兑换

题目含义:给定 nnn 种不同硬币和目标金额 amtamtamt,其中每种硬币的个数不限。问能够凑出目标金额的最少硬币数量,如果无法凑出目标金额,则返回 −1-11
例如:coins = [1, 2, 5], amt=11,则最少硬币数量为 333,即两个面值为 555 和一个面值为 111 的硬币。

动态规划
(1)二维的 dp 数组:dp[i][j] 表示使用前 i 个硬币凑出目标金额 j 的最少硬币个数,其中数组维度为 (n+1)×(amt+1)(n+1) \times (amt+1)(n+1)×(amt+1)

(2)状态转移方程:
  (2.1)当 coins[i] > j 时,dp[i][j] = dp[i-1][j],表明当前硬币的面值过大,选择不了当前硬币;
  (2.2)当 coins[i] <= j 时,dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1),表明硬币面值不超过当前目标金额 j 下,从不选择硬币选择硬币中判断最佳方案。

dp[i][j-coins[i]]+1:由于物品的数量是无限的,因此将硬币 coins[i] 放入背包后,仍可以从前 iii 个硬币中选择;由于背包已放入了 coins[i],为了不破坏当前金额的总数,需得到 j−coins[i]j-coins[i]jcoins[i] 最少的硬币个数。

(3)确定边界条件:
  (3.1)当目标金额为 000 时,凑出它的最少硬币数量为 000,因此 dp 数组首列都为 000,即 dp[i][0] = 0
  (3.2)当没有硬币时,无法凑出目标金额 >0>0>0,相当于无效解,因此可将它们置为 amt+1amt+1amt+1 来表示无效,即 dp[0][j] = amt+1,且 j > 0

时间和空间复杂度:空间复杂度时间复杂度均为 O(n×amt)O(n \times amt)O(n×amt)

class Solution:def coinChange(self, coins: List[int], amount: int) -> int:n = len(coins)dp = [[0] * (amount+1) for _ in range(n+1)] for j in range(1, amount+1):dp[0][j] = 1 + amountfor i in range(1, n+1):for j in range(1, amount+1):if coins[i-1] > j:dp[i][j] = dp[i-1][j]  # 面值太大,凑不成总金额为jelse:dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i-1]] + 1)return dp[n][amount] if dp[n][amount] != (1 + amount) else -1

空间优化的一维DP解法
根据上面的解法,dp[i][j] 的状态是由上方的 dp[i-1][j] 和左方的 dp[i][j-coins[i]] 转移来的,因此将二维数组优化为一维的:
(1)当硬币面值大于 jjj 时,dp[j] = dp[j]
(2)当硬币面值小于等于 aaa 时,dp[j] = min(dp[j], dp[j-coins[i]]+1)
(3)初始化:dp[0]=0,当 j>0j>0j>0dp[j]=amt+1
(4)空间复杂度:空间复杂度为 O(amt)O(amt)O(amt)

class Solution:def coinChange(self, coins: List[int], amount: int) -> int:mmax = amount + 1# 没有硬币,不能凑成一定的金额,标为无效值 mmaxdp = [mmax for _ in range(amount+1)]  dp[0] = 0for c in coins:# 正序遍历for j in range(1, amount+1):if c <= j:dp[j] = min(dp[j], dp[j-c]+1) return dp[amount] if dp[amount] != mmax else -1
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:mmax = amount + 1dp = [mmax for _ in range(amount+1)]  dp[0] = 0for c in coins:# 直接从 c 开始,跳过了硬币面值超过金额a的情况for j in range(c, amount+1):dp[j] = min(dp[j], dp[j-c]+1) return dp[amount] if dp[amount] != mmax else -1

零钱规划Ⅱ

题目含义:给定一个包含 nnn 种不同面额的硬币数组 coinscoinscoins 和目标金额 amtamtamt,求可以凑成总金额的硬币组合数,其中每种硬币可以重复使用。
例如,coins=[1,2,5]coins=[1,2,5]coins=[1,2,5]amt=5amt=5amt=5,有四种方式凑成目标金额:{5}、{2, 2, 1} 、{2, 1, 1, 1} 和 {1,1,1,1,1}。

动态规划
(1)定义维度为 (n+1)×(amt+1)(n+1) \times (amt+1)(n+1)×(amt+1) 的 dp 数组,其中 dp[i][j]dp[i][j]dp[i][j] 表示使用前 iii 种硬币组成金额 jjj 的方案数。

(2)对于硬币 coins[i−1]≤jcoins[i-1] \leq jcoins[i1]j,无论选择不使用还是选择使用,都构成一种可行方案,因此转移方程为 dp[i][j]=dp[i−1][j]+dp[i][j−coins[i−1]]dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]dp[i][j]=dp[i1][j]+dp[i][jcoins[i1]]

(3)边界:

  • i=0,j=0i=0, j=0i=0,j=0 时,dp[0][0]=1dp[0][0] = 1dp[0][0]=1
  • i≠0,j=0i \neq 0, j=0i=0,j=0 时,dp[i][0]=1dp[i][0] = 1dp[i][0]=1(不选择任何硬币);
  • i=0,j≠0i=0, j \neq 0i=0,j=0 时,dp[0][j]=0dp[0][j] = 0dp[0][j]=0

复杂度:空间复杂度和时间复杂度均为 O(n×amt)O(n \times amt)O(n×amt)

class Solution:def change(self, amount: int, coins: List[int]) -> int:n = len(coins)dp = [[0] * (amount+1) for _ in range(n+1)]for i in range(n+1):dp[i][0] = 1for i in range(1, n+1):for j in range(1, amount+1):if coins[i-1] > j:dp[i][j] = dp[i-1][j]else:dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]return dp[n][amount]

空间优化的一维DP解法

class Solution:def change(self, amount: int, coins: List[int]) -> int:dp = [0] * (amount+1)# 没有硬币就不能组成 a>0 的目标金额,因此没有组合方案dp[0] = 1for c in coins:for a in range(c, amount+1):dp[a] += dp[a-c]return dp[amount]

编辑距离

题目含义:给定两个字符串 sssttt,返回将 sss 转换为 ttt 所需的最少编辑步数,其中编辑操作有三种:插入一个字符、删除一个字符、将字符替换为任意一个字符。
例如:将 s='horse' 转为 t = 'ros' 至少需要 333 步,包括将 sss 里的 ‘h’ 替换为 ‘r’(‘rorse’),再删除第二个 ‘r’ (‘rose’),最后删除 ‘e’(‘ros’)。

动态规划
(1)二维 dp 数组:每一轮的编辑都是针对字符串 sss 的第 iii 个字符,使两字符串尾部的字符相同,从而跳过它们,转而考虑其子问题:sssi−1i-1i1 个字符与 tttj−1j-1j1 个字符的最少编辑次数。
假设 nnnmmm 分别是字符串 sssttt 的长度,建立一个维度为 (n+1)×(m+1)(n+1) \times (m+1)(n+1)×(m+1) 的 dp 数组,其 dp[i][j] 表示将 sssi−1i-1i1 个字符更改为 tttj−1j-1j1 个字符所需的最少编辑步数。

(2)状态转移方程:

  • 插入:在 s[i−1]s[i-1]s[i1] 后头添加字符 t[j−1]t[j-1]t[j1],则问题转化为计算 sssi−1i-1i1 个字符与 tttj−2j-2j2 个字符的最少编辑次数,即 dp[i][j-1]
  • 删除:删除 s[i−1]s[i-1]s[i1],则问题转化为计算 sssi−2i-2i2 个字符与 tttj−1j-1j1 个字符的最少编辑次数,即 dp[i-1][j]
  • 更新:将 s[i−1]s[i-1]s[i1] 替换为 t[j−1]t[j-1]t[j1],则问题转化为计算从 sssi−2i-2i2 个字符与 tttj−2j-2j2 个字符的最少编辑次数,即 dp[i-1][j-1]
  • 没有修改:此时 s[i−1]=t[j−1]s[i-1] = t[j-1]s[i1]=t[j1],则问题转化为计算 sss 的前 i−2i-2i2 个字符与 tttj−2j-2j2 个字符的最少编辑次数,即 dp[i-1][j-1]

根据以上的分析可得,状态转移方程为:
dp[i][j]={min(dp[i][j−1],dp[i−1][j],dp[i−1][j−1]),s[i−1]≠t[j−1]dp[i−1][j−1],s[i−1]=t[j−1]dp[i][j] = \begin{cases} min \left(dp[i][j-1], \ dp[i-1][j], \ dp[i-1][j-1] \right), \ s[i-1] \neq t[j-1] \\ dp[i-1][j-1], \ s[i-1] = t[j-1] \end{cases} dp[i][j]={min(dp[i][j1], dp[i1][j], dp[i1][j1]), s[i1]=t[j1]dp[i1][j1], s[i1]=t[j1]

(3)确定边界条件:
当两字符串都为空时,编辑步数为 000,即 dp[0][0]=0dp[0][0]=0dp[0][0]=0
sss 为空而 ttt 不为空,最少的编辑次数为 ttt 的长度,即 dp[0][j]=jdp[0][j]=jdp[0][j]=j
sss 为不空而 ttt 为空,最少的编辑次数为 sss 的长度,即 dp[i][0]=idp[i][0]=idp[i][0]=i

复杂度:时间复杂度为 O(n×m)O(n \times m)O(n×m),空间复杂度为 O(n×m)O(n \times m)O(n×m)

class Solution:def minDistance(self, s: str, t: str) -> int:nS, nT = len(s), len(t)dp = [[0]*(nT+1) for _ in range(nS+1)]for i in range(1, nS + 1):dp[i][0] = ifor j in range(1, nT + 1):dp[0][j] = jfor i in range(1, nS + 1):for j in range(1, nT + 1):if s[i-1] == t[j-1]:dp[i][j] = dp[i-1][j-1]else:dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1return dp[nS][nT]

空间优化的一维DP解法
根据上面的分析,dp[i][j]dp[i][j]dp[i][j] 状态与上方 dp[i−1][j]dp[i-1][j]dp[i1][j]、左方 dp[i][j−1]dp[i][j-1]dp[i][j1] 以及左上角 dp[i−1][j−1]dp[i-1][j-1]dp[i1][j1] 相关。
若采用一维数组优化,在正序遍历过程中需注意 dp[i−1][j−1]dp[i-1][j-1]dp[i1][j1] 会被覆盖的问题。
因此,除初始化一维数组外,还需引入变量 oldoldold 存储左上角的 dp[j−1]dp[j-1]dp[j1]

class Solution:def minDistance(self, s: str, t: str) -> int:nT = len(t)dp = [0] * (nT + 1)for j in range(1, nT+1):dp[j] = jfor c in s:old = dp[0]dp[0] += 1  for j in range(1, nT+1):   # 在更新dp[j]前存储其值,作为下一轮的 dp[i-1][j-1]tmp = dp[j]   if c == t[j-1]:dp[j] = oldelse:dp[j] = min(dp[j], dp[j-1], old) + 1old = tmpreturn dp[nT]

参考:Hello 算法

http://www.dtcms.com/a/309109.html

相关文章:

  • 渗透作业3
  • Kafka Streams 并行处理机制深度解析:任务(Task)与流线程(Stream Threads)的协同设计
  • kafka快速部署、集成、调优
  • 超越 ChatGPT:智能体崛起,开启全自主 AI 时代
  • 中英混合的语音识别XPhoneBERT 监督的音频到音素的编码器结合 f0 特征LID
  • 阿里云微服务引擎 MSE 及 API 网关 2025 年 7 月产品动态
  • 单变量单步时序预测:CNN-LSTM卷积神经网络结合长短期记忆神经网络
  • MybatisPlus如何用wrapper语句灵活连接多查询条件
  • SpringBoot+LangChain4j解析pdf文档,不使用默认解析器
  • 解决VScode加载慢、保存慢,git加载慢,windows11系统最近异常卡顿的问题
  • 高端房产管理小程序
  • 【Ubuntu】安装使用pyenv - Python版本管理
  • ORACLE函数
  • JVM垃圾回收算法和分代收集算法的区别
  • 插件升级:Chat/Builder 合并,支持自定义 Agent、MCP、Rules
  • 深度学习(鱼书)day08--误差反向传播(后三节)
  • Day 28:类的定义和方法
  • 属性的运用和理解
  • 赛博算命之八字测算事业运势的Java实现(四柱、五行、十神、流年、格局详细测算)
  • Redisson实现Redis分布式锁的原理
  • Windows和Linux的tree工具
  • 【智能协同云图库】第七期:基于AI调用阿里云百炼大模型,实现AI图片编辑功能
  • 渗透测试报告通常包含哪些关键内容?
  • redis快速部署、集成、调优
  • Linux通用SPI作为Master——回环测试
  • Redis学习-----Redis的基本数据类型
  • Dify版本升级实操
  • Edge中如何找到原IE浏览器的Internet选项
  • 基于html,css,jquery,django,lstm,cnn,tensorflow,bert,推荐算法,mysql数据库
  • 8月1日RED指令强制生效,您的设备准备好了吗?