字符串模糊匹配中的“重复子序列”问题(计数版)
字数 4747 2025-12-24 07:30:19

字符串模糊匹配中的“重复子序列”问题(计数版)

题目描述
给定一个主字符串 s(长度为 n)和一个模式字符串 p(长度为 m),模式串中可能包含字符、通配符 ?(匹配任意单个字符)以及量词 *(匹配零个或多个任意字符)。
特别地,模式串中可能出现 带括号的重复子表达式,例如 (ab)* 表示零个或多个连续的 "ab" 子串。
为了简化,我们假设括号内只包含普通字符(无通配符或量词),且括号后紧跟一个量词:*(零次或多次)、+(一次或多次)或 ?(零次或一次)。
本题的目标是:计算主串 s 中,有多少个子序列(非连续)能够匹配模式串 p
注意:子序列是从主串中删除若干个字符(可以为 0)后剩余的字符按原顺序组成的序列。匹配的定义是:子序列与模式串按规则完全匹配。

示例

  • 输入:s = "abab", p = "(ab)*"
    解释:模式 (ab)* 表示零个或多个 "ab"。
    子序列匹配情况:

    • 取子序列 ""(空串)匹配零次重复,计 1 种。
    • 取子序列 "ab"(如取 s 的第 1、2 个字符)匹配一次重复,计 1 种。
    • 取子序列 "abab"(取全部字符)匹配两次重复,计 1 种。
      输出:3
  • 输入:s = "aabc", p = "a?c"
    解释:模式 a?c? 匹配任意单个字符。
    子序列匹配情况:

    • 取子序列 "abc"(删除第二个 'a')中,'?' 匹配 'b',计 1 种。
    • 取子序列 "aac"(删除 'b')中,'?' 匹配第二个 'a',但注意子序列需保持顺序,这里第二个字符是 'a',可以匹配 '?'。
      实际上,从 "aabc" 中取三个字符组成子序列,且第一个字符为 'a'、第三个字符为 'c' 的可能组合:
      1. 取索引 (0,1,3) 得 "aac",'?' 匹配 'a'。
      2. 取索引 (0,2,3) 得 "abc",'?' 匹配 'b'。
      3. 取索引 (1,2,3) 得 "abc",但第一个字符是第二个 'a',模式第一个字符是 'a',仍匹配,但此时 '?' 匹配 'b',与上一种重复吗?
        注意:子序列是由原串索引唯一确定的,不同索引视为不同子序列,即使得到的字符串相同。
        因此,应统计所有索引递增的子序列。
        输出:3(具体解释见后文计算)

解题过程循序渐进讲解

步骤 1:问题拆解与状态定义
本题核心是:主串 s 的子序列(非连续)匹配模式 p。
模式 p 中可能有括号重复组,例如 (ab)*。我们可以将模式 p 解析成一个 “模式单元”序列
每个模式单元是以下之一:

  1. 普通字符(如 'a')
  2. 通配符 '?'
  3. 带量词的括号组,如 (ab)*

定义 dp[i][j] 表示:考虑主串 s 的前 i 个字符(即 s[0..i-1])的子序列,匹配模式 p 的前 j 个模式单元,有多少种匹配方式。
这里 i 从 0 到 n(i=0 表示空主串前缀),j 从 0 到 M(M 是模式单元个数,将 p 解析后得到)。
最终答案是 dp[n][M]

步骤 2:模式串解析
我们需要将 p 解析为模式单元列表。
例如 p = "a(ab)*c?" 解析为:
单元 1: 普通字符 'a'
单元 2: 括号组内容 "ab",量词 '*'
单元 3: 普通字符 'c'
单元 4: 通配符 '?'

实现时,可以用一个数组 units 存储每个单元的类型(CHAR, WILD, GROUP)和额外信息(字符、组字符串、量词类型 * / + / ?)。

步骤 3:状态转移方程(无括号组时)
先考虑简单情况:模式单元只有普通字符和 '?'。
dp[0][0] = 1(空子序列匹配空模式)。
对于 i > 0, j > 0

  1. 我们可以 不选用 s[i-1] 来匹配第 j 个模式单元,那么匹配数继承自 dp[i-1][j]
  2. 我们可以 选用 s[i-1] 来匹配第 j 个模式单元,当且仅当 s[i-1] 与单元 j 匹配(字符相等或单元为 '?')。此时匹配数加上 dp[i-1][j-1]
    所以:
dp[i][j] = dp[i-1][j] + (match(s[i-1], unit[j]) ? dp[i-1][j-1] : 0)

其中 match 判断单个字符与单元匹配。

