最长公共子序列的变种:允许最多k次跳过的最长公共子序列
字数 7949 2025-12-06 20:13:47

最长公共子序列的变种:允许最多k次跳过的最长公共子序列

题目描述

给定两个字符串s和t,以及一个整数k。我们要找出s和t的最长公共子序列(LCS),但有一个特殊的规则:在匹配过程中,我们允许最多“跳过”k个字符。这里的“跳过”指的是,在构建公共子序列时,对于字符串s或t,我们可以选择跳过其中的某个字符,不计入最终的公共子序列,但会消耗一次“跳过”机会。总跳过次数(在s和t上跳过的字符数之和)不能超过k。求在满足此限制下,能够得到的最长公共子序列的长度。

举个例子
假设 s = "abcde", t = "acef", k = 1。

  • 常规LCS是"ace",长度为3。
  • 允许跳过1次,我们可以跳过s中的'd'或者t中的'f'。最优解是取"ace"(跳过t中的'f'),长度为3。如果k=0,则结果还是3。
    另一个例子:s = "agcat", t = "gac", k = 2。
  • 常规LCS是"ga"或"ac"或"gc",长度为2。
  • 允许跳过2次。一种方案是:匹配s[1]='g'和t[0]='g',跳过t[1]='a',匹配s[3]='a'和t[2]='a',再跳过s[4]='t'。这样我们得到子序列"ga",跳过次数为2(t[1]和s[4]),长度仍为2。能否得到长度为3的?可以尝试匹配"gca":匹配s[1]='g'和t[0]='g',匹配s[2]='c'和t[2]='c'(这里跳过了t[1]='a'和s[3]='a',但我们选择匹配c),但这样我们跳过了'a',得到的子序列是"gc",长度2。实际上,要得到"gca",需要s中的'g','c','a'和t中的'g','c','a'一一对应,但这在原始序列中位置不对齐,需要更多的跳过。通过穷举可知,在k=2时,无法得到长度3。但如果我们增加k,可能能得到更长的序列。

我们的目标是设计一个动态规划算法,精确计算出在最多跳过k个字符的前提下,最长公共子序列的长度。

解题思路

这个问题是经典LCS问题的变种。经典LCS的DP定义是:dp[i][j]表示s的前i个字符和t的前j个字符的LCS长度。状态转移为:

  1. 如果s[i-1] == t[j-1],则dp[i][j] = dp[i-1][j-1] + 1。
  2. 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。

现在引入跳过次数限制。我们需要在状态中增加一维,用来记录已经使用的跳过次数。但跳过次数是在两个字符串上发生的,我们关心的是总跳过次数。因此,我们可以定义状态dp[i][j][sk]表示:考虑s的前i个字符和t的前j个字符,并且总共使用了sk次跳过时,能得到的最长公共子序列的长度。

这里i, j, sk的范围是:0 <= i <= |s|, 0 <= j <= |t|, 0 <= sk <= k。

状态转移需要仔细考虑。在每一步,我们面对s的第i个字符(s[i-1])和t的第j个字符(t[j-1]),我们有几种选择:

  1. 匹配:如果s[i-1] == t[j-1],那么我们可以选择将这两个字符匹配,加入LCS。此时,长度加1,且不消耗跳过次数。因此,状态转移为:
    dp[i][j][sk] = max(dp[i][j][sk], dp[i-1][j-1][sk] + 1)。

  2. 不匹配,且有跳过机会可用:如果s[i-1] != t[j-1],我们可以选择跳过s的第i个字符,或者跳过t的第j个字符。但跳过操作会消耗一次跳过次数,并且不会增加LCS长度。

    • 跳过s[i-1]:这意味着我们不考虑s[i-1]参与匹配,那么状态从dp[i-1][j][sk-1]转移过来(注意,使用了一次跳过,所以是sk-1)。条件:sk > 0。
    • 跳过t[j-1]:同理,状态从dp[i][j-1][sk-1]转移过来。条件:sk > 0。
      所以,当s[i-1] != t[j-1]时,如果sk>0,我们有:
      dp[i][j][sk] = max(dp[i][j][sk], dp[i-1][j][sk-1], dp[i][j-1][sk-1])。
  3. 不匹配,且不跳过(即常规LCS的不匹配操作):在经典LCS中,当字符不相等时,我们有dp[i][j] = max(dp[i-1][j], dp[i][j-1])。这个操作实际上对应着“跳过s[i-1]”或“跳过t[j-1]”,但在经典LCS中,这种“跳过”不被计入次数限制。在我们这个变种问题中,我们是否允许这种不计入次数的“跳过”呢?这取决于问题的定义。仔细思考:经典LCS中的“跳过”,其实是一种“忽略”,它不将字符纳入子序列,但也不视为一种“消耗资源”的跳过。在我们的问题中,我们明确“跳过”是消耗资源的。那么,不计入次数的“忽略”是否应该被允许?如果我们允许不计入次数的忽略,那相当于我们总是可以执行常规LCS的转移,那么跳过次数k就失去了限制意义,最终结果会等于常规LCS。因此,在本题的定义下,所有不匹配的情况,如果要将字符排除在考虑之外,都必须消耗一次跳过机会。所以,当我们不想匹配当前字符时,我们必须使用一次跳过机会来跳过s[i-1]或t[j-1]。如果没有跳过机会可用(sk=0),且s[i-1]!=t[j-1],那么我们无法处理这两个字符,状态无法转移,保持为0(或初始值)。但等等,这可能会导致问题:比如s="ab", t="ac",k=0,按照上述规则,由于'a'匹配,'b'和'c'不匹配且无跳过机会,那么dp[2][2][0]只能从匹配'a'得到1,而无法通过跳过'b'或'c'得到1,结果是1,这实际上就是LCS("ab","ac")="a"的长度1,是正确的。如果k=1,我们可以选择跳过'b'或'c',结果还是1。但如果s="a", t="b", k=0,按照规则,dp[1][1][0]无法从任何地方转移,结果为0,这也是正确的(LCS为空)。

