最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次,且总出现次数有限制)
题目描述
给定两个字符串 s1 和 s2,以及一个字符集合 C(例如 C = {'a', 'b'}),要求找到 s1 和 s2 的最长公共子序列(LCS),但满足以下额外限制:
- 必须按顺序出现:子序列中所有属于
C的字符必须按照它们在s1和s2中的原始顺序出现。 - 连续出现要求:对于每个字符
c ∈ C,如果在子序列中出现,则必须连续出现至少 k 次(例如k=2,则"aa"有效,但单个"a"无效)。 - 总出现次数限制:子序列中所有
C字符的总出现次数不超过T(例如T=5)。
示例
设 s1 = "aabcde", s2 = "acbdab", C = {'a', 'b'}, k=2, T=4。
有效 LCS 示例:"acbd"(其中 'a' 和 'b' 各出现 2 次,总次数 4 ≤ T,且连续出现次数 ≥2)。
无效 LCS:"abc"('a' 和 'b' 未连续出现至少 2 次)。
解题过程
步骤 1:问题分析
本题是 LCS 的复杂变种,需在状态转移中跟踪以下信息:
- 当前匹配的字符位置(
i在s1,j在s2)。 - 当前子序列中
C字符的连续出现次数(针对每个c ∈ C)。 C字符的总出现次数(不能超过T)。
由于连续次数和总次数的组合可能很多,需设计高效的状态表示。
步骤 2:状态设计
定义动态规划状态:
dp[i][j][t][last_char][last_count] 表示考虑 s1 的前 i 个字符和 s2 的前 j 个字符时,C 字符总出现次数为 t,且最后一个字符是 last_char(last_char=0 表示非 C 字符,否则为字符索引),该字符已连续出现 last_count 次的情况下的 LCS 长度。
但直接实现状态数过大(O(n*m*T*|C|*k)),需优化:
- 仅当
last_char ∈ C时才记录last_count,否则last_count=0。 - 由于
k固定,last_count只需记录0到k(超过k的视为k,因为连续次数 ≥k 即满足要求)。
状态简化:
dp[i][j][t][last][cnt]:
i:s1 索引(0..n)j:s2 索引(0..m)t:总次数(0..T)last:最后字符类型(0:非C,1..|C|:C中字符索引)cnt:最后字符连续次数(0..k)
步骤 3:状态转移
对于每个状态 (i, j, t, last, cnt),考虑下一步选择:
情况 1:跳过 s1[i]
新状态:(i+1, j, t, last, cnt),LCS 长度不变。
情况 2:跳过 s2[j]
新状态:(i, j+1, t, last, cnt),LCS 长度不变。
情况 3:匹配 s1[i] 和 s2[j](当 s1[i] == s2[j] 时)
设匹配字符为 ch = s1[i],分情况:
-
ch 不属于 C:
新状态:last' = 0,cnt' = 0,t' = t,LCS 长度 +1。
更新dp[i+1][j+1][t'][0][0]。 -
ch 属于 C:
设字符索引idx = index(ch in C)。- 若
last == idx(连续相同字符):
新连续次数cnt' = min(k, cnt + 1)。
总次数t' = t + 1(仅当cnt < k时增加,因为连续次数从k-1到k不增加总次数?——不对,每次出现都计数,但连续次数只影响合法性)。
正确做法:总次数始终 +1,但需检查t+1 ≤ T。 - 若
last != idx或last=0(新字符或之前非C):
新连续次数cnt' = 1,总次数t' = t + 1。
但需注意:若cnt' < k且last == idx,则当前字符不满足连续 k 次要求,不能直接加入?——实际上,我们允许中间过程不满足,但最终子序列中每个 C 字符段必须 ≥k。因此,在状态转移时,仅当完成一段连续字符(即下次字符变化时)才检查上一段是否满足 ≥k。
- 若
修正:更简单的做法是,只在匹配字符 ch ∈ C 时,检查是否可形成连续 k 次的段。我们可以预先记录每个字符在 s1 和 s2 中的连续段信息,然后在状态转移时,如果匹配的字符是 C 字符,且当前连续次数达到 k,则允许将其作为一段的结束。
步骤 4:简化实现
由于状态复杂,实际编码时可采用记忆化搜索,从 (0,0,0,0,0) 开始递归:
-
如果
i==n或j==m,返回 0。 -
如果
s1[i] == s2[j]:- 如果
ch ∉ C,直接匹配,递归(i+1,j+1,t,0,0) + 1。 - 如果
ch ∈ C:
设idx,计算新连续次数new_cnt:- 若
last == idx:new_cnt = cnt + 1 - 否则:
new_cnt = 1
若new_cnt < k:只能继续累积,递归(i+1,j+1,t+1,idx,new_cnt) + 1。
若new_cnt == k:完成一段,递归(i+1,j+1,t+1,idx,k) + 1,同时允许后续重新开始(last=0, cnt=0)?不对,完成一段后,后续可继续该字符或换字符。实际上,完成 k 次后,再出现相同字符可继续累积(但总次数增加),不同字符则新开一段。
关键限制:总次数 t ≤ T。
- 若
- 如果
-
跳过 s1[i] 或 s2[j] 的情况类似。
取所有情况的最大值。
步骤 5:边界与答案
答案:所有 i=n, j=m 的状态中,满足每个 C 字符段长度 ≥k(即 last=0 或 cnt≥k)的最大 LCS 长度。
注意:递归中需传递“当前段是否满足”的标志,或最后检查所有段满足。
步骤 6:复杂度优化
状态数 O(n*m*T*|C|*k),若参数大需优化:
- 可忽略
last和cnt当最后字符非 C。 - 使用滚动数组减少空间。
总结
本题通过扩展 LCS 状态,加入对特定字符连续出现次数和总次数的限制,体现了动态规划处理复杂约束的能力。核心是合理设计状态表示,确保所有限制条件在转移中被正确维护。