步骤 4:引入括号组(带量词)的匹配
括号组可能重复多次,例如 (ab)* 可以匹配 ""、"ab"、"abab"...
我们考虑将括号组视为一个“子模式”,其匹配需要动态规划。
定义辅助状态 groupDP[k][t] 表示:用主串 s 的某段前缀(前 k 个字符)的子序列,匹配括号组重复 t 次,有多少种方式。
但 t 可以很大(理论上无限),不过注意到主串长度有限,最多重复次数受限于主串长度。

更实用的方法是:将括号组的匹配嵌入到主 DP 中。
设当前单元是括号组 G,量词为 Q(* / + / ?)。
我们可以在主 DP 中新增一维表示“当前处于该括号组的第几次重复”,但这样状态太多。

另一种思路:将括号组的匹配转化为 “子序列匹配子串” 问题。
对于括号组 G(字符串 g,长度为 L),我们预计算一个数组 f[i][r] 表示:从 s 的前 i 个字符中,选取子序列匹配 g 重复 r 次(即 g 连续重复 r 次构成的字符串)的方法数。
然后,在主 DP 中,当遇到括号组单元时,我们枚举可能的重复次数 r(根据量词决定 r 的范围),然后从 dp[i'][j-1] 转移到 dp[i][j],其中转移时乘以 f[i][r] 但需注意子序列选取不重叠。

实际上更精确的是:我们将括号组视为一个整体单元,用 DP 来计算从 s 的某段区间内选取子序列匹配该组重复若干次的方法数,然后合并到主 DP。

步骤 5:简化版解法(量词只有 *)
为了清晰,我们先考虑量词只有 *(零次或多次)的情况。
定义 dp[i][j] 如上,并增加一个辅助状态 gp[i][j] 表示:考虑 s 的前 i 个字符,当前正在匹配括号组 G(第 j 个单元),且 已经匹配了至少一次该组 的情况下,有多少种匹配方式(即匹配了 G* 中的若干次重复)。

转移时:

  1. 若单元 j 是普通字符或 '?',用前述转移。
  2. 若单元 j 是括号组 G*:
    • 我们可以决定匹配零次:直接从 dp[i][j-1] 继承到 dp[i][j]
    • 我们可以匹配至少一次:这需要进入 gp 状态。
      gp[i][j] 可以从 gp[i-1][j] 继承(不选用 s[i-1]),也可以从 dp[i-L][j-1] 转移过来(开始第一次匹配,其中 L 是组 G 的长度,且 s[i-L..i-1] 的子序列要匹配 G),还可以从 gp[i-L][j] 转移(增加一次重复匹配)。

但这里要求子序列匹配 G 重复 r 次,且子序列索引递增,不能混淆。

步骤 6:实际实现技巧(将括号组视为一个字符类)
我们可以将括号组 G 量词 Q 转换为 匹配一个“块”,这个块是 G 重复若干次(根据 Q 决定最少和最多次数)。
然后,问题转化为:主串 s 中选取子序列,使得它是由这些“块”按顺序组成的序列。
这等价于:每个模式单元(包括括号组)匹配主串的一个子序列片段(非连续)。

更简单的方法:将模式串 p 展开所有可能的常规字符串(无括号),然后对每个展开串计算 s 的子序列匹配数,最后求和。但展开可能太多,不可行。

步骤 7:通用 DP 设计
我们换一种状态定义:
dp[i][j] 表示:s 的前 i 个字符中,选取子序列匹配 p 的前 j 个字符(将括号组视为普通字符序列)的方法数。
但这样括号组量词难以处理。

最终方案:

  1. 将模式串 p 转换为一个 NFA(非确定性有限自动机),其中每个状态代表模式匹配的位置。
  2. 用 DP 计算:dp[i][q] 表示 s 的前 i 个字符的子序列,有多少种方式到达 NFA 状态 q。
  3. 初始状态 dp[0][start] = 1
  4. 转移:
    • 不选用 s[i-1]:dp[i][q] += dp[i-1][q]
    • 选用 s[i-1]:若从状态 q 通过字符 s[i-1](或通配符 '?')可以到达状态 q',则 dp[i][q'] += dp[i-1][q]
    • 对于 ε 转移(如量词 * 可以重复零次),需要预先处理闭包。
  5. 最终答案是所有接受状态的 dp[n][q] 之和。

这实际上将问题转化为 子序列匹配正则表达式 的计数问题。

步骤 8:示例详细计算(第二个示例)
s = "aabc", p = "a?c"(无括号组,简单情况)
模式单元:1:'a', 2:'?', 3:'c'
dp[i][j] 表(行 i=0..4,列 j=0..3):

