线性动态规划:最长公共子序列的变种——带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现)
字数 2237 2025-11-02 10:11:13
线性动态规划:最长公共子序列的变种——带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现)
题目描述
给定两个字符串 s 和 t,以及一个字符集合 C(例如 C = {'a', 'b'}),要求找到 s 和 t 的最长公共子序列(LCS),且该子序列必须满足:对于集合 C 中的每个字符,如果它在子序列中出现,则必须连续出现至少 k 次(例如 k=2)。如果无法满足条件,返回 0。
示例
- 输入:
s = "aababc", t = "aabdbc", C = {'a'}, k = 2 - 解释:普通 LCS 可能是
"aabab"(长度 5),但'a'在子序列中未连续出现至少 2 次(例如"aabab"中'a'被'b'隔断)。满足条件的 LCS 是"aaba"('a'连续出现 2 次以上),但需验证在t中的匹配情况。实际最长可行子序列是"aab"('a'连续出现 2 次)。
解题思路
-
状态定义
设dp[i][j][x]表示考虑s的前i个字符和t的前j个字符时,当前已匹配的子序列末尾连续字符为x(x属于C或特殊标记0表示非C字符),且连续出现次数的状态。但直接记录连续次数会导致状态爆炸,需优化。- 简化:将问题拆分为两种模式:
- 模式 A:当前子序列末尾是
C中的字符,且正在累计连续次数。 - 模式 B:当前子序列末尾是非
C字符或C字符但未累计连续次数。
- 模式 A:当前子序列末尾是
- 简化:将问题拆分为两种模式:
-
状态转移
- 定义
dp[i][j][c][cnt]为考虑s[0..i-1]和t[0..j-1],当前子序列末尾字符为c(c属于C或空标记),且c已连续出现cnt次时的最长长度。 - 初始状态:
dp[0][j][*][*] = 0,dp[i][0][*][*] = 0。 - 转移方程:
- 不选
s[i-1]或t[j-1]:
dp[i][j][c][cnt] = max(dp[i-1][j][c][cnt], dp[i][j-1][c][cnt]) - 当
s[i-1] == t[j-1]时,设字符ch = s[i-1]:- 若
ch不属于C:可直接追加,重置状态为ch出现 1 次(或非C模式)。 - 若
ch属于C:- 若前状态字符也是
ch,则连续次数cnt增加 1,但需保证cnt ≤ k? - 若前状态字符不是
ch,则需检查是否满足k次连续的条件后才能切换字符。
- 若前状态字符也是
- 若
- 不选
- 定义
-
连续字符约束处理
- 关键:只有当
ch属于C且连续出现次数cnt >= k时,才允许在子序列中切换到其他字符。 - 实现时,可用辅助状态
free表示当前无连续字符约束(即末尾是非C字符,或C字符已满足k次连续)。
- 关键:只有当
-
优化状态设计
- 将状态简化为
dp[i][j][lastChar][continuousCount],其中lastChar为当前末尾字符(用索引表示),continuousCount记录连续次数。 - 初始化:
dp[0][0][0][0] = 0(lastChar=0表示无字符)。 - 转移:
- 忽略
s[i]或t[j]:继承状态。 - 匹配
s[i]和t[j]:- 如果
s[i]不属于C,则可直接追加,重置连续次数为 1。 - 如果
s[i]属于C:- 若前状态字符也是
s[i],则连续次数 +1。 - 若前状态字符不是
s[i],则必须前状态字符为非C或已满足k次连续,才能切换。
- 若前状态字符也是
- 如果
- 忽略
- 将状态简化为
-
最终答案
- 遍历所有
i,j,以及所有可能的lastChar和continuousCount,取最大值,且需保证当前状态满足约束(若lastChar属于C,则continuousCount >= k或为 0)。
- 遍历所有
详细步骤
- 初始化 DP 表,尺寸
[len(s)+1][len(t)+1][|C|+2][k+1](多出的两个状态用于非C字符和空字符)。 - 遍历
i从 0 到len(s),j从 0 到len(t):- 对于每个状态
(lastChar, cnt):- 继承不选当前字符的状态。
- 若
s[i-1] == t[j-1]:- 计算新连续次数:若字符匹配且与前状态字符相同,则
newCnt = cnt + 1,否则newCnt = 1。 - 检查约束:若新字符属于
C且newCnt < k,则只能更新当前字符的状态,不能切换到其他字符。 - 更新 DP 值。
- 计算新连续次数:若字符匹配且与前状态字符相同,则
- 对于每个状态
- 答案取所有满足约束的状态的最大值。
复杂度分析
- 时间复杂度:
O(n * m * |C| * k),其中n和m为字符串长度。 - 空间复杂度:可通过滚动数组优化到
O(m * |C| * k)。
关键点
- 通过状态
(lastChar, continuousCount)刻画连续字符约束。 - 在状态转移时,严格检查是否允许切换字符。
- 初始化需注意空状态的边界处理。