线性动态规划:带通配符的最长公共子序列(进阶版:允许通配符匹配任意字符,且通配符可匹配空字符,并允许最多跳过K个不匹配字符)
题目描述
给定两个字符串s和t,以及一个包含通配符''的字符集,其中''可以匹配零个或任意多个任意字符。现在要求找出s和t的最长公共子序列,但允许在匹配过程中跳过最多K个不匹配的字符(即这些字符可以被忽略,不计入子序列,但会增加不匹配计数)。要求返回最长公共子序列的长度。
示例
s = "abcd"
t = "abxcyd"
K = 1
解释:''可以匹配任意字符(包括空字符),但这里我们还有一个额外的跳过机制。一个可能的最长公共子序列是"abcd"(其中'*'匹配了空字符,而'x'被跳过,用掉了K=1次跳过机会),所以长度为4。
解题步骤
第一步:理解问题与核心难点
这是一个在经典最长公共子序列(LCS)问题上的双重扩展:
- 通配符'*'可以匹配零个或多个任意字符。这本身就是一个变体。
- 额外允许跳过最多K个字符(从s或t中跳过),跳过不计入子序列但消耗跳过次数。
- 需要同时处理通配符的灵活匹配和跳过机制。
第二步:定义状态
设dp[i][j][k]表示考虑s的前i个字符、t的前j个字符、已经跳过了k个字符时,能够匹配的最长子序列长度。
这里i, j从0开始,k从0到K。
注意:跳过可以发生在s或t的任意位置,跳过意味着忽略当前字符,不匹配,但占用一次跳过机会。
第三步:状态转移方程
我们需要考虑三种操作:匹配、使用通配符、跳过字符。
-
匹配:如果s[i-1] == t[j-1](字符相等),那么可以直接匹配这两个字符,子序列长度+1,跳过次数k不变:
dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-1][k] + 1) -
通配符处理:如果s[i-1] == '*',这个通配符可以匹配零个或多个t中的字符。
- 匹配0个字符:相当于忽略s的当前'*',状态转移为dp[i-1][j][k]
- 匹配1个字符:相当于用''匹配t[j-1],然后继续考虑s[i]和t[j-1](因为''可匹配多个,这里我们让''继续保留以便可能匹配更多),状态转移为dp[i][j-1][k] + 1
注意:这里实际上我们需要处理''匹配多个的情况,但通过允许dp[i][j-1]转移,可以隐式实现多次匹配,因为i不变,j减少,相当于用同一个''匹配t的连续字符。
类似地,如果t[j-1] == '',也做对称处理。
-
跳过操作:我们可以选择跳过s的当前字符或t的当前字符,这会消耗一次跳过机会(k+1),但子序列长度不增加。
- 跳过s[i-1]:dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k+1]) // 注意这里k是当前剩余跳过次数,消耗后变为k+1,但为了dp表索引一致性,我们用k表示已跳过次数,则转移应为:dp[i][j][k+1] = max(..., dp[i-1][j][k]),但这样写不方便。我们重新定义k为已使用的跳过次数,则跳过时k+1不超过K。
我们重新统一:k表示已使用的跳过次数,0<=k<=K。
那么跳过s[i-1]:如果k+1 <= K, dp[i][j][k+1] = max(dp[i][j][k+1], dp[i-1][j][k])
跳过t[j-1]:如果k+1 <= K, dp[i][j][k+1] = max(dp[i][j][k+1], dp[i][j-1][k])
注意这里子序列长度不变。
- 跳过s[i-1]:dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k+1]) // 注意这里k是当前剩余跳过次数,消耗后变为k+1,但为了dp表索引一致性,我们用k表示已跳过次数,则转移应为:dp[i][j][k+1] = max(..., dp[i-1][j][k]),但这样写不方便。我们重新定义k为已使用的跳过次数,则跳过时k+1不超过K。
-
不匹配也不跳过:直接继承之前状态,即dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k], dp[i][j-1][k])
第四步:初始化
dp[0][j][k] = 0 对任意j,k(s为空,只能跳过t的字符,但跳过次数有限,所以匹配长度为0)
dp[i][0][k] = 0 对任意i,k
dp[0][0][k] = 0
注意:如果s或t开头有'*',它们可以匹配空字符,但不会增加长度,所以初始化为0是合理的。
第五步:计算顺序
i从0到len(s),j从0到len(t),k从0到K。注意转移时可能需要从较小的k转移到较大的k(跳过时),所以k循环放在最内层,并且从小到大。
第六步:答案
最终答案是max_{k=0..K} dp[len(s)][len(t)][k],即在所有可能的跳过次数下,最大的子序列长度。
第七步:示例推演
以s="abcd", t="abxcyd", K=1为例。
dp[1][1][0] = 1 (匹配a)
dp[2][2][0] = 2 (匹配b)
dp[3][3][0] = 2 (c != x, 不匹配)
dp[3][4][0] = 2 (c != c? 这里t[3]是x, 不对) 我们一步步来:
实际上,我们可以用跳过:跳过t中的x,此时dp[3][4][1] = dp[3][3][0] = 2(跳过x,长度不变,跳过次数+1)
之后遇到'','*'可以匹配t中的'c'(t[4]是c),所以dp[4][5][1] = dp[4][4][1] + 1? 细节略,但最终可以得到dp[5][6][1] = 4。
第八步:优化与思考
本题状态数O(nmK),转移O(1),在K不大时可接受。注意通配符的处理需要小心,确保'*'匹配多个字符的逻辑正确。
另外,如果跳过次数K很大,接近n+m,则问题退化为允许任意跳过,即求带通配符的LCS,此时K的维度可以省略。
这就是这道题目的完整解法。它结合了通配符匹配、跳过机制和LCS,是一个综合性较强的线性DP问题。