动态规划不再难:一步一步教你攻克经典问题 (2)

目录

1. 最长公共子序列

2. 最小路径问题

3. 硬币找零问题

4. 全文总结


简介:在上一篇博文(动态规划不再难:一步一步教你攻克经典问题 (1))中,我们介绍了动态规划的基本概念与思想,并举了两个基本的例子,斐波那契数列与 0/1 背包问题。这篇文章将介绍另外三个长见的动态规划问题,最长公共子序列,矩阵路径问题,和硬币找零问题。

1. 最长公共子序列

问题描述:

给定两个字符串 XY,求这两个字符串的最长公共子序列的长度。子序列是由原字符串中字符顺序不变的子集组成的,但不要求是连续的

动态规划解法:

  • 定义状态

    • dp[i][j] 表示字符串 X[0..i-1]Y[0..j-1] 的最长公共子序列的长度。

    • dp[i][j] 表示 XY 的前 i 个字符与前 j 个字符的最长公共子序列的长度。

  • 状态转移方程

    • 如果 X[i-1] == Y[j-1],那么这两个字符可以被加入到公共子序列中,即:

                                      dp [ i ] [ j ] = dp [ i - 1 ] [ j - 1 ] + 1
    • 如果 X[i-1] != Y[j-1],那么我们需要去掉当前的字符,比较两种情况:

                                      dp [ i ] [ j ] = max⁡(dp [ i - 1 ] [ j ], dp [ i ] [ j - 1 ])
    • 边界条件:如果 ij 为 0,说明其中一个字符串为空,最长公共子序列的长度为 0:

                                      dp [ i ] [ 0 ] = 0, dp [ 0 ] [ j ] = 0
  • 最终结果

    • 最终的解是 dp[m][n],其中 mn 分别是 XY 的长度。

例子:假设 X = "ABCBDAB"Y = "BDCABB",它们的最长公共子序列是 "BCAB",所以最长公共子序列的长度是 4。程序如下:

def longestCommonSubsequence(X, Y):
    m, n = len(X), len(Y)
    # 创建 dp 数组,大小为 (m+1) x (n+1)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # 填充 dp 数组
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if X[i - 1] == Y[j - 1]:  # 如果当前字符相等
                dp[i][j] = dp[i - 1][j - 1] + 1  # LCS 长度加 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])  # 否则取前一个子问题的最大值
    
    # 返回 dp[m][n],即最长公共子序列的长度
    return dp[m][n]

# 示例
X = "ABCBDAB"
Y = "BDCABB"
result = longestCommonSubsequence(X, Y)
print(f"最长公共子序列的长度: {result}")

代码时间复杂度O(m * n),其中 mn 分别是字符串 XY 的长度。空间复杂度O(m * n),需要一个二维数组来存储中间结果。

程序运行结果:

最长公共子序列的长度: 4

对应的 dp 数组(部分)填充过程如下:

i,j0BDCABB
00000000
A0000111
B0111122
C0112233
B0112233
D0122233
A0122333
B0122344

最终 dp[7][6] = 4,即 XY 的最长公共子序列的长度为 4

2. 最小路径问题

问题描述

给定一个 m x n 的矩阵,其中每个元素代表从该位置到达目标的代价(或路径的权重)。你从矩阵的左上角((0, 0))出发,只能向右或向下移动,最终到达矩阵的右下角((m-1, n-1))。求从左上角到右下角的路径的最小代价。

动态规划解法:

  • 定义状态

    • dp[i][j] 表示从 (0, 0)(i, j) 的最小路径和。

  • 状态转移方程

    • 如果 X[i-1] == Y[j-1],那么这两个字符可以被加入到公共子序列中,即:

                    dp [ i ] [ j ] = matrix [ i ] [ j ] + min( dp [ i - 1 ] [ j ], dp [ i ] [ j - 1 ])
    • 边界条件1:

                    第一行:dp[0][j] = dp[0][j-1] + matrix[0][j],只能从左方过来。
    • 边界条件2:

                    第一列:dp[i][0] = dp[i-1][0] + matrix[i][0],只能从上方过来。                     
  • 最终结果

    • 最终的解是 dp[m - 1][n - 1],路径的最小代价。

