动态规划:两个数组的 DP 问题二
在动态规划问题中,涉及两个数组的问题通常有一些特定的规律或约束,可能是 子序列问题、子集问题,或 字符串匹配问题 等。
假设我们有两个数组,要求通过某种方式对它们进行比对、匹配或优化,这样的问题通常可以通过二维 动态规划表 来求解。下面将展示一个典型的“两个数组”类型的动态规划问题,并给出详细的解析和解法。
问题:最长公共子序列(LCS)
问题描述:
给定两个数组或字符串 A
和 B
,求这两个数组的 最长公共子序列(LCS)。即,求在两个数组中都存在的最长子序列。
子序列是指在数组中删除一些元素(可以不删除任何元素),但不改变元素的顺序。
输入:
- 数组 A 和数组 B
A = [1, 3, 4, 1]
B = [1, 2, 3, 4, 1]
输出:
- LCS 的长度
例如,在上面的示例中,LCS 是 [1, 3, 4, 1]
,长度为 4
。
1. 动态规划解法
1.1. 状态定义
我们使用 二维 DP 表 来解决这个问题,dp[i][j]
表示 A[0...i-1]
和 B[0...j-1]
之间的最长公共子序列的长度。
1.2. 状态转移方程
- 当
A[i-1] == B[j-1]
时,dp[i][j] = dp[i-1][j-1] + 1
。- 这意味着我们找到了一个公共元素,可以将它加入当前的子序列,且子序列长度增加1。
- 当
A[i-1] != B[j-1]
时,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。- 如果当前的元素不相等,我们只能选择忽略
A[i-1]
或忽略B[j-1]
,因此选择这两者中的最大值。
- 如果当前的元素不相等,我们只能选择忽略
1.3. 初始化
dp[i][0] = 0
:表示空数组与任何数组的公共子序列长度为 0。dp[0][j] = 0
:表示任何数组与空数组的公共子序列长度为 0。
1.4. 算法实现
def longestCommonSubsequence(A, B):
# 获取两个数组的长度
m, n = len(A), len(B)
# 创建二维 DP 表,初始化为 0
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 A[i - 1] == B[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # 如果元素相同
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # 如果元素不同
# 最后一个单元格即为结果,表示最长公共子序列的长度
return dp[m][n]
# 示例
A = [1, 3, 4, 1]
B = [1, 2, 3, 4, 1]
print(longestCommonSubsequence(A, B)) # 输出 4
1.5. 代码解释
- 初始化 DP 表:使用二维列表
dp
存储子问题的解,dp[i][j]
表示A[0...i-1]
和B[0...j-1]
之间的 LCS 长度。 - 状态转移:
- 如果
A[i-1] == B[j-1]
,说明这两个元素属于 LCS,dp[i][j]
的值为dp[i-1][j-1] + 1
。 - 否则,LCS 的长度是
dp[i-1][j]
和dp[i][j-1]
中的最大值。
- 如果
- 最终结果:
dp[m][n]
就是A
和B
之间的 LCS 长度。
1.6. 时间复杂度与空间复杂度
- 时间复杂度:
O(m * n)
,其中m
和n
分别是数组A
和数组B
的长度。每个元素的计算都需要O(1)
的时间,最终填充一个m * n
的 DP 表。 - 空间复杂度:
O(m * n)
,需要存储m * n
的 DP 表。
2. 优化空间
我们可以通过观察到的规律,减少 dp
表的空间复杂度。从上面的递推关系来看,计算 dp[i][j]
只依赖于 dp[i-1][j-1]
、dp[i-1][j]
和 dp[i][j-1]
,因此只需要保存 当前行 和 前一行 即可。
优化空间的实现
def longestCommonSubsequence(A, B):
m, n = len(A), len(B)
# 初始化前一行和当前行
prev = [0] * (n + 1)
curr = [0] * (n + 1)
# 填充 DP 表
for i in range(1, m + 1):
for j in range(1, n + 1):
if A[i - 1] == B[j - 1]:
curr[j] = prev[j - 1] + 1
else:
curr[j] = max(prev[j], curr[j - 1])
prev = curr[:] # 更新前一行
return curr[n]
# 示例
A = [1, 3, 4, 1]
B = [1, 2, 3, 4, 1]
print(longestCommonSubsequence(A, B)) # 输出 4
2.1. 代码解释
- 我们只维护 当前行 (
curr
) 和 前一行 (prev
),每次计算完一行后,将当前行赋值给前一行,减少空间消耗。
2.2. 时间与空间复杂度
- 时间复杂度:依然是
O(m * n)
,因为每个元素依然需要计算。 - 空间复杂度:优化后是
O(n)
,只需要存储n + 1
的两行数组。
3. 总结
通过动态规划解决 两个数组的 LCS 问题,我们可以高效地计算出两个数组之间的最长公共子序列。关键的步骤是定义好状态转移方程,并通过二维数组存储子问题的解,最终求解出整个问题的答案。
- 在本例中,使用
dp[i][j]
表示A[0...i-1]
和B[0...j-1]
的最长公共子序列长度。 - 空间优化可以通过保存两行来进一步减少空间复杂度。
这种类型的 两个数组的动态规划问题 不仅是 LCS,类似的应用还有最长公共子串、编辑距离等问题,思路和技巧都非常相似。
发表回复