10. 正则表达式匹配
考点
- LCS
- 初始值、状态转移设计
思路
一、问题概述
给定字符串 s 与模式串 p,判断
p 是否能够完全匹配 s。
模式串仅包含三类字符:
- 普通字符(如
a、b) - 通配符
.:可匹配任意单个字符 - 修饰符
*:表示其前一个字符可以出现 0 次或任意多次
需要注意的是:
*不是一个独立字符,它始终与其前一个字符构成一个整体(如a*、.*)。
二、动态规划建模
1. 状态定义
定义二维 DP 数组: \[ f[i][j] = \begin{cases} \text{true} & \text{若 } s[0..i-1] \text{ 能匹配 } p[0..j-1] \\ \text{false} & \text{否则} \end{cases} \] 其中:
- \(i \in [0, |s|]\)
- \(j \in [0, |p|]\)
这是前缀匹配型 DP,而不是“是否出现子串”。
三、初始化的关键难点(本题最易出错点)
1. 基础状态
\[ f[0][0] = \text{true} \]
空字符串与空模式串显然匹配。
2. 空字符串 vs 非空模式串
这是整道题最容易出错的地方。
空字符串 并不能 匹配任意模式串,例如:
""❌"a"""❌"abc"
但以下情况是合法的:
""✅"a*"""✅"a*b*c*"
也就是说:
空字符串并不能匹配任意模式前缀。只有当模式前缀可以完全由若干个
t*片段拼接而成(其中t为普通字符或.)时,空字符串才可能匹配该前缀。例如""可匹配c*、a*b*c*、.*,但不能匹配a、ab、.*a。
因此初始化第 0 行时,必须严格遵循 * 的语义: \[
f[0][j] =
\begin{cases}
f[0][j-2] & \text{若 } p[j-1] = '*' \\
\text{false} & \text{否则}
\end{cases}
\] 对应代码:
1 | for (int j = 2; j <= n2; ++j) |
绝不能把整行 f[0][j] 初始化为
true,否则会导致例如:
1 | s = "aaa" |
被错误判为 true。
四、状态转移方程设计
1. 普通字符 / 点号 .
当 p[j-1] 是普通字符,或 .:
若满足 p[j-1] = s[i-1] 或
p[j-1] = '.',则: \[
f[i][j] = f[i-1][j-1]
\] 含义:当前字符成功匹配,问题规模缩小为前一位。
2. 星号 *(核心难点)
设 p[j-1] == '*',则其修饰字符为
p[j-2]。
情况一:x* 匹配 0 次
直接忽略 x* 这两个字符: \[
f[i][j] = f[i][j-2]
\]
情况二:x* 匹配 ≥ 1 次
前提:x 能匹配 s[i-1],即
p[j-2] = s[i-1] 或 p[j-2] = '.'。
此时: \[
f[i][j] = f[i-1][j]
\] 含义:当前字符被 x* 吸收,模式串仍可继续使用
*。
3. 综合转移公式
\[ f[i][j] = \begin{cases} f[i-1][j-1] & \text{若字符或 '.' 匹配} \\ f[i][j-2] \lor f[i-1][j] & \text{若 } p[j-1]='*' \text{ 且前字符可匹配} \end{cases} \]
五、算法复杂度分析
- 时间复杂度:
\[ O(|s|\cdot|p|) \]
- 空间复杂度:
\[ O(|s|\cdot|p|) \]
在本题限制(长度 ≤ 20)下完全可行。
六、完整 AC 代码(附录)
1 | class Solution { |