初始化:dp[0][0] = 1,其余 0。

i=1(s[0]='a')
j=1:匹配 'a',dp[1][1] += dp[0][0] = 1
其他 j 继承上一行:dp[1][0] = dp[0][0] = 1
结果:dp[1] = [1,1,0,0]

i=2(s[1]='a')
继承:dp[2][j] = dp[1][j]
选用:

  • j=1:匹配 'a',dp[2][1] += dp[1][0] = 1 → dp[2][1] = 1+1=2
  • j=2:匹配 '?',dp[2][2] += dp[1][1] = 1 → dp[2][2] = 1
    结果:dp[2] = [1,2,1,0]

i=3(s[2]='b')
继承:dp[3][j] = dp[2][j]
选用:

  • j=1:不匹配('b'!='a'),不加
  • j=2:匹配 '?',dp[3][2] += dp[2][1] = 2 → dp[3][2] = 1+2=3
  • j=3:匹配 'c'?不匹配,不加
    结果:dp[3] = [1,2,3,0]

i=4(s[3]='c')
继承:dp[4][j] = dp[3][j]
选用:

  • j=1:不匹配
  • j=2:匹配 '?',dp[4][2] += dp[3][1] = 2 → dp[4][2] = 3+2=5
  • j=3:匹配 'c',dp[4][3] += dp[3][2] = 3 → dp[4][3] = 0+3=3
    结果:dp[4] = [1,2,5,3]

答案 dp[4][3] = 3,与之前分析一致。

步骤 9:扩展到括号组
若 p 中有括号组,例如 (ab)*,则在 NFA 中对应一个循环:状态 q1 读入 "a" 到 q2,读入 "b" 到 q3,q3 有 ε 转移回 q1(允许零次时还有旁路)。
然后使用上述 NFA DP 即可计算。

总结
本题的核心是将模式串转换为有限自动机(NFA),然后用动态规划计算子序列匹配的路径数。
状态定义为 dp[i][q] 表示 s 的前 i 个字符的子序列,有多少种方式到达自动机状态 q。
转移时考虑“不选用当前字符”和“选用当前字符并沿自动机边转移”两种情况。
最终答案是所有接受状态的 dp[n][q] 之和。
这种方法能统一处理普通字符、通配符、括号组及其量词,是此类“子序列匹配复杂模式”计数问题的通用解法。

