线性动态规划:最长公共子序列的变种——带限制的最长公共子序列
题目描述
给定两个字符串 s1 和 s2,以及一个字符串 evil,要求找出 s1 和 s2 的最长公共子序列(LCS),且该子序列中不包含 evil 作为连续子串。返回满足条件的最长公共子序列的长度。如果不存在这样的子序列,返回 0。
示例
输入:
s1 = "abcde", s2 = "ace", evil = "c"
输出:2
解释:s1 和 s2 的 LCS 是 "ace",但 "ace" 包含子串 "c",因此不合法。合法的 LCS 是 "ae" 或 "ce",长度为 2。
解题过程
步骤1:理解问题与常规LCS的区别
常规 LCS 使用动态规划,定义 dp[i][j] 表示 s1[0..i-1] 和 s2[0..j-1] 的 LCS 长度。本题增加限制:LCS 不能包含 evil 子串。这意味着我们需要在状态中跟踪当前已匹配的 evil 前缀长度,避免匹配到完整的 evil。
步骤2:状态设计
定义 dp[i][j][k]:
i:考虑s1的前i个字符(0 ≤ i ≤ len(s1))j:考虑s2的前j个字符(0 ≤ j ≤ len(s2))k:当前公共子序列的后缀与evil的最长匹配前缀长度(0 ≤ k < len(evil))
状态值:在满足不出现完整evil的条件下,当前的最长公共子序列长度。
注意:k 不能等于 len(evil),因为那样意味着已出现 evil 子串,非法状态。
步骤3:状态转移
我们需要考虑两种情况:
- 不选当前字符:继承之前的状态
- 跳过
s1[i-1]:dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k]) - 跳过
s2[j-1]:dp[i][j][k] = max(dp[i][j][k], dp[i][j-1][k])
- 跳过
- 选当前字符(仅当
s1[i-1] == s2[j-1]时):- 设当前字符为
c = s1[i-1]。 - 我们需要计算加入
c后,evil的匹配前缀长度如何变化。这可以通过 KMP 的失配函数(next数组)预处理。
- 设当前字符为
预处理next数组:
对 evil 计算前缀函数 next[m],其中 next[t] 表示 evil[0..t-1] 的最长公共真前后缀长度。
定义函数 getNextState(k, c):从当前匹配长度 k 开始,加入字符 c 后,新的匹配长度。
- 如果
evil[k] == c,则新匹配长度 =k+1 - 否则,回溯到
next[k]继续匹配,直到匹配或为0。
状态转移方程(选字符):
如果 s1[i-1] == s2[j-1],设 c = s1[i-1],计算 nk = getNextState(k, c)。
如果 nk < len(evil)(未形成完整evil),则:
dp[i][j][nk] = max(dp[i][j][nk], dp[i-1][j-1][k] + 1)
步骤4:初始化
dp[0][j][0] = 0,dp[i][0][0] = 0:空字符串与任何字符串的LCS为0,且匹配长度为0。
其他状态初始化为负无穷(表示不可达)。
步骤5:答案提取
答案是所有 dp[len(s1)][len(s2)][k](0 ≤ k < len(evil))的最大值。
步骤6:复杂度分析
- 时间复杂度:O(n * m * L),其中 n = len(s1), m = len(s2), L = len(evil)
- 空间复杂度:可优化到 O(m * L)
总结
本题通过将 evil 的匹配状态融入DP,结合KMP的失配处理,有效避免了非法子串的出现。关键在于理解状态 k 表示当前已匹配的 evil 前缀长度,并在加入字符时更新该状态。