动态规划思想:从理论到实践的探索

动态规划(Dynamic Programming,DP)是一种解决复杂问题的算法思想,核心是通过分解问题、存储中间结果来避免重复计算。它的核心目标是优化递归或分治过程中出现的重叠子问题,通过空间换时间的方式大幅提升效率。
核心思想
分解子问题 将大问题拆解为相互关联的小问题,找到问题的递归结构。例如:
斐波那契数列:f(n) = f(n-1) + f(n-2)
编辑距离:将字符串转换拆解为字符级别的插入、删除、替换操作。DNA序列编辑距离
重叠子问题 子问题之间重复出现,若直接递归会重复计算多次。例如斐波那契数列中,计算 f(5) 需要多次计算 f(3) 和 f(2)。
最优子结构 大问题的最优解可以由子问题的最优解组合得到。例如,最短路径问题中,从 A 到 C 的最短路径必然经过中间点 B 的最短路径。
记忆化存储(Memoization) 将子问题的解存储起来(通常用数组或哈希表),避免重复计算。

一般步骤

  1. 定义状态:确定问题的状态表示,通常使用一个或多个变量来表示子问题。
  2. 确定状态转移方程:根据问题的最优子结构性质,找出状态之间的转移关系。
  3. 初始化边界条件:确定初始状态的值,这些值是不需要通过状态转移方程计算的。
  4. 计算最终结果:根据状态转移方程和边界条件,逐步计算出最终问题的解。

斐波那契数列

其数值为:0、1、1、2、3、5、8、13、21、34……在数学上,这一数列以如下递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)

def fibonacci(n):
    # 创建一个列表来存储斐波那契数
    dp = [0] * (n + 1)

    # 初始化前两个斐波那契数
    dp[0], dp[1] = 0, 1

    # 从第三个数字开始计算到第 n 个数字
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]

    # 返回第 n 个斐波那契数
    return dp[n]

# 测试函数
print(fibonacci(10))  # 输出: 55
  1. 定义状态
    状态表示的是子问题,对于斐波那契数列,那么子问题就是计算第ii个斐波那契数( 0 ≤ i ≤ n 0≤i≤n 0in)。我们可以定义一个一维数组 dpdp , 其中 dp[i] dp[i] 表示第 ii 个斐波那契数。

  2. 确定状态转移方程
    根据斐波那契数列的定义,第 nn 个斐波那契数等于第 n−1n-1 个斐波那契数和第 n−2n-2 个斐波那契数之和。所以状态转移方程为 dp[i]=dp[i−1]+dp[i−2]dp[i] = dp[i-1] + dp[i-2] :,其中 i≥2i≥2 。

  3. 初始化边界条件
    在斐波那契数列的定义中,明确给出了第 0 个和第 1 个斐波那契数的值,即 F(0)=0,F(1)=1F(0)=0,F(1)=1 。 在动态规划的数组 dpdp 中,对应的初始化就是 dp[0]=0,dp[1]=1dp[0]=0,dp[1]=1 。这些初始值是不需要通过状态转移方程计算的,是已知的。

  4. 计算最终结果
    根据状态转移方程和边界条件,我们可以从 i=2i=2 开始,逐步计算出 dp[2],dp[3]…dp[n]dp[2],dp[3]…dp[n] 。 最终, 就是第 dp[n]dp[n] 个斐波那契数,也就是我们要求解的最终结果。

DNA序列编辑距离

输入:dna1 = “AGT”,dna2 = “AGCT” 输出:1
输入:dna1 = “AACCGGTT”,dna2 = “AACCTTGG” 输出:4
输入:dna1 = “A”,dna2 = “T” 输出:1

