最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现)
题目描述
给定两个字符串 s 和 t,以及一个整数 k。要求找出 s 和 t 的最长公共子序列(LCS),但这个子序列需要满足一个额外的条件:对于子序列中的任意一个字符,它必须连续出现至少 k 次。这里的“连续出现”是指在子序列中,相同的字符必须连续出现,形成一个连续的块,并且这个块的长度不能小于 k。如果某个字符在子序列中只出现一次,那么 k 必须为1才满足条件。如果无法找到满足条件的公共子序列,则返回0。
解题过程
-
问题分析与状态定义
这是一个带有限制条件的LCS变种。标准的LCS使用dp[i][j]表示s[0..i-1]和t[0..j-1]的LCS长度。现在我们需要额外考虑子序列中字符连续出现的次数限制。我们需要扩展状态,以记录当前正在匹配的连续字符块的信息。关键思路是:当我们匹配到字符
c时,我们不仅关心当前位置,还关心这个c是作为一个新连续块的开始,还是作为一个已有连续块的延续。我们定义动态规划状态如下:
dp[i][j][c][cnt]表示考虑字符串s的前i个字符和字符串t的前j个字符,并且当前的公共子序列以字符c结尾,且这个字符c已经连续出现了cnt次的情况下,满足条件的最长公共子序列的长度。其中:
i的范围是 [0, len(s)]j的范围是 [0, len(t)]c是字符集中的一个字符(通常可以映射到0-25,如果只有小写字母)。cnt的范围是 [1, k](因为连续出现次数小于k的块是不合格的,但我们中间状态需要记录,最终有效块的cnt必须>=k。实际上,cnt可以从1记录到max(len(s), len(t)),但为了优化,我们可以只记录到k,因为当cnt>=k时,这个块已经合格,我们只关心合格块的最大长度,可以统一用k来表示cnt>=k的状态)。
为了优化,我们可以修改状态定义:
dp[i][j][c][cnt]中cnt的范围是 [1, k]。- 当
cnt<k时,表示当前正在形成一个以c结尾的连续块,并且这个块当前长度是cnt,尚未达到合格标准。 - 当
cnt==k时,表示当前以c结尾的连续块已经达到合格标准(长度至少为k)。之后如果再添加相同的字符c,块的长度会增加,但状态仍然可以保持在cnt = k,因为我们只关心块是否合格,不关心超过k的具体长度。
-
状态转移方程
状态转移需要考虑以下几种情况:a) 不匹配
s[i-1]或t[j-1]
- 继承s少一个字符的状态:dp[i][j][c][cnt] = max(dp[i][j][c][cnt], dp[i-1][j][c][cnt])
- 继承t少一个字符的状态:dp[i][j][c][cnt] = max(dp[i][j][c][cnt], dp[i][j-1][c][cnt])b) 匹配
s[i-1]和t[j-1],且s[i-1] == c(延续当前块)
如果当前状态末尾字符c正好等于s[i-1]和t[j-1],那么我们可以延续当前的连续块。
- 如果cnt < k:我们可以将连续块长度增加1。
dp[i][j][c][cnt+1] = max(dp[i][j][c][cnt+1], dp[i-1][j-1][c][cnt] + 1)
- 如果cnt >= k(在状态中体现为cnt == k):延续块,但状态cnt保持为 k。
dp[i][j][c][k] = max(dp[i][j][c][k], dp[i-1][j-1][c][k] + 1)c) 匹配
s[i-1]和t[j-1],但s[i-1] != c(开始一个新块)
如果匹配的字符是一个新的字符ch(即s[i-1] == t[j-1] = ch且ch != c),那么我们需要结束当前的块(如果当前块是合格的,即cnt >= k,或者当前块长度cnt>=1但我们可以选择开始新块),并开始一个以ch开头的新块。
新块的长度初始为1。
- 从合格块开始新块:如果之前存在一个以任意字符c_prev结尾的合格块 (cnt_prev >= k),或者之前还没有任何块(初始状态),我们可以开始一个新块。
dp[i][j][ch][1] = max(dp[i][j][ch][1], dp[i-1][j-1][c_prev][cnt_prev] + 1)对于所有c_prev和cnt_prev >= k(或者初始状态值)。
- 特别注意初始状态:当i=0或j=0时,是空字符串,可以认为存在一个长度为0的“合格块”,这样从第一个匹配的字符开始,就可以开始一个长度为1的新块。 -
初始化
- 我们可以初始化一个“空序列”的状态。通常,我们可以设置一个初始值,表示还没有任何字符被选择。
- 令
dp[0][0][c][cnt] = 0对于所有c,cnt。但实际上,在i=0或j=0时,没有任何字符,我们用一个基础值0来表示空序列的长度。 - 在实际操作中,我们可能会使用一个很小的数(比如-10^9)来初始化所有状态,表示不可达,然后将
dp[0][0][*][*]或者一个虚拟状态设为0。
-
最终结果
最终答案不是简单的dp[len(s)][len(t)][*][*]的最大值。因为最终形成的公共子序列,其最后一个连续块也必须是合格的(长度>=k)。
所以,我们需要在所有状态dp[len(s)][len(t)][c][cnt]中,寻找那些cnt >= k的状态,并取最大值。
answer = max{ dp[len(s)][len(t)][c][cnt] for all c, for all cnt >= k }
如果最大值是0(并且初始空序列我们设为0),说明没有找到任何合格的公共子序列,返回0。 -
复杂度优化
直接四维DP的状态数是 O(n * m * |Σ| * k),其中 |Σ| 是字符集大小。如果字符串长度几百,字符集26,k不大,是可以接受的。如果很大,可能需要优化,比如用滚动数组优化空间。
总结
这个算法通过扩展LCS的状态,增加了“结尾字符”和“连续出现次数”两个维度,巧妙地处理了字符必须连续出现至少k次的限制。状态转移时,仔细区分了“延续当前块”和“开始新块”两种情况,确保了最终得到的公共子序列满足题目要求。