最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列
字数 785 2025-11-19 23:48:18
最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列
让我为您讲解一个有趣的线性动态规划问题:在经典最长公共子序列(LCS)的基础上,增加字符必须按特定顺序出现的限制条件。
问题描述
给定两个字符串s和t,以及一个必须按顺序出现的字符序列p,求s和t的最长公共子序列,且该子序列必须包含p作为其子序列(即p中的字符必须按顺序出现在结果中)。
输入:
- 字符串s,长度为n
- 字符串t,长度为m
- 必须包含的模式p,长度为k
输出:
- 满足条件的最长公共子序列的长度
示例:
s = "abcde", t = "acebd", p = "bd"
结果:4 (子序列"abde"或"acbd")
解题思路
这个问题可以看作是经典LCS问题的扩展,我们需要在寻找s和t的公共子序列时,确保结果包含p作为子序列。
动态规划状态定义
我们定义三维DP数组:
dp[i][j][l] 表示考虑s的前i个字符、t的前j个字符,且已经匹配了p的前l个字符时的最长公共子序列长度
其中:
- 0 ≤ i ≤ n
- 0 ≤ j ≤ m
- 0 ≤ l ≤ k
状态转移方程
情况1:s[i] == t[j]
- 如果s[i] == p[l](当前字符匹配模式p的第l个字符)
dp[i][j][l] = max(dp[i-1][j-1][l-1] + 1, dp[i-1][j-1][l] + 1) - 否则
dp[i][j][l] = dp[i-1][j-1][l] + 1
情况2:s[i] != t[j]
dp[i][j][l] = max(dp[i-1][j][l], dp[i][j-1][l])
情况3:模式匹配的转移
即使s[i] != t[j],如果s[i] == p[l]且能在t中找到匹配,或者t[j] == p[l]且能在s中找到匹配,我们也可以推进模式匹配。
详细解题步骤
步骤1:初始化
# 创建三维DP数组,大小为(n+1) x (m+1) x (k+1)
dp = [[[0] * (k+1) for _ in range(m+1)] for _ in range(n+1)]
# 初始化边界条件
for l in range(k+1):
for i in range(n+1):
dp[i][0][l] = -float('inf') # t为空字符串时无法形成有效序列
for j in range(m+1):
dp[0][j][l] = -float('inf') # s为空字符串时无法形成有效序列
dp[0][0][0] = 0 # 空字符串匹配空模式
步骤2:填充DP表
for i in range(1, n+1):
for j in range(1, m+1):
for l in range(k+1):
# 情况1:s[i-1] == t[j-1]
if s[i-1] == t[j-1]:
if l > 0 and s[i-1] == p[l-1]:
# 当前字符匹配模式字符
dp[i][j][l] = max(
dp[i-1][j-1][l-1] + 1, # 使用当前字符匹配模式
dp[i-1][j-1][l] + 1 # 使用当前字符但不匹配模式
)
else:
# 当前字符不匹配模式字符
dp[i][j][l] = dp[i-1][j-1][l] + 1
# 情况2:s[i-1] != t[j-1]
else:
dp[i][j][l] = max(dp[i-1][j][l], dp[i][j-1][l])
# 情况3:检查是否可以通过跳过字符来推进模式匹配
if l > 0:
if s[i-1] == p[l-1]:
# 在t中寻找匹配p[l-1]的字符
for jj in range(j):
if t[jj] == p[l-1]:
dp[i][j][l] = max(dp[i][j][l], dp[i-1][jj][l-1] + 1)
if t[j-1] == p[l-1]:
# 在s中寻找匹配p[l-1]的字符
for ii in range(i):
if s[ii] == p[l-1]:
dp[i][j][l] = max(dp[i][j][l], dp[ii][j-1][l-1] + 1)
步骤3:优化实现
上面的实现有O(n×m×k×max(n,m))的时间复杂度,我们可以优化到O(n×m×k):
# 预计算辅助数组来优化
prev_s = [[-1] * (k+1) for _ in range(n+1)]
prev_t = [[-1] * (k+1) for _ in range(m+1)]
for l in range(1, k+1):
# 对于s,记录每个位置之前最近的出现p[l-1]的位置
last = -1
for i in range(1, n+1):
if s[i-1] == p[l-1]:
last = i
prev_s[i][l] = last
# 对于t,记录每个位置之前最近的出现p[l-1]的位置
last = -1
for j in range(1, m+1):
if t[j-1] == p[l-1]:
last = j
prev_t[j][l] = last
# 填充DP表(优化版)
for i in range(1, n+1):
for j in range(1, m+1):
for l in range(k+1):
if s[i-1] == t[j-1]:
if l > 0 and s[i-1] == p[l-1]:
dp[i][j][l] = max(dp[i-1][j-1][l-1] + 1, dp[i-1][j-1][l] + 1)
else:
dp[i][j][l] = dp[i-1][j-1][l] + 1
else:
dp[i][j][l] = max(dp[i-1][j][l], dp[i][j-1][l])
# 使用预计算数组优化模式匹配
if l > 0:
pi = prev_s[i][l] # s中在i之前最近的出现p[l-1]的位置
pj = prev_t[j][l] # t中在j之前最近的出现p[l-1]的位置
if pi != -1 and pj != -1:
dp[i][j][l] = max(dp[i][j][l], dp[pi-1][pj-1][l-1] + 1)
步骤4:获取最终结果
# 最终答案是dp[n][m][k],即完整匹配模式p时的最长公共子序列
result = dp[n][m][k] if dp[n][m][k] > 0 else -1
复杂度分析
- 时间复杂度:O(n×m×k)
- 空间复杂度:O(n×m×k)
示例验证
以s="abcde", t="acebd", p="bd"为例:
- 有效子序列:"abde"(长度4)
- 计算过程会找到这个包含"bd"的最长公共子序列
这个算法通过三维动态规划巧妙地处理了字符必须按顺序出现的约束条件,是经典LCS问题的有趣扩展。