目录
1. 全背包问题
2. 矩阵路径计数
3. 最小编辑距离(Levenshtein Distance)
4. 全文总结
简介:在前两篇博文中,我们介绍了动态规划的基本概念与思想,并讲解了几个常见的动态规划(DP)的例子,比如斐波那契数列, 0/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
< ndp[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) |
在全背包问题中,物品的选择次数没有限制,因此每个物品可以选择多次,导致了状态转移的变化,必须在每次考虑物品时更新背包的价值。
问题描述:
给定一个 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
这是一个相对比较简单的例子,和计算矩阵内最短路径的问题非常类似。
问题描述: 给定两个字符串 word1
和 word2
,返回将 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)
,其中 m
和 n
分别是 word1
和 word2
的长度。我们需要遍历整个 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 |
想要进一步优化空间复杂度,可以使用 一维数组 来保存状态,从而减少空间的消耗。
在这一篇博客中,我们介绍了三个新的动态规划问题,完全背包,矩阵路径计数,和编辑距离计算。小伙伴们应该对动态规划的解题过程有了更深刻的认识。到这里我们已经对DP算法有了不错的理解,但是如果是参加面试的话还需要更多的练习。首先,面试官会经常要求对空间复杂度进行优化。另外面试过程中,面试官会将基础问题进行变种,以增加算法设计的难度。在今后的blog中,我会找一些 Leetcode 中的面试题,来为大家进行讲解。
参考文献:
动态规划不再难:一步一步教你攻克经典问题 (1)-CSDN博客
动态规划不再难:一步一步教你攻克经典问题 (2)-CSDN博客