动态规划:两个数组的 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,类似的应用还有最长公共子串、编辑距离等问题,思路和技巧都非常相似。
发表回复