最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次)
字数 1469 2025-11-20 10:53:09

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

题目描述

给定两个字符串s和t,以及一个字符集C和一个整数k。要求找出s和t的最长公共子序列,且满足:

  1. 对于字符集C中的每个字符,如果在子序列中出现,则必须连续出现至少k次
  2. 子序列中字符的相对顺序必须与在s和t中出现的相对顺序一致

例如:
s = "abcde", t = "ace", C = {'c'}, k = 1
满足条件的最长公共子序列是"ace"(长度为3)

s = "aabcc", t = "aacc", C = {'c'}, k = 2
满足条件的最长公共子序列是"aacc"(长度为4),而不是"aabc"(因为'c'没有连续出现2次)

解题过程

步骤1:理解问题本质
这是一个带约束的最长公共子序列问题。我们需要在传统的LCS基础上,增加对特定字符连续出现次数的限制。

步骤2:定义状态
设dp[i][j][c][cnt]表示考虑s的前i个字符和t的前j个字符,以字符c结尾且该字符已经连续出现cnt次的最长公共子序列长度。

但由于字符集可能很大,这种状态定义会占用过多空间。我们可以优化为:
dp[i][j][len]表示考虑s的前i个字符和t的前j个字符,当前匹配的子序列以某个字符结尾且该字符已经连续出现len次的最长公共子序列长度。

步骤3:状态转移方程

对于每个位置(i, j),我们考虑三种情况:

  1. 不选择s[i]和t[j]
    dp[i][j][len] = max(dp[i-1][j][len], dp[i][j-1][len])

  2. s[i] == t[j]时,选择该字符
    如果当前字符等于前一个字符(即连续出现):
    dp[i][j][len] = max(dp[i][j][len], dp[i-1][j-1][len-1] + 1) (当len > 1)

    如果当前字符是新的字符(即开始新的连续段):
    dp[i][j][1] = max(dp[i][j][1], dp[i-1][j-1][prev_len] + 1) (对于所有prev_len)

  3. 检查约束条件
    对于字符集C中的字符,只有当连续出现次数cnt >= k时,我们才认为这个匹配是有效的。

步骤4:优化状态设计
由于直接使用四维DP可能空间太大,我们可以重新设计状态:
设dp[i][j]表示考虑s的前i个字符和t的前j个字符时的最长有效子序列长度
设last[i][j]记录当前连续段的字符和长度

更实用的方法是使用:
dp[i][j] = 常规LCS的DP值
special[i][j][c] = 以字符c结尾且满足连续条件的特殊子序列长度

步骤5:具体算法实现

def longest_common_subsequence_with_constraint(s, t, C, k):
    m, n = len(s), len(t)
    
    # dp[i][j] 表示s[0:i]和t[0:j]的最长有效公共子序列长度
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # cont[i][j] 记录以当前字符结尾的连续段长度
    cont = [[0] * (n + 1) for _ in range(m + 1)]
    
    # char[i][j] 记录当前连续段的字符
    char = [[''] * (n + 1) for _ in range(m + 1)]
    
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s[i-1] == t[j-1]:
                current_char = s[i-1]
                
                # 情况1:不选择当前字符
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                
                # 情况2:选择当前字符
                if char[i-1][j-1] == current_char:
                    # 连续出现
                    new_cont = cont[i-1][j-1] + 1
                    new_len = dp[i-1][j-1] + 1
                    
                    # 检查是否满足约束
                    if current_char in C:
                        if new_cont >= k:
                            if new_len > dp[i][j]:
                                dp[i][j] = new_len
                                cont[i][j] = new_cont
                                char[i][j] = current_char
                        else:
                            # 不满足约束,不能更新
                            pass
                    else:
                        # 不在约束字符集中,直接更新
                        if new_len > dp[i][j]:
                            dp[i][j] = new_len
                            cont[i][j] = new_cont
                            char[i][j] = current_char
                else:
                    # 新的字符段
                    new_len = dp[i-1][j-1] + 1
                    
                    if current_char in C:
                        # 约束字符,必须检查是否能开始新的连续段
                        if 1 >= k or (i > 1 and j > 1 and dp[i-1][j-1] > 0):
                            # 可以开始新段(要么长度够,要么前面有内容)
                            if new_len > dp[i][j]:
                                dp[i][j] = new_len
                                cont[i][j] = 1
                                char[i][j] = current_char
                    else:
                        # 非约束字符,直接更新
                        if new_len > dp[i][j]:
                            dp[i][j] = new_len
                            cont[i][j] = 1
                            char[i][j] = current_char
            else:
                # 字符不匹配,取左边或上边的最大值
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                # 继承连续信息(选择值更大的那个)
                if dp[i-1][j] > dp[i][j-1] or (dp[i-1][j] == dp[i][j-1] and dp[i-1][j] > 0):
                    cont[i][j] = cont[i-1][j]
                    char[i][j] = char[i-1][j]
                else:
                    cont[i][j] = cont[i][j-1]
                    char[i][j] = char[i][j-1]
    
    return dp[m][n]

