最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现)
字数 2895 2025-11-06 12:40:04

最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现)

题目描述

给定两个字符串 st,以及一个整数 k。要求找出 st 的最长公共子序列(LCS),但这个子序列需要满足一个额外的条件:对于子序列中的任意一个字符,它必须连续出现至少 k。这里的“连续出现”是指在子序列中,相同的字符必须连续出现,形成一个连续的块,并且这个块的长度不能小于 k。如果某个字符在子序列中只出现一次,那么 k 必须为1才满足条件。如果无法找到满足条件的公共子序列,则返回0。

解题过程

  1. 问题分析与状态定义
    这是一个带有限制条件的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的具体长度。
  2. 状态转移方程
    状态转移需要考虑以下几种情况:

    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] = chch != 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_prevcnt_prev >= k(或者初始状态值)。
    - 特别注意初始状态:当 i=0j=0 时,是空字符串,可以认为存在一个长度为0的“合格块”,这样从第一个匹配的字符开始,就可以开始一个长度为1的新块。

  3. 初始化

    • 我们可以初始化一个“空序列”的状态。通常,我们可以设置一个初始值,表示还没有任何字符被选择。
    • dp[0][0][c][cnt] = 0 对于所有 c, cnt。但实际上,在 i=0j=0 时,没有任何字符,我们用一个基础值0来表示空序列的长度。
    • 在实际操作中,我们可能会使用一个很小的数(比如-10^9)来初始化所有状态,表示不可达,然后将 dp[0][0][*][*] 或者一个虚拟状态设为0。
  4. 最终结果
    最终答案不是简单的 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。

  5. 复杂度优化
    直接四维DP的状态数是 O(n * m * |Σ| * k),其中 |Σ| 是字符集大小。如果字符串长度几百,字符集26,k不大,是可以接受的。如果很大,可能需要优化,比如用滚动数组优化空间。

总结
这个算法通过扩展LCS的状态,增加了“结尾字符”和“连续出现次数”两个维度,巧妙地处理了字符必须连续出现至少k次的限制。状态转移时,仔细区分了“延续当前块”和“开始新块”两种情况,确保了最终得到的公共子序列满足题目要求。

最长公共子序列的变种:带字符出现次数限制的最长公共子序列(进阶版:限制某些字符必须连续出现) 题目描述 给定两个字符串 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次的限制。状态转移时,仔细区分了“延续当前块”和“开始新块”两种情况,确保了最终得到的公共子序列满足题目要求。