LeetCode 10. 正则表达式匹配

162 阅读2分钟

目录:算法日记

题目来源:10. 正则表达式匹配

题目描述

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖整个字符串 s 的,而不是部分字符串。

题目示例

输入: s = "ab", p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

数据范围

  • 1<=s.length <=201 <= s.length <= 20
  • 1<=p.length <=301 <= p.length <= 30
  • s 只包含从 a-z 的小写字母。
  • p 只包含从 a-z 的小写字母,以及字符 . 和 *
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

算法思路

两个字符串间匹配问题考虑动态规划序列模型,使用闫式DP法分析,确保状态不重不漏

动态规划.png

如上图所示,dp[i][j]表示字符串s中前i个字符与字符串p中前j个字符是否匹配。若dp[i][j]true说明存在匹配的方案。需要特别注意,'*'需与前一位字符配合使用

接下来考虑状态计算。设当前字符串s中第i个字符与字符串p中第j个字符进行匹配。由于'*'涉及情况最多,因此对'*'分情况讨论。

  • 若不存在'*'
    • dp[i-1][j-1]有合法方案,且s[i]p[j]匹配,则存在dp[i][j]合法方案;
  • 若存在'*'
    1. '*'表示0个字符,dp[i][j-2]
    2. '*'表示1个字符:dp[i-1][j-2],且s[i]p[j-1]匹配;
    3. '*'表示2个字符:dp[i-2][j-2],且s[i-1]p[j-1]匹配,且s[i]p[j-1]匹配;
    4. ...
    • 这里借鉴完全背包问题优化思路,对于当前状态dp[i][j]dp[i-1][j]已知,且对于状态dp[i-1][j]有:
      • '*'表示0个字符,dp[i-1][j-2]
      • '*'表示1个字符,dp[i-2][j-2],且s[i-1]p[j-1]匹配;
      • ...
      • 由上述推导可知,dp[i-1][j]状态可通过添加s[i]p[j-1]匹配关系,来表示上述2-4之间的状态。
    • 2-4之间状态可优化为:dp[i-1][j]有合法方案,且s[i]p[j-1]匹配,则存在dp[i][j]合法方案;

AC代码

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
var isMatch = function(s, p) {
    let n = s.length;
    let m = p.length;
    s = ' ' + s;
    p = ' ' + p;
    const dp = new Array(n + 1).fill(0).map(x => new Array(m + 1).fill(false));
    dp[0][0] = true;
    for(let i = 0; i <= n; ++i) {
        //p长度为0时必然匹配失败,从1开始
        for(let j = 1; j <= m; ++j) {
            //*必须与前一位字符配合使用
            if(j + 1 <= m && p[j + 1] === '*') continue;
            if(p[j] !== '*') {
                dp[i][j] = i && dp[i - 1][j - 1] && (p[j] === s[i] || p[j] === '.');
            } else {
                dp[i][j] = dp[i][j - 2] || i && dp[i - 1][j] && (p[j - 1] === s[i] || p[j - 1] === '.');
            }
        }
    }
    return dp[n][m];
};