动态规划:驯服正则表达式的*号魔王

哈喽,各位,我是前端小L。

上一篇 文章中,我们刚刚驯服了通配符匹配中的 *,它拥有"凭空消失"和"无限吞噬"两种神力。我们当时以为,这已经是匹配问题的极限了。

然而,今天,我们将面对一个更古老、更强大、也更狡猾的对手------正则表达式中的 *。它不再是那个可以独立行动的"大魔王",它的所有力量,都必须依附于它前面的那个字符 。这个看似微小的规则变化,将彻底颠覆我们的状态转移逻辑,要求我们进行一次前所未有的"向前看两位"的精密操作。

这,就是"正则表达式匹配"。

力扣 10. 正则表达式匹配

https://leetcode.cn/problems/regular-expression-matching/

题目分析 & 核心区别: 给定一个字符串 s 和一个模式 p,实现正则表达式匹配。

  • .:可以匹配任何单个 字符。(等同于上一题的 ?

  • *:匹配零个或多个前面的那个元素。

划重点!* 的意义完全变了!

  • 在"通配符匹配"中,* 是独立的,匹配任意序列。

  • 在"正则表达式"中,* 必须和它前面的字符 c 组成 c*,作为一个整体,表示 c 可以出现0次、1次、或任意多次。* 永远不会单独出现。

这个"捆绑"关系,是解开本题所有谜题的唯一钥匙。

DP棋盘的再构建:在捆绑中寻找出路

我们依然使用二维DP棋盘作为战场。

1. DP状态定义: dp[i][j] 表示:字符串 s 的前 i 个字符 (s[0...i-1]) 是否能与模式 p 的前 j 个字符 (p[0...j-1]) 完全匹配

2. 状态转移的"终极"对决: 当我们计算 dp[i][j] 时,我们依然聚焦于模式的最后一个字符 p[j-1]

  • Case 1: p[j-1] 不是 * (是普通字母或 .) 这是简单模式。要匹配成功,s[i-1] 必须能与 p[j-1] 匹配(要么相等,要么 p[j-1].),并且它们的前缀也必须已经匹配成功。 dp[i][j] = match(s[i-1], p[j-1]) && dp[i-1][j-1] (其中 match 是一个辅助判断函数)

  • Case 2: p[j-1]* (Boss登场!) * 必须和它前面的 p[j-2] 捆绑在一起,形成 p[j-2]* 这个组合。这个组合有两种决策:

    • 决策A:"零次"------让 p[j-2]* 这个组合直接作废 思路 : 我们让 p[j-2]* 匹配一个空序列,把它当成"空气"。 后果 : 那么 s 的前 i 个字符能否匹配 p 的前 j 个字符,就完全取决于它能否匹配 p 的前 j-2 个字符(即跳过 p[j-2]* 这个组合)。 公式 : dp[i][j-2]

    • 决策B:"一次或多次"------让 p[j-2]* 至少匹配一个字符 思路 : 我们要用 p[j-2]* 这个组合,去"吞噬"字符串末尾的 s[i-1]前提 : s[i-1] 必须能和 p[j-2] 匹配上(要么相等,要么 p[j-2].)。如果连这都做不到,这个决策就直接失败。 后果 : 如果匹配上了,说明 s[i-1] 被成功"消化"掉了。现在的问题,变成了 s 的前 i-1 个字符,能否与 p 的前 j 个字符继续匹配。 为什么还是 p 的前 j 个字符? 因为 * 的魔力还在!p[j-2]* 这个组合在吞噬掉 s[i-1] 之后,依然可以继续去吞噬 s[i-2] 等等。 公式 : match(s[i-1], p[j-2]) && dp[i-1][j]

    最终,dp[i][j] 只要满足这两种决策中的任意一种 ,就算成功。 dp[i][j] = dp[i][j-2] || (match(s[i-1], p[j-2]) && dp[i-1][j])

3. Base Cases

  • dp[0][0] = true:空对空,匹配。

  • dp[i][0] (i > 0):非空字符串对空模式,不匹配。

  • dp[0][j] (j > 0):空字符串对非空模式。只有当模式是 a*b*c*... 这种形式时才可能匹配。 if (p[j-1] == '*') { dp[0][j] = dp[0][j-2]; }

代码实现

复制代码
class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.length();
        int n = p.length();

        auto matches = [&](int i, int j) {
            if (i == 0) return false;
            if (p[j - 1] == '.') return true;
            return s[i - 1] == p[j - 1];
        };

        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));

        // Base Cases
        dp[0][0] = true;
        for (int j = 1; j <= n; ++j) {
            if (p[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[j - 1] != '*') {
                    if (matches(i, j)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                } else { // p[j-1] is '*'
                    // 决策A: 匹配0次
                    dp[i][j] = dp[i][j - 2];
                    // 决策B: 匹配1次或多次
                    if (matches(i, j - 1)) {
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                    }
                }
            }
        }
        return dp[m][n];
    }
};

注:为了处理 i=0 的情况,将 match 逻辑封装成辅助函数,代码更清晰。

总结:二维序列DP的巅峰对决

对比项 通配符匹配 (LC 44) 正则表达式匹配 (LC 10)
* 的含义 独立的,匹配任意序列 依附的,匹配前一个字符的0次或多次
* 的状态来源 dp[i-1][j] (吞噬) dp[i][j-1] (消失) dp[i-1][j] (吞噬) dp[i][j-2] (消失)
核心区别 * 的"消失"是看j-1 * 的"消失"是看j-2,需要跳过char*组合
思维复杂度 很高 极高 ,需要理解*的依附性

今天,我们终于攻克了二维序列DP中,逻辑最复杂、细节最魔鬼的终极Boss。它教会我们,动态规划的建模,本质上是将一个复杂问题的规则,不重不漏、无歧义地翻译成数学语言(状态转移方程)的过程。

* 依附于前一个字符的这个简单规则,导致其"消失"的行为从 dp[i][j-1] 变成了 dp[i][j-2],这正是DP状态转移严谨性的完美体现。

咱们下期再见!

相关推荐
蒋星熠1 天前
爬虫中Cookies模拟浏览器登录技术详解
开发语言·爬虫·python·正则表达式·自动化·php·web
hanliu20032 天前
实训11 正则表达式
正则表达式
fruge3 天前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
爱吃甜品的糯米团子3 天前
JavaScript 正则表达式:选择、分组与引用深度解析
前端·javascript·正则表达式
高山上有一只小老虎3 天前
java 正则表达式大全
java·正则表达式
weixin_436804074 天前
正则表达式可视化 - 正则表达式可视化与文本匹配工具
正则表达式
盼哥PyAI实验室6 天前
正则表达式:文本处理的强大工具
java·服务器·正则表达式
盼哥PyAI实验室6 天前
Python 正则表达式实战 + 详解:从匹配QQ邮箱到掌握核心语法
python·mysql·正则表达式
.又是新的一天.6 天前
09-正则表达式
正则表达式
lkbhua莱克瓦247 天前
Java练习-正则表达式 1
java·笔记·正则表达式·github