字符串模糊匹配中的“重复子序列”问题(计数版) 题目描述 给定一个主字符串 s (长度为 n)和一个模式字符串 p (长度为 m),模式串中可能包含字符、通配符 ? (匹配任意单个字符)以及量词 * (匹配零个或多个任意字符)。 特别地,模式串中可能出现 带括号的重复子表达式 ,例如 (ab)* 表示零个或多个连续的 "ab" 子串。 为了简化,我们假设括号内只包含普通字符(无通配符或量词),且括号后紧跟一个量词: * (零次或多次)、 + (一次或多次)或 ? (零次或一次)。 本题的目标是:计算主串 s 中,有多少个子序列(非连续)能够匹配模式串 p ? 注意:子序列是从主串中删除若干个字符(可以为 0)后剩余的字符按原顺序组成的序列。匹配的定义是:子序列与模式串按规则完全匹配。 示例 输入: s = "abab" , p = "(ab)*" 解释:模式 (ab)* 表示零个或多个 "ab"。 子序列匹配情况: 取子序列 ""(空串)匹配零次重复,计 1 种。 取子序列 "ab"(如取 s 的第 1、2 个字符)匹配一次重复,计 1 种。 取子序列 "abab"(取全部字符)匹配两次重复,计 1 种。 输出:3 输入: s = "aabc" , p = "a?c" 解释:模式 a?c 中 ? 匹配任意单个字符。 子序列匹配情况: 取子序列 "abc"(删除第二个 'a')中,'?' 匹配 'b',计 1 种。 取子序列 "aac"(删除 'b')中,'?' 匹配第二个 'a',但注意子序列需保持顺序,这里第二个字符是 'a',可以匹配 '?'。 实际上,从 "aabc" 中取三个字符组成子序列,且第一个字符为 'a'、第三个字符为 'c' 的可能组合: 取索引 (0,1,3) 得 "aac",'?' 匹配 'a'。 取索引 (0,2,3) 得 "abc",'?' 匹配 'b'。 取索引 (1,2,3) 得 "abc",但第一个字符是第二个 'a',模式第一个字符是 'a',仍匹配,但此时 '?' 匹配 'b',与上一种重复吗? 注意:子序列是由原串索引唯一确定的,不同索引视为不同子序列,即使得到的字符串相同。 因此,应统计所有索引递增的子序列。 输出:3(具体解释见后文计算) 解题过程循序渐进讲解 步骤 1:问题拆解与状态定义 本题核心是:主串 s 的子序列(非连续)匹配模式 p。 模式 p 中可能有括号重复组,例如 (ab)* 。我们可以将模式 p 解析成一个 “模式单元”序列 。 每个模式单元是以下之一: 普通字符(如 'a') 通配符 '?' 带量词的括号组,如 (ab)* 定义 dp[i][j] 表示:考虑主串 s 的前 i 个字符(即 s[0..i-1] )的子序列,匹配模式 p 的前 j 个模式单元,有多少种匹配方式。 这里 i 从 0 到 n(i=0 表示空主串前缀),j 从 0 到 M(M 是模式单元个数,将 p 解析后得到)。 最终答案是 dp[n][M] 。 步骤 2:模式串解析 我们需要将 p 解析为模式单元列表。 例如 p = "a(ab)*c?" 解析为: 单元 1: 普通字符 'a' 单元 2: 括号组内容 "ab",量词 '* ' 单元 3: 普通字符 'c' 单元 4: 通配符 '?' 实现时,可以用一个数组 units 存储每个单元的类型(CHAR, WILD, GROUP)和额外信息(字符、组字符串、量词类型 * / + / ?)。 步骤 3:状态转移方程(无括号组时) 先考虑简单情况:模式单元只有普通字符和 '?'。 dp[0][0] = 1 (空子序列匹配空模式)。 对于 i > 0 , j > 0 : 我们可以 不选用 s[i-1] 来匹配第 j 个模式单元,那么匹配数继承自 dp[i-1][j] 。 我们可以 选用 s[i-1] 来匹配第 j 个模式单元,当且仅当 s[i-1] 与单元 j 匹配(字符相等或单元为 '?')。此时匹配数加上 dp[i-1][j-1] 。 所以: 其中 match 判断单个字符与单元匹配。 步骤 4:引入括号组(带量词)的匹配 括号组可能重复多次,例如 (ab)* 可以匹配 ""、"ab"、"abab"... 我们考虑将括号组视为一个“子模式”,其匹配需要动态规划。 定义辅助状态 groupDP[k][t] 表示:用主串 s 的某段前缀(前 k 个字符)的子序列,匹配括号组重复 t 次,有多少种方式。 但 t 可以很大(理论上无限),不过注意到主串长度有限,最多重复次数受限于主串长度。 更实用的方法是:将括号组的匹配嵌入到主 DP 中。 设当前单元是括号组 G,量词为 Q(* / + / ?)。 我们可以在主 DP 中新增一维表示“当前处于该括号组的第几次重复”,但这样状态太多。 另一种思路:将括号组的匹配转化为 “子序列匹配子串” 问题。 对于括号组 G(字符串 g,长度为 L),我们预计算一个数组 f[i][r] 表示:从 s 的前 i 个字符中,选取子序列匹配 g 重复 r 次(即 g 连续重复 r 次构成的字符串)的方法数。 然后,在主 DP 中,当遇到括号组单元时,我们枚举可能的重复次数 r(根据量词决定 r 的范围),然后从 dp[i'][j-1] 转移到 dp[i][j] ,其中转移时乘以 f[i][r] 但需注意子序列选取不重叠。 实际上更精确的是:我们将括号组视为一个整体单元,用 DP 来计算从 s 的某段区间内选取子序列匹配该组重复若干次的方法数,然后合并到主 DP。 步骤 5:简化版解法(量词只有 * ) 为了清晰,我们先考虑量词只有 * (零次或多次)的情况。 定义 dp[i][j] 如上,并增加一个辅助状态 gp[i][j] 表示:考虑 s 的前 i 个字符,当前正在匹配括号组 G(第 j 个单元),且 已经匹配了至少一次该组 的情况下,有多少种匹配方式(即匹配了 G* 中的若干次重复)。 转移时: 若单元 j 是普通字符或 '?',用前述转移。 若单元 j 是括号组 G* : 我们可以决定匹配零次:直接从 dp[i][j-1] 继承到 dp[i][j] 。 我们可以匹配至少一次:这需要进入 gp 状态。 gp[i][j] 可以从 gp[i-1][j] 继承(不选用 s[ i-1]),也可以从 dp[i-L][j-1] 转移过来(开始第一次匹配,其中 L 是组 G 的长度,且 s[ i-L..i-1] 的子序列要匹配 G),还可以从 gp[i-L][j] 转移(增加一次重复匹配)。 但这里要求子序列匹配 G 重复 r 次,且子序列索引递增,不能混淆。 步骤 6:实际实现技巧(将括号组视为一个字符类) 我们可以将括号组 G 量词 Q 转换为 匹配一个“块” ,这个块是 G 重复若干次(根据 Q 决定最少和最多次数)。 然后,问题转化为:主串 s 中选取子序列,使得它是由这些“块”按顺序组成的序列。 这等价于:每个模式单元(包括括号组)匹配主串的一个子序列片段(非连续)。 更简单的方法:将模式串 p 展开所有可能的常规字符串(无括号),然后对每个展开串计算 s 的子序列匹配数,最后求和。但展开可能太多,不可行。 步骤 7:通用 DP 设计 我们换一种状态定义: dp[i][j] 表示:s 的前 i 个字符中,选取子序列匹配 p 的前 j 个字符(将括号组视为普通字符序列)的方法数。 但这样括号组量词难以处理。 最终方案: 将模式串 p 转换为一个 NFA(非确定性有限自动机),其中每个状态代表模式匹配的位置。 用 DP 计算: dp[i][q] 表示 s 的前 i 个字符的子序列,有多少种方式到达 NFA 状态 q。 初始状态 dp[0][start] = 1 。 转移: 不选用 s[ i-1]: dp[i][q] += dp[i-1][q] 选用 s[ i-1]:若从状态 q 通过字符 s[ i-1](或通配符 '?')可以到达状态 q',则 dp[i][q'] += dp[i-1][q] 对于 ε 转移(如量词 * 可以重复零次),需要预先处理闭包。 最终答案是所有接受状态的 dp[ n][ q ] 之和。 这实际上将问题转化为 子序列匹配正则表达式 的计数问题。 步骤 8:示例详细计算(第二个示例) s = "aabc", p = "a?c"(无括号组,简单情况) 模式单元:1:'a', 2:'?', 3:'c' dp[ i][ j ] 表(行 i=0..4,列 j=0..3): 初始化:dp[ 0][ 0 ] = 1,其余 0。 i=1(s[ 0 ]='a') j=1:匹配 'a',dp[ 1][ 1] += dp[ 0][ 0 ] = 1 其他 j 继承上一行:dp[ 1][ 0] = dp[ 0][ 0 ] = 1 结果:dp[ 1] = [ 1,1,0,0 ] i=2(s[ 1 ]='a') 继承:dp[ 2][ j] = dp[ 1][ j ] 选用: j=1:匹配 'a',dp[ 2][ 1] += dp[ 1][ 0] = 1 → dp[ 2][ 1 ] = 1+1=2 j=2:匹配 '?',dp[ 2][ 2] += dp[ 1][ 1] = 1 → dp[ 2][ 2 ] = 1 结果:dp[ 2] = [ 1,2,1,0 ] i=3(s[ 2 ]='b') 继承:dp[ 3][ j] = dp[ 2][ j ] 选用: j=1:不匹配('b' !='a'),不加 j=2:匹配 '?',dp[ 3][ 2] += dp[ 2][ 1] = 2 → dp[ 3][ 2 ] = 1+2=3 j=3:匹配 'c'?不匹配,不加 结果:dp[ 3] = [ 1,2,3,0 ] i=4(s[ 3 ]='c') 继承:dp[ 4][ j] = dp[ 3][ j ] 选用: j=1:不匹配 j=2:匹配 '?',dp[ 4][ 2] += dp[ 3][ 1] = 2 → dp[ 4][ 2 ] = 3+2=5 j=3:匹配 'c',dp[ 4][ 3] += dp[ 3][ 2] = 3 → dp[ 4][ 3 ] = 0+3=3 结果:dp[ 4] = [ 1,2,5,3 ] 答案 dp[ 4][ 3 ] = 3,与之前分析一致。 步骤 9:扩展到括号组 若 p 中有括号组,例如 (ab)* ,则在 NFA 中对应一个循环:状态 q1 读入 "a" 到 q2,读入 "b" 到 q3,q3 有 ε 转移回 q1(允许零次时还有旁路)。 然后使用上述 NFA DP 即可计算。 总结 本题的核心是将模式串转换为有限自动机(NFA),然后用动态规划计算子序列匹配的路径数。 状态定义为 dp[i][q] 表示 s 的前 i 个字符的子序列,有多少种方式到达自动机状态 q。 转移时考虑“不选用当前字符”和“选用当前字符并沿自动机边转移”两种情况。 最终答案是所有接受状态的 dp[ n][ q ] 之和。 这种方法能统一处理普通字符、通配符、括号组及其量词,是此类“子序列匹配复杂模式”计数问题的通用解法。