最长公共子序列的变种:带通配符和间隔限制的最长公共子序列(允许通配符匹配任意字符,且匹配字符之间允许最多跳过k个字符)
题目描述
给定两个字符串 s1 和 s2,以及一个非负整数 k。字符串中可能包含通配符 *,它可以匹配任意字符(包括空字符)。现在要求从 s1 和 s2 中分别取出一个子序列,使得这两个子序列相等,并且满足以下条件:
- 子序列中的字符必须按原字符串中的顺序出现。
- 当匹配字符时,如果字符相同,或者其中一个字符是通配符
*,则视为匹配成功。 - 匹配的两个字符在各自原字符串中的位置索引差(即跳过字符的数量)不能超过
k。具体来说,假设从s1中取出的字符位置为i,从s2中取出的字符位置为j,且这是子序列中相邻的两个匹配对,则必须满足|i - 前一个匹配的i| <= k且|j - 前一个匹配的j| <= k。换句话说,在匹配的子序列中,任意两个相邻匹配的字符在各自字符串中的距离(跳过字符数)不能超过k。
求满足条件的最长子序列的长度。
示例
输入:
s1 = "a*b*c"
s2 = "adbpc"
k = 2
解释:
- 通配符
*可以匹配任意字符。 - 最长公共子序列可以是
"abc",匹配方式为:
s1中的 'a'(索引0)匹配 s2中的 'a'(索引0)
s1中的 ''(索引1)匹配 s2中的 'd'(索引1)
s1中的 'b'(索引2)匹配 s2中的 'b'(索引3)
s1中的 ''(索引3)匹配 s2中的 'p'(索引4)
s1中的 'c'(索引4)匹配 s2中的 'c'(索引5)
但注意:相邻匹配的索引差不能超过 k=2。检查:
s1中索引从0到1差为1(≤2),s2中索引从0到1差为1(≤2);
s1中索引从1到2差为1(≤2),s2中索引从1到3差为2(≤2);
s1中索引从2到3差为1(≤2),s2中索引从3到4差为1(≤2);
s1中索引从3到4差为1(≤2),s2中索引从4到5差为1(≤2)。
满足条件,所以长度为5。
如果没有间隔限制(k很大),整个字符串都可以匹配,长度为5。但如果 k=1,则 s2 中 'b' 到 'c' 的索引差为2>1,无法全部匹配,最长可能是4(例如匹配 "ab" 和 "adbp")。
输出:
5
解题步骤
这是一个线性动态规划问题,但状态定义需要同时考虑两个字符串的索引,并且增加一个维度来记录上一个匹配的位置以满足间隔限制。为了降低复杂度,我们采用三维DP。
步骤1:定义状态
设 dp[i][j][p] 表示考虑 s1 的前 i 个字符和 s2 的前 j 个字符,且最后一个匹配的字符在 s1 中的索引是 p 的情况下,所能得到的最长公共子序列长度。
但这样状态会很大(O(n³)),且难以处理间隔限制。我们需要更巧妙的设计。
另一种思路:
定义 dp[i][j] 表示以 s1[i] 和 s2[j] 作为匹配的最后一对字符时,所能得到的最长公共子序列长度。然后,在转移时,我们需要检查之前所有可能的匹配对 (i', j'),满足 i' < i 且 j' < j 且 i - i' <= k 且 j - j' <= k。这样复杂度是 O(n² * k²),在k较小时可行。
步骤2:转移方程
如果 s1[i] 和 s2[j] 可以匹配(即字符相等或有一个是通配符 *),则:
dp[i][j] = 1 + max{ dp[i'][j'] } (其中 i' 满足 0 <= i' < i 且 i - i' <= k, j' 满足 0 <= j' < j 且 j - j' <= k)
如果 s1[i] 和 s2[j] 不能匹配,则 dp[i][j] = 0。
注意:如果之前没有匹配对,则取1(即只匹配当前这对字符)。
步骤3:初始化
如果 s1[i] 和 s2[j] 可匹配,则至少可以形成一个长度为1的子序列,所以 dp[i][j] 至少为1。我们可以初始化为0,然后在可匹配时先赋值为1,再尝试从之前的状态转移。
步骤4:最终答案
最终答案是所有 dp[i][j] 的最大值,因为最长公共子序列可能以任意一对字符作为结尾。
步骤5:复杂度优化
直接枚举所有 i', j' 会导致 O(n² * k²) 的复杂度。如果k很大(接近n),则为 O(n⁴),不可接受。但我们可以用滑动窗口最大值来优化:
对于固定的 i, j,我们需要在矩阵 dp 中区域 [i-k, i-1] x [j-k, j-1] 内找最大值。这可以分解为:
先对每个 i,维护一个关于 j 的滑动窗口最大值(窗口大小k);再对每个 j,维护一个关于 i 的滑动窗口最大值。但这里是二维区域最大值,比较棘手。
一个更简单的方法:由于k通常不会很大,我们可以接受 O(n² * k) 的复杂度,即固定 i, j 后,枚举 i' 从 max(0, i-k) 到 i-1,然后对每个 i',我们需要在 j' 从 max(0, j-k) 到 j-1 中找最大的 dp[i'][j']。我们可以提前用另一个二维数组 rowMax[i'][j] 记录对于固定的 i',在 j 的滑动窗口内的最大值。这样,对于每个 (i, j),枚举 i' 时直接取 rowMax[i'][j-1] 即可。复杂度 O(n² * k)。
具体实现步骤
- 初始化
dp为全0,rowMax为全0。 - 遍历
i从0到 len(s1)-1,遍历j从0到 len(s2)-1。 - 如果
s1[i]和s2[j]可匹配(相等或任意一个是*):
a. 先设dp[i][j] = 1。
b. 枚举i'从max(0, i-k)到i-1:
取prevJ = max(0, j-k)到j-1范围内的最大值,即rowMax[i'][j-1](注意处理边界)。
如果rowMax[i'][j-1] > 0,则dp[i][j] = max(dp[i][j], 1 + rowMax[i'][j-1])。 - 更新
rowMax[i][j]为max(rowMax[i][j-1], dp[i][j])(当 j>0),否则为dp[i][j]。 - 同时更新全局答案
ans = max(ans, dp[i][j])。 - 最终返回
ans。
边界情况处理
- 如果 k=0,则只允许字符一一对应匹配,没有跳过,相当于普通LCS但带通配符。
- 通配符
*可以匹配任意字符,包括另一个通配符。 - 注意字符串可能为空,此时答案为0。
示例推演
以 s1="a*b*c", s2="adbpc", k=2 为例。
略去详细表格,但按上述算法,最终 dp[4][5] 会得到5,对应整个字符串匹配。
总结
这道题是LCS的变种,增加了通配符匹配和相邻匹配字符的间隔限制。通过三维状态的压缩和滑动窗口优化,可以在合理时间内求解。关键点是设计状态转移时考虑间隔限制,并利用辅助数组降低复杂度。