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

目录

1. 全背包问题

2. 矩阵路径计数

3. 最小编辑距离(Levenshtein Distance)

4. 全文总结


简介:在前两篇博文中,我们介绍了动态规划的基本概念与思想,并讲解了几个常见的动态规划(DP)的例子,比如斐波那契数列, 0/1 背包问题,找零钱和最短路径问题。这篇文章将介绍另外三个经典的动态规划问题,全背包问题,矩阵路径计数,和最小编辑距离计算。

1. 全背包问题

问题描述:

给定一组物品,每个物品有一个重量(weight)和价值(value),以及一个背包的最大容量 (capacity)。目标是求解在不超过背包容量的情况下,最大化背包中物品的总价值,每个物品可以选择多次。全背包问题是动态规划中的经典问题之一。注意与 0/1 背包问题 不同,在 完全背包问题 中,每个物品可以选择多次,即每种物品可以被选择任意次,而不是只能选择一次。

动态规划解法:

  • 定义状态

    • 定义 dp[i] 表示容量为 i 的背包能够放入的最大价值。

  • 状态转移方程

    • 与 0/1 背包问题不同,在全背包问题中,每个物品可以选择多次,因此状态转移方程为:

          dp[i] = max( dp[i], dp [ i - weight[j]] + value [j] ) for all items  0 ≤ j< n
    • dp[i - weight[j]] + value[j] 表示在背包剩余容量为 i - weight[j] 时,加入第 j 个物品后的最大价值。

    • 边界条件:dp[0] = 0,表示容量为 0 时,背包中的最大价值是 0                      

  • 最终结果

    • 最终的解是 dp[capacity],背包能够放入物品最大价值 

例子:假设我们有 3 个物品,背包容量为 4:

物品 重量 价值
1 1 5
2 2 15
3 3 18

计算全背包问题的动态规划代码如下:

def completeKnapsack(weights, values, capacity):
    # dp[i] 表示背包容量为 i 时的最大价值
    dp = [0] * (capacity + 1)
    
    # 遍历每个物品
    for i in range(len(weights)):
        for j in range(weights[i], capacity + 1):  # 背包容量从当前物品的重量开始
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
    
    return dp[capacity]

# 示例
weights = [1, 2, 3]  # 物品的重量
values = [5, 15, 18]  # 物品的价值
capacity = 4  # 背包容量

print(completeKnapsack(weights, values, capacity))  

对于当前物品,从 weight[i] 开始更新背包的最大价值。dp[j] 表示当背包容量为 j 时的最大价值。注意,内层循环必须从当前物品的重量开始,因为一个物品可以放入背包多次。时间复杂度O(n * W),其中 n 是物品的种类数,W 是背包的容量。我们需要遍历每个物品,并对每个背包容量进行更新。空间复杂度O(W),我们只需要一个长度为 W + 1 的数组来保存每个容量的最大价值。

程序运行结果:

# 输出
30

全背包问题与 0/1 背包问题的区别:

特点 0/1 背包问题 完全背包问题(全背包)
物品选择次数 每个物品只能选择一次 每个物品可以选择多次
状态转移方式 dp [ i ][ w ] = max(dp [i-1 ][ w ], dp [ i-1 ][ w - weight [ i ] + value [ i ]) dp[i] = max(dp[i], dp[i - weight[j]] + value[j])(物品选择多次)
时间复杂度 O(n * W) O(n * W)
空间复杂度 O(n * W) O(W)

在全背包问题中,物品的选择次数没有限制,因此每个物品可以选择多次,导致了状态转移的变化,必须在每次考虑物品时更新背包的价值。

2. 矩阵路径计数

问题描述:
给定一个 m x n 的网格,机器人从左上角 (0, 0) 开始,每次可以向右或向下移动,问机器人到达右下角 (m-1, n-1) 的路径有多少条。

动态规划解法:

  • 状态定义:dp[i][j] 表示从 (0, 0)(i, j) 的路径总数。

  • 状态转移方程:当前位置的路径数是来自上方和左方路径数之和。

                     dp[i][j] =  dp[i-1][j] +  dp[i][j-1]
  • 边界条件:第一行和第一列的路径数只能从左方或从上方来,因此需要初始化为 1。

例子:假设我们从左上角 (0, 0) 开始,走到(3, 7),代码实现如下:

def uniquePaths(m, n):
    dp = [[1] * n for _ in range(m)]  # 初始化第一行和第一列为 1
    
    # 填充其余部分
    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]