综合以上,我们得到状态转移方程:

对于每个i从1到|s|,每个j从1到|t|,每个sk从0到k:

  1. 如果s[i-1] == t[j-1]:
    dp[i][j][sk] = max(dp[i][j][sk], dp[i-1][j-1][sk] + 1)
  2. 如果sk > 0:
    // 考虑跳过s[i-1]或t[j-1]
    dp[i][j][sk] = max(dp[i][j][sk], dp[i-1][j][sk-1], dp[i][j-1][sk-1])
    注意:即使字符相等,我们也可以选择不匹配而跳过,消耗一次跳过机会。但通常情况下,如果字符相等,我们不会选择跳过,因为匹配能增加长度且不消耗跳过次数,更优。所以在实现时,即使字符相等,我们也可以考虑跳过操作,但显然匹配更优,所以可以忽略跳过。但为了完整性,我们可以将跳过操作放在字符不等或通用情况中。由于跳过操作不会使结果更优(相较于匹配),所以可以简化。

初始化
dp[0][j][sk] = 0 对任意j, sk。因为s为空时,LCS长度为0。
dp[i][0][sk] = 0 对任意i, sk。
当sk=0时,dp[i][j][0]就是常规LCS(但注意,常规LCS允许不计次数的忽略,而我们这里当sk=0时不允许跳过,所以当字符不等时,dp[i][j][0]只能从dp[i-1][j-1][0]转移?不对,实际上经典LCS的转移包含dp[i-1][j]和dp[i][j-1],这对应着跳过s[i-1]或t[j-1]但不消耗跳过次数。在我们的定义中,这是不允许的。所以,当sk=0时,状态转移应该为:如果字符相等,dp[i][j][0]=dp[i-1][j-1][0]+1;如果字符不等,dp[i][j][0] = max(dp[i-1][j][0], dp[i][j-1][0])?这会导致矛盾,因为跳过操作被允许但没有消耗次数。所以,必须明确:在sk=0时,不允许任何跳过操作,那么当字符不等时,我们不能从dp[i-1][j][0]或dp[i][j-1][0]转移,因为它们隐含了跳过操作。那么如何计算dp[i][j][0]?实际上,当sk=0时,我们只能匹配相等的字符,不能跳过任何字符。所以,dp[i][j][0]应该等于:s[0..i-1]和t[0..j-1]中连续匹配的最长子序列长度?不,应该是允许从任意位置匹配,但不能跳过。这相当于:我们从两个字符串中按顺序选取相等的字符,但一旦遇到不等的字符,就不能再继续往后匹配了,因为不能跳过。这显然不对,比如s="abc", t="adc", k=0,常规LCS是"ac"长度为2,按照我们的定义,在匹配了'a'之后,遇到b和d不相等,由于不能跳过,我们无法跳过b或d去匹配后面的c,结果只能是1,这就不符合常规LCS了。所以,我们对问题的理解需要调整。

重新审视问题定义:题目中说“允许最多跳过k个字符”,意思是:在构建公共子序列时,我们可以选择忽略(跳过)s或t中的某些字符,这些被忽略的字符不计入子序列,但会消耗跳过次数。而没有跳过的操作,其实就是常规LCS中的“不匹配”情况,在常规LCS中,当我们取max(dp[i-1][j], dp[i][j-1])时,就相当于跳过了s[i-1]或t[j-1],但这种跳过是“免费”的,不消耗跳过次数。而现在,我们给“跳过”操作增加了成本(消耗一次跳过机会),但我们仍然允许“免费”的忽略吗?题目没有明确说明。通常这类问题的设定是:所有的“跳过”操作都计入k次限制。也就是说,原来经典LCS中的“不匹配”操作,现在需要消耗跳过次数。这样,k=0时,我们就不能进行任何跳过,那么只有当两个字符串完全相同时,才能得到LCS长度等于字符串长度。否则,一旦遇到不等的字符,就必须停止,因为不能跳过。这显然不是我们想要的LCS变种。

更合理的解释是:在经典LCS中,我们有两种操作:匹配字符,或者跳过某个字符(即不匹配)。经典LCS中,跳过是免费的。现在,我们给跳过操作加上次数限制,但匹配操作不受限。所以,在状态转移中,当字符不等时,我们可以选择跳过s[i-1]或跳过t[j-1],但这会消耗一次跳过次数。同时,即使字符相等,我们也可以选择不匹配而跳过,但那样会浪费跳过次数,所以通常不会选择。此外,我们仍然允许“免费”的跳过吗?不,如果允许免费跳过,那么跳过次数限制就形同虚设。所以,我们必须要求所有跳过操作都消耗次数。

基于这个理解,我们修正状态转移:

定义dp[i][j][sk]如前。

状态转移:

  1. 如果s[i-1] == t[j-1]:
    我们可以选择匹配:dp[i][j][sk] = dp[i-1][j-1][sk] + 1。
    我们也可以选择不匹配,而是跳过s[i-1]或t[j-1](消耗跳过次数),但这样不会使结果更优,所以可以忽略。
  2. 如果s[i-1] != t[j-1]:
    此时我们不能匹配,只能选择跳过其中一个字符(如果还有跳过次数的话):
    如果sk > 0:
    跳过s[i-1]:从dp[i-1][j][sk-1]转移。
    跳过t[j-1]:从dp[i][j-1][sk-1]转移。
    取两者最大值。
    如果sk == 0,那么我们无法处理当前两个字符,状态无法从i,j转移,但实际上,我们可以从dp[i-1][j][0]或dp[i][j-1][0]转移吗?不可以,因为那意味着我们进行了跳过操作但没有消耗次数,这是不允许的。所以当sk=0且字符不等时,我们无法从任何状态转移,dp[i][j][0]应该等于0?但这样会丢失之前的结果。例如s="ab", t="ac",k=0。i=1,j=1: 'a'=='a',dp[1][1][0]=1。i=1,j=2: 'a'!='c',sk=0,无法跳过,那么dp[1][2][0]应该等于多少?它应该至少等于dp[1][1][0]=1,因为我们可以忽略t[1]='c',但忽略就是跳过,需要消耗次数。所以严格来说,当k=0时,我们只能匹配完全相同的字符序列,一旦遇到不等的字符,后续就无法继续匹配了。但这样得到的结果是:LCS必须是两个字符串的公共前缀,且一旦出现不同字符,匹配就终止。这显然不是我们想要的LCS。

矛盾点在于:经典LCS允许免费跳过,而我们现在不允许免费跳过。为了得到有意义的LCS,我们需要允许“忽略”操作,但忽略操作必须计入跳过次数。这样,k=0时,LCS长度就等于两个字符串完全匹配的最长子序列长度,即要求子序列在两个字符串中出现的字符顺序完全一致且连续?不,即使k=0,我们也可以选择匹配一些字符,跳过一些字符,但跳过消耗次数,k=0就不能跳过,所以只能匹配所有字符,即要求子序列必须是原字符串的子序列,且不能跳过任何字符,这意味着子序列在两个字符串中必须是完全相同的子序列,且在原字符串中出现的相对顺序相同,但可以间隔?例如s="abcde", t="ace",如果k=0,我们可以匹配a,c,e,但过程中跳过了b,d,这实际上跳过了s中的b和d,消耗了2次跳过,k=0不允许。所以k=0时,我们无法得到"ace",只能得到"a"或"c"或"e"等单个字符,或者连续匹配的序列如"ab"?但"ab"在t中不是连续出现的。所以k=0时,结果不一定是常规LCS。

实际上,这是LCS的一个常见变种,称为“带跳过次数限制的LCS”或“带缺口惩罚的LCS”。通常定义是:允许跳过(gap),但跳过有代价,总跳过次数有限制。在这种定义下,状态转移方程就是我们上面讨论的,其中sk表示已使用的跳过次数。

初始化
dp[0][j][sk] = 0,因为s为空,LCS长度为0。
dp[i][0][sk] = 0。
同时,对于任何sk,dp[0][0][sk]=0。

最终答案
max_{0<=sk<=k} dp[|s|][|t|][sk]。因为我们可以使用不超过k次的跳过。

时间复杂度:O(|s| * |t| * k)。

举例验证

以s="abcde", t="ace", k=1为例。

我们计算dp表,为了简化,只展示sk=0和sk=1的部分,且只写最终dp值。

初始化:所有dp[0][][]=0, dp[][0][]=0。

i=1,j=1: s[0]='a', t[0]='a'相等。

  • sk=0: 匹配,dp[1][1][0] = dp[0][0][0]+1=1。
  • sk=1: 匹配,dp[1][1][1] = dp[0][0][1]+1=1;也可以跳过,但不会更优。

i=1,j=2: s[0]='a', t[1]='c'不等。

  • sk=0: 不能跳过,所以dp[1][2][0] = 0?但我们可以从dp[1][1][0]=1转移吗?不可以,因为那意味着我们跳过了t[1],但跳过一次。所以dp[1][2][0]保持初始值0。
  • sk=1: 可以跳过s[0]或t[1]。跳过s[0]: dp[0][2][0]=0;跳过t[1]: dp[1][1][0]=1。取max=1。所以dp[1][2][1]=1。

i=1,j=3: s[0]='a', t[2]='e'不等。

  • sk=0: 0
  • sk=1: 跳过s[0]: dp[0][3][0]=0;跳过t[2]: dp[1][2][0]=0。取max=0。但实际上,我们可以从dp[1][2][1]=1转移吗?不可以,因为这里我们处理的是i=1,j=3,状态转移只能从(i-1,j)或(i,j-1)来,且跳过操作消耗次数。所以dp[1][3][1]=0。

类似地,我们可以继续计算。最终,我们会得到dp[5][3][0]和dp[5][3][1]的值。可以验证,最终答案是3(使用sk=1,匹配a,c,e,其中跳过了s中的'b'和'd',但跳过了两次,k=1不够?实际上,匹配"ace"需要跳过s中的b和d,两次跳过,k=1不允许。所以实际上k=1时,最多只能匹配两个字符,比如"ac"(跳过s中的b)或"ce"(跳过s中的d)等。所以结果应该是2。我们通过这个例子明确:匹配"ace"需要跳过s中的两个字符,需要k>=2。当k=1时,我们最多只能跳过1个字符,所以最长公共子序列可能是"ac"(跳过s中的b)或"ce"(跳过s中的d)或"ae"(跳过s中的b,c,d中的两个,但只能跳一次,所以不行)等。所以结果应该是2。

通过这个例子,我们确认了算法的逻辑。

算法实现步骤

  1. 获取字符串s和t的长度,记作n和m。
  2. 初始化三维数组dp,大小为(n+1) x (m+1) x (k+1),所有元素初始化为0。
  3. 遍历i从1到n:
    遍历j从1到m:
    遍历sk从0到k:
    如果s[i-1] == t[j-1]:
    dp[i][j][sk] = dp[i-1][j-1][sk] + 1
    如果sk > 0:
    // 考虑跳过s[i-1]或t[j-1]
    dp[i][j][sk] = max(dp[i][j][sk], dp[i-1][j][sk-1], dp[i][j-1][sk-1])
    注意:即使字符相等,也可以选择跳过,但跳过不会比匹配更优,所以可以省略。但为了代码统一,可以保留。
  4. 最终答案ans = max_{0<=sk<=k} dp[n][m][sk]。
  5. 返回ans。

边界情况

  • 如果k很大(比如大于n+m),那么跳过次数足够多,我们可以跳过所有不相干的字符,结果就是min(n,m),但我们的算法仍然有效,因为sk最大到k,但实际跳过次数不会超过n+m。
  • 如果k=0,那么只有当两个字符串完全相同时,才能得到非零结果,且结果等于相同前缀的长度?实际上,k=0时,我们的算法允许匹配相等的字符,但不允许跳过。所以最终结果等于:从两个字符串中按顺序选取相等字符,但不能跳过任何字符,这意味着一旦遇到不等的字符,后续就无法继续匹配。这实际上得到的是两个字符串的最长公共前缀?不,因为我们可以跳过字符吗?不能。所以,例如s="abcde", t="acf",k=0,匹配a后,b和c不等,由于不能跳过,匹配终止,所以结果是1。但常规LCS是"ac"长度为2,需要跳过s中的b。所以k=0时,我们的算法结果不同于常规LCS。这符合问题定义。

总结

这个算法通过在LCS的经典DP状态上增加一维“已用跳过次数”,将跳过操作转化为状态转移的一部分,从而在O(nmk)时间内求解带跳过次数限制的最长公共子序列问题。理解的关键在于明确“跳过”操作消耗资源,且所有跳过都必须计入次数,而匹配操作不消耗次数。

最长公共子序列的变种:允许最多k次跳过的最长公共子序列 题目描述 给定两个字符串s和t,以及一个整数k。我们要找出s和t的最长公共子序列(LCS),但有一个特殊的规则:在匹配过程中,我们允许最多“跳过”k个字符。这里的“跳过”指的是,在构建公共子序列时,对于字符串s或t,我们可以选择跳过其中的某个字符,不计入最终的公共子序列,但会消耗一次“跳过”机会。总跳过次数(在s和t上跳过的字符数之和)不能超过k。求在满足此限制下,能够得到的最长公共子序列的长度。 举个例子 假设 s = "abcde", t = "acef", k = 1。 常规LCS是"ace",长度为3。 允许跳过1次,我们可以跳过s中的'd'或者t中的'f'。最优解是取"ace"(跳过t中的'f'),长度为3。如果k=0,则结果还是3。 另一个例子:s = "agcat", t = "gac", k = 2。 常规LCS是"ga"或"ac"或"gc",长度为2。 允许跳过2次。一种方案是:匹配s[ 1]='g'和t[ 0]='g',跳过t[ 1]='a',匹配s[ 3]='a'和t[ 2]='a',再跳过s[ 4]='t'。这样我们得到子序列"ga",跳过次数为2(t[ 1]和s[ 4]),长度仍为2。能否得到长度为3的?可以尝试匹配"gca":匹配s[ 1]='g'和t[ 0]='g',匹配s[ 2]='c'和t[ 2]='c'(这里跳过了t[ 1]='a'和s[ 3 ]='a',但我们选择匹配c),但这样我们跳过了'a',得到的子序列是"gc",长度2。实际上,要得到"gca",需要s中的'g','c','a'和t中的'g','c','a'一一对应,但这在原始序列中位置不对齐,需要更多的跳过。通过穷举可知,在k=2时,无法得到长度3。但如果我们增加k,可能能得到更长的序列。 我们的目标是设计一个动态规划算法,精确计算出在最多跳过k个字符的前提下,最长公共子序列的长度。 解题思路 这个问题是经典LCS问题的变种。经典LCS的DP定义是:dp[ i][ j ]表示s的前i个字符和t的前j个字符的LCS长度。状态转移为: 如果s[ i-1] == t[ j-1],则dp[ i][ j] = dp[ i-1][ j-1 ] + 1。 否则,dp[ i][ j] = max(dp[ i-1][ j], dp[ i][ j-1 ])。 现在引入跳过次数限制。我们需要在状态中增加一维,用来记录已经使用的跳过次数。但跳过次数是在两个字符串上发生的,我们关心的是总跳过次数。因此,我们可以定义状态dp[ i][ j][ sk ]表示:考虑s的前i个字符和t的前j个字符,并且总共使用了sk次跳过时,能得到的最长公共子序列的长度。 这里i, j, sk的范围是:0 <= i <= |s|, 0 <= j <= |t|, 0 <= sk <= k。 状态转移需要仔细考虑。在每一步,我们面对s的第i个字符(s[ i-1])和t的第j个字符(t[ j-1 ]),我们有几种选择: 匹配 :如果s[ i-1] == t[ j-1 ],那么我们可以选择将这两个字符匹配,加入LCS。此时,长度加1,且不消耗跳过次数。因此,状态转移为: dp[ i][ j][ sk] = max(dp[ i][ j][ sk], dp[ i-1][ j-1][ sk ] + 1)。 不匹配,且有跳过机会可用 :如果s[ i-1] != t[ j-1 ],我们可以选择跳过s的第i个字符,或者跳过t的第j个字符。但跳过操作会消耗一次跳过次数,并且不会增加LCS长度。 跳过s[ i-1]:这意味着我们不考虑s[ i-1]参与匹配,那么状态从dp[ i-1][ j][ sk-1 ]转移过来(注意,使用了一次跳过,所以是sk-1)。条件:sk > 0。 跳过t[ j-1]:同理,状态从dp[ i][ j-1][ sk-1 ]转移过来。条件:sk > 0。 所以,当s[ i-1] != t[ j-1 ]时,如果sk>0,我们有: dp[ i][ j][ sk] = max(dp[ i][ j][ sk], dp[ i-1][ j][ sk-1], dp[ i][ j-1][ sk-1 ])。 不匹配,且不跳过(即常规LCS的不匹配操作) :在经典LCS中,当字符不相等时,我们有dp[ i][ j] = max(dp[ i-1][ j], dp[ i][ j-1])。这个操作实际上对应着“跳过s[ i-1]”或“跳过t[ j-1]”,但在经典LCS中,这种“跳过”不被计入次数限制。在我们这个变种问题中,我们是否允许这种不计入次数的“跳过”呢?这取决于问题的定义。仔细思考:经典LCS中的“跳过”,其实是一种“忽略”,它不将字符纳入子序列,但也不视为一种“消耗资源”的跳过。在我们的问题中,我们明确“跳过”是消耗资源的。那么,不计入次数的“忽略”是否应该被允许?如果我们允许不计入次数的忽略,那相当于我们总是可以执行常规LCS的转移,那么跳过次数k就失去了限制意义,最终结果会等于常规LCS。因此, 在本题的定义下,所有不匹配的情况,如果要将字符排除在考虑之外,都必须消耗一次跳过机会 。所以,当我们不想匹配当前字符时,我们必须使用一次跳过机会来跳过s[ i-1]或t[ j-1]。如果没有跳过机会可用(sk=0),且s[ i-1]!=t[ j-1],那么我们无法处理这两个字符,状态无法转移,保持为0(或初始值)。但等等,这可能会导致问题:比如s="ab", t="ac",k=0,按照上述规则,由于'a'匹配,'b'和'c'不匹配且无跳过机会,那么dp[ 2][ 2][ 0]只能从匹配'a'得到1,而无法通过跳过'b'或'c'得到1,结果是1,这实际上就是LCS("ab","ac")="a"的长度1,是正确的。如果k=1,我们可以选择跳过'b'或'c',结果还是1。但如果s="a", t="b", k=0,按照规则,dp[ 1][ 1][ 0 ]无法从任何地方转移,结果为0,这也是正确的(LCS为空)。 综合以上,我们得到状态转移方程: 对于每个i从1到|s|,每个j从1到|t|,每个sk从0到k: 如果s[ i-1] == t[ j-1 ]: dp[ i][ j][ sk] = max(dp[ i][ j][ sk], dp[ i-1][ j-1][ sk ] + 1) 如果sk > 0: // 考虑跳过s[ i-1]或t[ j-1 ] dp[ i][ j][ sk] = max(dp[ i][ j][ sk], dp[ i-1][ j][ sk-1], dp[ i][ j-1][ sk-1 ]) 注意:即使字符相等,我们也可以选择不匹配而跳过,消耗一次跳过机会。但通常情况下,如果字符相等,我们不会选择跳过,因为匹配能增加长度且不消耗跳过次数,更优。所以在实现时,即使字符相等,我们也可以考虑跳过操作,但显然匹配更优,所以可以忽略跳过。但为了完整性,我们可以将跳过操作放在字符不等或通用情况中。由于跳过操作不会使结果更优(相较于匹配),所以可以简化。 初始化 dp[ 0][ j][ sk ] = 0 对任意j, sk。因为s为空时,LCS长度为0。 dp[ i][ 0][ sk ] = 0 对任意i, sk。 当sk=0时,dp[ i][ j][ 0]就是常规LCS(但注意,常规LCS允许不计次数的忽略,而我们这里当sk=0时不允许跳过,所以当字符不等时,dp[ i][ j][ 0]只能从dp[ i-1][ j-1][ 0]转移?不对,实际上经典LCS的转移包含dp[ i-1][ j]和dp[ i][ j-1],这对应着跳过s[ i-1]或t[ j-1]但不消耗跳过次数。在我们的定义中,这是不允许的。所以,当sk=0时,状态转移应该为:如果字符相等,dp[ i][ j][ 0]=dp[ i-1][ j-1][ 0]+1;如果字符不等,dp[ i][ j][ 0] = max(dp[ i-1][ j][ 0], dp[ i][ j-1][ 0])?这会导致矛盾,因为跳过操作被允许但没有消耗次数。所以,必须明确:在sk=0时,不允许任何跳过操作,那么当字符不等时,我们不能从dp[ i-1][ j][ 0]或dp[ i][ j-1][ 0]转移,因为它们隐含了跳过操作。那么如何计算dp[ i][ j][ 0]?实际上,当sk=0时,我们只能匹配相等的字符,不能跳过任何字符。所以,dp[ i][ j][ 0]应该等于:s[ 0..i-1]和t[ 0..j-1 ]中连续匹配的最长子序列长度?不,应该是允许从任意位置匹配,但不能跳过。这相当于:我们从两个字符串中按顺序选取相等的字符,但一旦遇到不等的字符,就不能再继续往后匹配了,因为不能跳过。这显然不对,比如s="abc", t="adc", k=0,常规LCS是"ac"长度为2,按照我们的定义,在匹配了'a'之后,遇到b和d不相等,由于不能跳过,我们无法跳过b或d去匹配后面的c,结果只能是1,这就不符合常规LCS了。所以,我们对问题的理解需要调整。 重新审视问题定义 :题目中说“允许最多跳过k个字符”,意思是:在构建公共子序列时,我们可以选择忽略(跳过)s或t中的某些字符,这些被忽略的字符不计入子序列,但会消耗跳过次数。而没有跳过的操作,其实就是常规LCS中的“不匹配”情况,在常规LCS中,当我们取max(dp[ i-1][ j], dp[ i][ j-1])时,就相当于跳过了s[ i-1]或t[ j-1 ],但这种跳过是“免费”的,不消耗跳过次数。而现在,我们给“跳过”操作增加了成本(消耗一次跳过机会),但我们仍然允许“免费”的忽略吗?题目没有明确说明。通常这类问题的设定是:所有的“跳过”操作都计入k次限制。也就是说,原来经典LCS中的“不匹配”操作,现在需要消耗跳过次数。这样,k=0时,我们就不能进行任何跳过,那么只有当两个字符串完全相同时,才能得到LCS长度等于字符串长度。否则,一旦遇到不等的字符,就必须停止,因为不能跳过。这显然不是我们想要的LCS变种。 更合理的解释是:在经典LCS中,我们有两种操作:匹配字符,或者跳过某个字符(即不匹配)。经典LCS中,跳过是免费的。现在,我们给跳过操作加上次数限制,但匹配操作不受限。所以,在状态转移中,当字符不等时,我们可以选择跳过s[ i-1]或跳过t[ j-1 ],但这会消耗一次跳过次数。同时,即使字符相等,我们也可以选择不匹配而跳过,但那样会浪费跳过次数,所以通常不会选择。此外,我们仍然允许“免费”的跳过吗?不,如果允许免费跳过,那么跳过次数限制就形同虚设。所以,我们必须要求所有跳过操作都消耗次数。 基于这个理解,我们修正状态转移: 定义dp[ i][ j][ sk ]如前。 状态转移: 如果s[ i-1] == t[ j-1 ]: 我们可以选择匹配:dp[ i][ j][ sk] = dp[ i-1][ j-1][ sk ] + 1。 我们也可以选择不匹配,而是跳过s[ i-1]或t[ j-1 ](消耗跳过次数),但这样不会使结果更优,所以可以忽略。 如果s[ i-1] != t[ j-1 ]: 此时我们不能匹配,只能选择跳过其中一个字符(如果还有跳过次数的话): 如果sk > 0: 跳过s[ i-1]:从dp[ i-1][ j][ sk-1 ]转移。 跳过t[ j-1]:从dp[ i][ j-1][ sk-1 ]转移。 取两者最大值。 如果sk == 0,那么我们无法处理当前两个字符,状态无法从i,j转移,但实际上,我们可以从dp[ i-1][ j][ 0]或dp[ i][ j-1][ 0]转移吗?不可以,因为那意味着我们进行了跳过操作但没有消耗次数,这是不允许的。所以当sk=0且字符不等时,我们无法从任何状态转移,dp[ i][ j][ 0]应该等于0?但这样会丢失之前的结果。例如s="ab", t="ac",k=0。i=1,j=1: 'a'=='a',dp[ 1][ 1][ 0]=1。i=1,j=2: 'a'!='c',sk=0,无法跳过,那么dp[ 1][ 2][ 0]应该等于多少?它应该至少等于dp[ 1][ 1][ 0]=1,因为我们可以忽略t[ 1 ]='c',但忽略就是跳过,需要消耗次数。所以严格来说,当k=0时,我们只能匹配完全相同的字符序列,一旦遇到不等的字符,后续就无法继续匹配了。但这样得到的结果是:LCS必须是两个字符串的公共前缀,且一旦出现不同字符,匹配就终止。这显然不是我们想要的LCS。 矛盾点在于:经典LCS允许免费跳过,而我们现在不允许免费跳过。为了得到有意义的LCS,我们需要允许“忽略”操作,但忽略操作必须计入跳过次数。这样,k=0时,LCS长度就等于两个字符串完全匹配的最长子序列长度,即要求子序列在两个字符串中出现的字符顺序完全一致且连续?不,即使k=0,我们也可以选择匹配一些字符,跳过一些字符,但跳过消耗次数,k=0就不能跳过,所以只能匹配所有字符,即要求子序列必须是原字符串的子序列,且不能跳过任何字符,这意味着子序列在两个字符串中必须是完全相同的子序列,且在原字符串中出现的相对顺序相同,但可以间隔?例如s="abcde", t="ace",如果k=0,我们可以匹配a,c,e,但过程中跳过了b,d,这实际上跳过了s中的b和d,消耗了2次跳过,k=0不允许。所以k=0时,我们无法得到"ace",只能得到"a"或"c"或"e"等单个字符,或者连续匹配的序列如"ab"?但"ab"在t中不是连续出现的。所以k=0时,结果不一定是常规LCS。 实际上,这是LCS的一个常见变种,称为“带跳过次数限制的LCS”或“带缺口惩罚的LCS”。通常定义是:允许跳过(gap),但跳过有代价,总跳过次数有限制。在这种定义下,状态转移方程就是我们上面讨论的,其中sk表示已使用的跳过次数。 初始化 : dp[ 0][ j][ sk ] = 0,因为s为空,LCS长度为0。 dp[ i][ 0][ sk ] = 0。 同时,对于任何sk,dp[ 0][ 0][ sk ]=0。 最终答案 : max_ {0<=sk<=k} dp[ |s|][ |t|][ sk ]。因为我们可以使用不超过k次的跳过。 时间复杂度 :O(|s| * |t| * k)。 举例验证 以s="abcde", t="ace", k=1为例。 我们计算dp表,为了简化,只展示sk=0和sk=1的部分,且只写最终dp值。 初始化:所有dp[ 0][ ][ ]=0, dp[ ][ 0][ ]=0。 i=1,j=1: s[ 0]='a', t[ 0 ]='a'相等。 sk=0: 匹配,dp[ 1][ 1][ 0] = dp[ 0][ 0][ 0 ]+1=1。 sk=1: 匹配,dp[ 1][ 1][ 1] = dp[ 0][ 0][ 1 ]+1=1;也可以跳过,但不会更优。 i=1,j=2: s[ 0]='a', t[ 1 ]='c'不等。 sk=0: 不能跳过,所以dp[ 1][ 2][ 0] = 0?但我们可以从dp[ 1][ 1][ 0]=1转移吗?不可以,因为那意味着我们跳过了t[ 1],但跳过一次。所以dp[ 1][ 2][ 0 ]保持初始值0。 sk=1: 可以跳过s[ 0]或t[ 1]。跳过s[ 0]: dp[ 0][ 2][ 0]=0;跳过t[ 1]: dp[ 1][ 1][ 0]=1。取max=1。所以dp[ 1][ 2][ 1 ]=1。 i=1,j=3: s[ 0]='a', t[ 2 ]='e'不等。 sk=0: 0 sk=1: 跳过s[ 0]: dp[ 0][ 3][ 0]=0;跳过t[ 2]: dp[ 1][ 2][ 0]=0。取max=0。但实际上,我们可以从dp[ 1][ 2][ 1]=1转移吗?不可以,因为这里我们处理的是i=1,j=3,状态转移只能从(i-1,j)或(i,j-1)来,且跳过操作消耗次数。所以dp[ 1][ 3][ 1 ]=0。 类似地,我们可以继续计算。最终,我们会得到dp[ 5][ 3][ 0]和dp[ 5][ 3][ 1 ]的值。可以验证,最终答案是3(使用sk=1,匹配a,c,e,其中跳过了s中的'b'和'd',但跳过了两次,k=1不够?实际上,匹配"ace"需要跳过s中的b和d,两次跳过,k=1不允许。所以实际上k=1时,最多只能匹配两个字符,比如"ac"(跳过s中的b)或"ce"(跳过s中的d)等。所以结果应该是2。我们通过这个例子明确:匹配"ace"需要跳过s中的两个字符,需要k>=2。当k=1时,我们最多只能跳过1个字符,所以最长公共子序列可能是"ac"(跳过s中的b)或"ce"(跳过s中的d)或"ae"(跳过s中的b,c,d中的两个,但只能跳一次,所以不行)等。所以结果应该是2。 通过这个例子,我们确认了算法的逻辑。 算法实现步骤 获取字符串s和t的长度,记作n和m。 初始化三维数组dp,大小为(n+1) x (m+1) x (k+1),所有元素初始化为0。 遍历i从1到n: 遍历j从1到m: 遍历sk从0到k: 如果s[ i-1] == t[ j-1 ]: dp[ i][ j][ sk] = dp[ i-1][ j-1][ sk ] + 1 如果sk > 0: // 考虑跳过s[ i-1]或t[ j-1 ] dp[ i][ j][ sk] = max(dp[ i][ j][ sk], dp[ i-1][ j][ sk-1], dp[ i][ j-1][ sk-1 ]) 注意:即使字符相等,也可以选择跳过,但跳过不会比匹配更优,所以可以省略。但为了代码统一,可以保留。 最终答案ans = max_ {0<=sk<=k} dp[ n][ m][ sk ]。 返回ans。 边界情况 如果k很大(比如大于n+m),那么跳过次数足够多,我们可以跳过所有不相干的字符,结果就是min(n,m),但我们的算法仍然有效,因为sk最大到k,但实际跳过次数不会超过n+m。 如果k=0,那么只有当两个字符串完全相同时,才能得到非零结果,且结果等于相同前缀的长度?实际上,k=0时,我们的算法允许匹配相等的字符,但不允许跳过。所以最终结果等于:从两个字符串中按顺序选取相等字符,但不能跳过任何字符,这意味着一旦遇到不等的字符,后续就无法继续匹配。这实际上得到的是两个字符串的最长公共前缀?不,因为我们可以跳过字符吗?不能。所以,例如s="abcde", t="acf",k=0,匹配a后,b和c不等,由于不能跳过,匹配终止,所以结果是1。但常规LCS是"ac"长度为2,需要跳过s中的b。所以k=0时,我们的算法结果不同于常规LCS。这符合问题定义。 总结 这个算法通过在LCS的经典DP状态上增加一维“已用跳过次数”,将跳过操作转化为状态转移的一部分,从而在O(n m k)时间内求解带跳过次数限制的最长公共子序列问题。理解的关键在于明确“跳过”操作消耗资源,且所有跳过都必须计入次数,而匹配操作不消耗次数。