def solution(dna1: str, dna2: str) -> int:
   n, m = len(dna1), len(dna2)
   if n * m == 0:
       return n + m  # 任一DNA序列为空时,直接返回长度和

   # 初始化动态规划表 (n+1)x(m+1) 二位数组
   f = [[0] * (m + 1) for _ in range(n + 1)]

   # 3.初始化边界条件
   for i in range(n + 1):
       f[i][0] = i  # dna2为空,删除所有dna1的字符
   for j in range(m + 1):
       f[0][j] = j  # dna1为空,插入所有dna2的字符

   # 填充动态规划表
   for i in range(1, n + 1):
       for j in range(1, m + 1):
           # 计算三种操作的代价
           delete = f[i-1][j] + 1
           insert = f[i][j-1] + 1
           replace_or_match = f[i-1][j-1] + (dna1[i-1] != dna2[j-1])
           # 取最小值作为当前状态
           f[i][j] = min(delete, insert, replace_or_match)

   return f[n][m]  # 最终结果
  1. 定义状态
    设两个 DNA 序列分别为 dna1 和 dna2,长度分别为 n 和 m。我们要定义子问题来逐步求解将 dna1 转换为 dna2 的编辑距离。
    定义一个二维数组 f,其中 f[i][j] 表示将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符所需的最少操作次数。这里 i 的取值范围是 0 到 n,j 的取值范围是 0 到 m。
  2. 确定状态转移方程
    对于 f[i][j],可以通过三种不同的操作从之前的状态转移过来,分别是删除、插入和替换(或匹配)操作,我们需要取这三种操作代价中的最小值作为 f[i][j] 的值:
    删除操作:如果要将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符,且选择删除 dna1 的第 i 个字符,那么只需要在将 dna1 的前 i - 1 个字符转换为 dna2 的前 j 个字符的基础上再进行一次删除操作。所以这种操作的代价为 f[i - 1][j] + 1。
    插入操作:若选择在 dna1 的末尾插入一个字符使其与 dna2 的第 j 个字符匹配,那么就是在将 dna1 的前 i 个字符转换为 dna2 的前 j - 1 个字符的基础上再进行一次插入操作。这种操作的代价为 f[i][j - 1] + 1。
    替换或匹配操作:比较 dna1 的第 i 个字符和 dna2 的第 j 个字符,如果它们相等,说明不需要额外操作;如果不相等,则需要进行一次替换操作。该操作的代价为 f[i - 1][j - 1] + (dna1[i - 1] != dna2[j - 1]),这里 (dna1[i - 1] != dna2[j - 1]) 是一个布尔表达式,若不相等结果为 1,相等则为 0。
    因此,状态转移方程为:

状态转移方程

  1. 初始化边界条件
    当 j = 0 时,意味着要将 dna1 的前 i 个字符转换为空字符串,显然需要进行 i 次删除操作,所以 f[i][0] = i,其中 i 从 0 到 n。
    当 i = 0 时,即要将空字符串转换为 dna2 的前 j 个字符,需要进行 j 次插入操作,所以 f[0][j] = j,其中 j 从 0 到 m。
  2. 计算最终结果
    根据状态转移方程和边界条件,我们可以通过两层嵌套循环来填充二维数组 f。外层循环遍历 i 从 1 到 n,内层循环遍历 j 从 1 到 m,每次根据状态转移方程计算 f[i][j] 的值。
    最终,f[n][m] 就表示将 dna1 的全部字符转换为 dna2 的全部字符所需的最少操作次数,也就是两个 DNA 序列的编辑距离。
    以样例1为例分布讲解代码各部分含义:

添加图片注释,不超过 140 字(可选)

初始状态

当 i = 1,j = 1 时:A 与 A

dna1[i - 1] = “A”,dna2[j - 1] = “A”。
delete = f[0][1] + 1 = 1 + 1 = 2,表示先将空字符串转换为 “A”(f[0][1]),再删除 dna1 的第 1 个字符 “A”。
insert = f[1][0] + 1 = 1 + 1 = 2,表示先将 “A” 转换为空字符串(f[1][0]),再插入字符 “A”。
replace_or_match = f[0][0] + (dna1[0] != dna2[0]) = 0 + 0 = 0,因为 “A” == “A”,不需要替换操作。

当 i = 1,j = 2 时:A 与 AG

dna1[i - 1] = “A”,dna2[j - 1] = “G”。
delete = f[0][2] + 1 = 2 + 1 = 3。 插入“AG” + 删除A
insert = f[1][1] + 1 = 0 + 1 = 1。 无需删除 + 插入G
replace_or_match = f[0][1] + (dna1[0] != dna2[1]) = 1 + 1 = 2。插入A --> AA 将第二个A替换为G A --> AA --> AG
f[1][2] = min(3, 1, 2) = 1。
在这里插入图片描述

以此类推,逐步填充整个 f 数组。最终填充完的 f 数组如下:
在这里插入图片描述

  • 前三步引领分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

复杂度分析
时间复杂度: O(n×m)O(n×m) ,因为使用了两层嵌套循环来填充二维数组 f。
空间复杂度: O(n×m)O(n×m) ,主要用于存储二维数组 f。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

胜天半月子

打不打商的无所谓,能帮到你就好

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

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

打赏作者

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

抵扣说明:

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

余额充值