字符串模糊匹配中的“重复子序列”问题(计数版)
题目描述
给定一个主字符串 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]。
所以:
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* 中的若干次重复)。
转移时:
- 若单元 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] - 对于 ε 转移(如量词 * 可以重复零次),需要预先处理闭包。
- 不选用 s[i-1]:
- 最终答案是所有接受状态的 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] 之和。
这种方法能统一处理普通字符、通配符、括号组及其量词,是此类“子序列匹配复杂模式”计数问题的通用解法。