# 示例
print(uniquePaths(3, 7))  

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

# 输出 
28

这是一个相对比较简单的例子,和计算矩阵内最短路径的问题非常类似。

3. 最小编辑距离(Levenshtein Distance)

问题描述: 给定两个字符串 word1word2,返回将 word1 转换为 word2 所需的最小操作数。操作包括插入、删除或替换一个字符。

动态规划解法:

  • 状态定义:dp[i][j] 表示将 word1[0...i-1] 转换为 word2[0...j-1] 的最小编辑距离。。

  • 状态转移方程:

    • 如果 word1[i-1] == word2[j-1],则无需任何操作,dp[i][j] = dp[i-1][j-1]

    • 否则,有三种操作:

      • 插入:从 word1[0...i-1] 转换到 word2[0...j-1] 需要插入 word2[j-1],即 dp[i][j-1] + 1

      • 删除:从 word1[0...i-1] 转换到 word2[0...j-1] 需要删除 word1[i-1],即 dp[i-1][j] + 1

      • 替换:从 word1[0...i-1] 转换到 word2[0...j-1] 需要将 word1[i-1] 替换为 word2[j-1],即 dp[i-1][j-1] + 1

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

    • dp[i][0] = i,表示将 word1[0...i-1] 转换为空字符串需要 i 次删除操作

    • dp[0][j] = j,表示将空字符串转换为 word2[0...j-1] 需要 j 次插入操作。

例子

word1 = "kitten", word2 = "sitting",计算最小编辑距离。代码实现如下:

def minDistance(word1, word2):
    m, n = len(word1), len(word2)
    
    # 创建一个 (m+1) * (n+1) 的 dp 数组
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # 初始化边界条件
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
    
    # 填充 dp 数组
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]  # 字符相同,无需操作
            else:
                dp[i][j] = min(dp[i - 1][j] + 1,  # 删除
                               dp[i][j - 1] + 1,  # 插入
                               dp[i - 1][j - 1] + 1)  # 替换
    
    return dp[m][n]

# 示例
word1 = "kitten"
word2 = "sitting"
print(minDistance(word1, word2))  

时间复杂度O(m * n),其中 mn 分别是 word1word2 的长度。我们需要遍历整个 dp 数组来填充每个值。空间复杂度O(m * n),我们需要一个大小为 (m+1) * (n+1) 的二维数组来存储中间结果。程序运行结果:     

# 输出 (kitten -> sitten -> sittin -> sitting)
3 

最小编辑距离问题 通过动态规划的方式,可以高效地计算两个字符串之间的最小转换操作数。使用 二维数组 来保存每个子问题的结果,并通过 状态转移方程 更新 dp 数组,如下:

0 1 2 3 4 5 6 7
1 1 2 3 4 5 6 7
2 2 1 2 3 4 5 6
3 3 2 1 2 3 4 5
4 4 3 2 1 2 3 4
5 5 4 3 2 2 3 4
6 6 5 4 3 3 2 3

想要进一步优化空间复杂度,可以使用 一维数组 来保存状态,从而减少空间的消耗。

4. 全文总结

在这一篇博客中,我们介绍了三个新的动态规划问题,完全背包,矩阵路径计数,和编辑距离计算。小伙伴们应该对动态规划的解题过程有了更深刻的认识。到这里我们已经对DP算法有了不错的理解,但是如果是参加面试的话还需要更多的练习。首先,面试官会经常要求对空间复杂度进行优化。另外面试过程中,面试官会将基础问题进行变种,以增加算法设计的难度。在今后的blog中,我会找一些 Leetcode 中的面试题,来为大家进行讲解。

参考文献:

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

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

你可能感兴趣的:(动态规划,算法)