最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次)
题目描述
给定两个字符串s和t,以及一个字符集C和一个整数k。要求找出s和t的最长公共子序列,且满足:
- 对于字符集C中的每个字符,如果在子序列中出现,则必须连续出现至少k次
- 子序列中字符的相对顺序必须与在s和t中出现的相对顺序一致
例如:
s = "abcde", t = "ace", C = {'c'}, k = 1
满足条件的最长公共子序列是"ace"(长度为3)
s = "aabcc", t = "aacc", C = {'c'}, k = 2
满足条件的最长公共子序列是"aacc"(长度为4),而不是"aabc"(因为'c'没有连续出现2次)
解题过程
步骤1:理解问题本质
这是一个带约束的最长公共子序列问题。我们需要在传统的LCS基础上,增加对特定字符连续出现次数的限制。
步骤2:定义状态
设dp[i][j][c][cnt]表示考虑s的前i个字符和t的前j个字符,以字符c结尾且该字符已经连续出现cnt次的最长公共子序列长度。
但由于字符集可能很大,这种状态定义会占用过多空间。我们可以优化为:
dp[i][j][len]表示考虑s的前i个字符和t的前j个字符,当前匹配的子序列以某个字符结尾且该字符已经连续出现len次的最长公共子序列长度。
步骤3:状态转移方程
对于每个位置(i, j),我们考虑三种情况:
-
不选择s[i]和t[j]
dp[i][j][len] = max(dp[i-1][j][len], dp[i][j-1][len]) -
s[i] == t[j]时,选择该字符
如果当前字符等于前一个字符(即连续出现):
dp[i][j][len] = max(dp[i][j][len], dp[i-1][j-1][len-1] + 1) (当len > 1)如果当前字符是新的字符(即开始新的连续段):
dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-1][prev_len] + 1) (对于所有prev_len) -
检查约束条件
对于字符集C中的字符,只有当连续出现次数cnt >= k时,我们才认为这个匹配是有效的。
步骤4:优化状态设计
由于直接使用四维DP可能空间太大,我们可以重新设计状态:
设dp[i][j]表示考虑s的前i个字符和t的前j个字符时的最长有效子序列长度
设last[i][j]记录当前连续段的字符和长度
更实用的方法是使用:
dp[i][j] = 常规LCS的DP值
special[i][j][c] = 以字符c结尾且满足连续条件的特殊子序列长度
步骤5:具体算法实现
def longest_common_subsequence_with_constraint(s, t, C, k):
m, n = len(s), len(t)
# dp[i][j] 表示s[0:i]和t[0:j]的最长有效公共子序列长度
dp = [[0] * (n + 1) for _ in range(m + 1)]
# cont[i][j] 记录以当前字符结尾的连续段长度
cont = [[0] * (n + 1) for _ in range(m + 1)]
# char[i][j] 记录当前连续段的字符
char = [[''] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s[i-1] == t[j-1]:
current_char = s[i-1]
# 情况1:不选择当前字符
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# 情况2:选择当前字符
if char[i-1][j-1] == current_char:
# 连续出现
new_cont = cont[i-1][j-1] + 1
new_len = dp[i-1][j-1] + 1
# 检查是否满足约束
if current_char in C:
if new_cont >= k:
if new_len > dp[i][j]:
dp[i][j] = new_len
cont[i][j] = new_cont
char[i][j] = current_char
else:
# 不满足约束,不能更新
pass
else:
# 不在约束字符集中,直接更新
if new_len > dp[i][j]:
dp[i][j] = new_len
cont[i][j] = new_cont
char[i][j] = current_char
else:
# 新的字符段
new_len = dp[i-1][j-1] + 1
if current_char in C:
# 约束字符,必须检查是否能开始新的连续段
if 1 >= k or (i > 1 and j > 1 and dp[i-1][j-1] > 0):
# 可以开始新段(要么长度够,要么前面有内容)
if new_len > dp[i][j]:
dp[i][j] = new_len
cont[i][j] = 1
char[i][j] = current_char
else:
# 非约束字符,直接更新
if new_len > dp[i][j]:
dp[i][j] = new_len
cont[i][j] = 1
char[i][j] = current_char
else:
# 字符不匹配,取左边或上边的最大值
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# 继承连续信息(选择值更大的那个)
if dp[i-1][j] > dp[i][j-1] or (dp[i-1][j] == dp[i][j-1] and dp[i-1][j] > 0):
cont[i][j] = cont[i-1][j]
char[i][j] = char[i-1][j]
else:
cont[i][j] = cont[i][j-1]
char[i][j] = char[i][j-1]
return dp[m][n]
步骤6:算法分析
- 时间复杂度:O(m×n),其中m和n分别是字符串s和t的长度
- 空间复杂度:O(m×n),可以通过滚动数组优化到O(n)
步骤7:示例验证
例1:s = "abcde", t = "ace", C = {'c'}, k = 1
结果:3(子序列"ace")
例2:s = "aabcc", t = "aacc", C = {'c'}, k = 2
结果:4(子序列"aacc",'c'连续出现2次)
这个算法在传统LCS的基础上,通过维护连续字符的信息来确保约束条件得到满足,是线性动态规划中处理带约束问题的典型范例。