步骤6:算法分析

  • 时间复杂度:O(m×n),其中m和n分别是字符串s和t的长度
  • 空间复杂度:O(m×n),可以通过滚动数组优化到O(n)

步骤7:示例验证

例1:s = "abcde", t = "ace", C = {'c'}, k = 1
结果:3(子序列"ace")

例2:s = "aabcc", t = "aacc", C = {'c'}, k = 2
结果:4(子序列"aacc",'c'连续出现2次)

这个算法在传统LCS的基础上,通过维护连续字符的信息来确保约束条件得到满足,是线性动态规划中处理带约束问题的典型范例。

最长公共子序列的变种:带字符必须按顺序出现限制的最长公共子序列(进阶版:限制某些字符必须连续出现k次) 题目描述 给定两个字符串s和t,以及一个字符集C和一个整数k。要求找出s和t的最长公共子序列,且满足: 对于字符集C中的每个字符,如果在子序列中出现,则必须连续出现至少k次 子序列中字符的相对顺序必须与在s和t中出现的相对顺序一致 例如: s = "abcde", t = "ace", C = {'c'}, k = 1 满足条件的最长公共子序列是"ace"(长度为3) s = "aabcc", t = "aacc", C = {'c'}, k = 2 满足条件的最长公共子序列是"aacc"(长度为4),而不是"aabc"(因为'c'没有连续出现2次) 解题过程 步骤1:理解问题本质 这是一个带约束的最长公共子序列问题。我们需要在传统的LCS基础上,增加对特定字符连续出现次数的限制。 步骤2:定义状态 设dp[ i][ j][ c][ cnt ]表示考虑s的前i个字符和t的前j个字符,以字符c结尾且该字符已经连续出现cnt次的最长公共子序列长度。 但由于字符集可能很大,这种状态定义会占用过多空间。我们可以优化为: dp[ i][ j][ len ]表示考虑s的前i个字符和t的前j个字符,当前匹配的子序列以某个字符结尾且该字符已经连续出现len次的最长公共子序列长度。 步骤3:状态转移方程 对于每个位置(i, j),我们考虑三种情况: 不选择s[ i]和t[ j] dp[ i][ j][ len] = max(dp[ i-1][ j][ len], dp[ i][ j-1][ len ]) s[ i] == t[ j]时,选择该字符 如果当前字符等于前一个字符(即连续出现): dp[ i][ j][ len] = max(dp[ i][ j][ len], dp[ i-1][ j-1][ len-1 ] + 1) (当len > 1) 如果当前字符是新的字符(即开始新的连续段): dp[ i][ j][ 1] = max(dp[ i][ j][ 1], dp[ i-1][ j-1][ prev_ len] + 1) (对于所有prev_ len) 检查约束条件 对于字符集C中的字符,只有当连续出现次数cnt >= k时,我们才认为这个匹配是有效的。 步骤4:优化状态设计 由于直接使用四维DP可能空间太大,我们可以重新设计状态: 设dp[ i][ j ]表示考虑s的前i个字符和t的前j个字符时的最长有效子序列长度 设last[ i][ j ]记录当前连续段的字符和长度 更实用的方法是使用: dp[ i][ j ] = 常规LCS的DP值 special[ i][ j][ c ] = 以字符c结尾且满足连续条件的特殊子序列长度 步骤5:具体算法实现 步骤6:算法分析 时间复杂度:O(m×n),其中m和n分别是字符串s和t的长度 空间复杂度:O(m×n),可以通过滚动数组优化到O(n) 步骤7:示例验证 例1:s = "abcde", t = "ace", C = {'c'}, k = 1 结果:3(子序列"ace") 例2:s = "aabcc", t = "aacc", C = {'c'}, k = 2 结果:4(子序列"aacc",'c'连续出现2次) 这个算法在传统LCS的基础上,通过维护连续字符的信息来确保约束条件得到满足,是线性动态规划中处理带约束问题的典型范例。