例子

matrix = [[1, -3, 1],
               [1, 5, 1],
               [4, -2, 1]]

程序如下:

def minPathSum(matrix):
    if not matrix:
        return 0
    
    m, n = len(matrix), len(matrix[0])
    dp = [[0] * n for _ in range(m)]
    
    dp[0][0] = matrix[0][0]  # 初始化起点
    
    # 填充第一行
    for j in range(1, n):
        dp[0][j] = dp[0][j-1] + matrix[0][j]
    
    # 填充第一列
    for i in range(1, m):
        dp[i][0] = dp[i-1][0] + matrix[i][0]
    
    # 填充其余部分
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = matrix[i][j] + min(dp[i-1][j], dp[i][j-1])
    
    return dp[m-1][n-1]

# 示例
matrix = [
    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]]

print(minPathSum(matrix))  

时间复杂度O(m * n),需要遍历整个矩阵。空间复杂度O(m * n),需要一个 m x n 的二维数组来存储中间结果。

程序运行结果:

# 输出 (路径为 1 → 3 → 1 → 1 → 1)
7

dp 数组填充过程大家自己尝试一下,不是很难。

3. 硬币找零问题

问题描述

给定一个面值数组 coins 和一个目标金额 amount,你需要找到凑成 amount 的最少硬币数量。如果无法凑成目标金额,返回 -1。

动态规划解法:

  • 状态定义

    • 定义 dp[i] 为凑成金额 i 所需要的最少硬币数量。

  • 状态转移方程

    • dp [ i ] = min⁡(dp [ i ], dp[i - coin] + 1 )   for all coins   coin ∈ coins 

    • 其中 dp[i - coin] + 1 表示从金额 i-coin 转移到金额 i 时需要加上一个硬币 coin

  • 初始化

    • dp[0] = 0,表示凑成金额 0 需要 0 个硬币。

    • 对于其他金额,初始化为 inf,表示无法凑成。

  • 边界条件

    • 如果目标金额无法通过给定的硬币组合凑成,返回 -1

例子

coins = [1 2 5], 三种硬币,amoount = 11

程序如下:

def coinChange(coins, amount):
    # 初始化 dp 数组,大小为 amount + 1,初始值为正无穷
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0  # 凑成 0 元需要 0 个硬币
    
    for i in range(1, amount + 1):
        for coin in coins:
            if i - coin >= 0:
                dp[i] = min(dp[i], dp[i - coin] + 1)
    
    # 如果 dp[amount] 仍然是无穷大,表示无法凑成目标金额
    return dp[amount] if dp[amount] != float('inf') else -1

# 示例
coins = [1, 2, 5]
amount = 11
print(coinChange(coins, amount))  

时间复杂度O(n * m),其中 namountm 是硬币的种类数。我们需要计算从 1amount 的每个金额,每次循环中我们遍历每个硬币。空间复杂度O(n),我们只需要一个大小为 amount + 1 的数组来存储中间结果

程序运行结果:

# 输出 (11 = 5 + 5 + 1)
3 

我们可以看到,找零钱问题其实是 0/1 背包问题的一个变种。dp 表格的话大家自己试一试看。

4. 全文总结

本文总结了三个动态规划问题的基本解法,最长公共子序列,矩阵最短路径,和找零钱问题。通过不停的练习,可以锻炼大家对于算法递推公式的推导能力,更精确的把握如何设置边界条件和起始条件。最后,希望大家能更好的理解动态规划这个算法。

参考文献:

动态规划不再难:一步一步教你攻克经典问题 (1)-CSDN博客

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

方博士AI机器人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值