动态规划问题之最长公共子序列
1. 理论原理推导
假设有两个序列:
-
序列
-
序列
子问题定义:
令 表示序列
和
的最长公共子序列的长度。
递推公式:
对于任意 和
,我们考虑两个位置上的元素
和
:
-
如果
,说明这两个元素可以同时出现在公共子序列中,此时:
-
如果
,那么最长公共子序列不可能同时包含这两个元素,只能在两种可能中取最大值:
边界条件:
-
当 i=0 或 j=0 时,意味着其中一个序列为空,此时
。
这一推导基于最优子结构性质:最长公共子序列问题的最优解可以由其子问题的最优解构造得到。
2. 时间复杂度推导
-
状态个数:
动态规划表 dp 的大小为 (m+1)×(n+1) 。 -
状态转移:
每个状态的计算只涉及常数时间的比较和加法操作。
因此,总的时间复杂度为:O(m×n)
空间复杂度也为 O(m×n) ;若通过空间优化(例如仅保留当前行和上一行),可降为 。
3. 算法步骤
下面是基于上述理论推导的动态规划求解 LCS 的具体步骤:
-
初始化:
-
构建一个大小为 (m+1)×(n+1)的二维数组
dp
,所有元素初始化为 0。 -
行和列的索引从 0 开始,令
(对于所有 j)和
(对于所有 i)。
-
-
状态转移:
-
对于 i 从 1 到 m:
-
对于 j 从 1 到 n:
-
若
,则令:
-
否则:
-
-
-
-
结果输出:
-
最终,
即为序列 X 和 Y 的最长公共子序列的长度。
-
-
(可选)路径回溯:
-
若需要重构出一个最长公共子序列,可以从
开始,逆向回溯:
-
当
时,该元素属于 LCS,记录下来,并同时减小 i 和 j;
-
当
时,移动到
;否则移动到
。
-
-
4. Python 代码示例
下面提供一个 Python 代码示例,既计算 LCS 的长度,也重构出一个 LCS:
def lcs_length(X, Y):
m, n = len(X), len(Y)
# 创建 (m+1) x (n+1) 的二维 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 X[i - 1] == Y[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
def construct_lcs(dp, X, Y):
"""
根据 dp 表从 X 和 Y 中回溯构造最长公共子序列
"""
i, j = len(X), len(Y)
lcs = []
while i > 0 and j > 0:
# 如果两个元素相等,加入 LCS
if X[i - 1] == Y[j - 1]:
lcs.append(X[i - 1])
i -= 1
j -= 1
else:
# 否则,沿着较大值的方向回溯
if dp[i - 1][j] >= dp[i][j - 1]:
i -= 1
else:
j -= 1
lcs.reverse() # 反转得到正确顺序
return lcs
def main():
# 示例序列,可以根据需要替换成任意两个序列
X = "ABCBDAB"
Y = "BDCAB"
# 计算 dp 表
dp = lcs_length(X, Y)
print("最长公共子序列的长度为:", dp[len(X)][len(Y)])
# 重构最长公共子序列
lcs_seq = construct_lcs(dp, X, Y)
print("最长公共子序列为:", ''.join(lcs_seq))
if __name__ == '__main__':
main()