LeetCode 10 | 正则表达式匹配:动态规划详解

8 阅读2分钟

在这篇文章里,我将带你深入理解 正则表达式匹配 这道题,并讲解 动态规划(DP)解法的核心思路、表格填法和代码实现。


一、题目理解

给定一个字符串 s 和一个模式 p,判断 s 是否能匹配 p

  • . 匹配任意单个字符
  • * 匹配零个或多个 前一个元素

示例

示例 1:
s = "aa", p = "a*"true
解释: '*' 表示可以重复前一个字符 'a' 多次,"aa" 可以匹配 "a*"

示例 2:
s = "mississippi", p = "mis*is*p*."false

二、为什么用动态规划

  • 匹配问题存在重叠子问题

    • 匹配 s[0..i]p[0..j] 可以通过匹配前面子串的结果递推得到
  • * 的零次或多次选择使得暴力递归容易超时

  • DP 用二维表格记录所有子问题,避免重复计算


三、DP 状态定义

dp[i][j] = s[0..i-1] 是否能匹配 p[0..j-1]
  • i = 0 → 空字符串
  • j = 0 → 空模式
  • 最终答案:dp[m][n],其中 m = s.length(), n = p.length()

四、DP 转移方程

1. 普通字符或 .

dp[i][j] = dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.')
  • 左上格 dp[i-1][j-1] 决定前缀是否匹配
  • 当前字符匹配(相等或 .)才为 true

2. * 情况

dp[i][j] = dp[i][j-2] || 
           (dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.'))
  • 零次匹配dp[i][j-2] → 忽略 * 和前一个字符
  • 多次匹配dp[i-1][j] → 当前字符匹配前一个字符,可以继续消耗 s

五、表格初始化

  1. 空字符串匹配空模式:
dp[0][0] = true;
  1. 空字符串匹配带 * 的模式:
for (int j = 2; j <= n; j++) {
    if (p.charAt(j-1) == '*') dp[0][j] = dp[0][j-2];
}

解释:空串只能通过 * 匹配 0 次,所以依赖左边两格的值


六、例子演示

s = "aab"
p = "c*a*b"

DP 表(T=true, F=false)

s\p""c*a*b
""TFTFTF
aFFFTTF
aFFFFTF
bFFFFFT
  • 每个格子依赖:

    • 左上:普通字符匹配
    • 左两格:* 零次匹配
    • 上格:* 多次匹配

七、Java 实现

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];

        dp[0][0] = true; // 空串匹配空模式

        // 初始化空字符串匹配模式
        for (int j = 2; j <= n; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2];
            }
        }

        // 填表
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (p.charAt(j - 1) == '*') {
                    dp[i][j] = dp[i][j - 2] ||
                               (dp[i - 1][j] && (s.charAt(i - 1) == p.charAt(j - 2) 
                                                 || p.charAt(j - 2) == '.'));
                } else {
                    dp[i][j] = dp[i - 1][j - 1] &&
                               (s.charAt(i - 1) == p.charAt(j - 1) 
                                || p.charAt(j - 1) == '.');
                }
            }
        }
        return dp[m][n];
    }
}

八、总结

  1. DP 核心思想:记录所有子串匹配结果,避免重复计算
  2. * 处理分两种情况:零次 / 多次
  3. 第一行初始化处理空字符串匹配带 * 的模式
  4. 时间复杂度:O(m * n),空间复杂度:O(m * n)

Tip:如果空间敏感,可以优化为滚动数组,压缩到 O(n)