动态规划之正则表达式匹配

动态规划之正则表达式匹配

leetcode-10 - 困难

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

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

动态规划

该题属于 leetcode 中的困难难度的题目,使用动态规划解决的关键在于找到合适的动态转移方程。

步骤一 定义子问题

将原问题的规模缩小为一个子问题。原问题是判断字符串s和字符规律p是否匹配,则子问题可以描述为判断字符串s(m)和字符规律p(n)是否匹配,其中 0 <= m < s.length()0 <= n < p.length()

与原问题相比,子问题是参数化的。动态规划是通过解决所有的子问题,进而解决原问题。因此,子问题需要具备两个特点:

  • 原问题可以由子问题表示。当 m == s.length()-1n == p.length()-1 时,子问题也就变成了原问题。
  • 一个子问题的解要能通过其他子问题的解得到。通过对 p[n] 分情况讨论可以由前面已解决的子问题的解推导出 s[m] 和 p[n] 是否匹配。

步骤二 确定子问题的递推关系(动态转移方程)

通过观察,确定子问题”s(m)与p(n)是否匹配”是否可以由前面已经解决的子问题来解决。
由于字符''的存在,字符规律p(n)可以与字符串s(m)不等长,此时会比较难以判断,因此我们可以根据p(n)的最后一个字符p[n]是否为''来对字符规律p(n)进行分类讨论:

  • p[n]!='*'
    这里根据p[n]是否为'.'又可以分为两种情况:

    • p[n]=='.'
      由于'.'可以匹配任何字符,因此,"s(m)与p(n)是否匹配"取决于"s(m-1)和p(n-1)是否匹配":

    • p[n]!='.'
      此时,"s(m)与p(n)是否匹配"不仅取决于"s(m-1)和p(n-1)是否匹配",还要看"s[m]与p[n]是否相等"。

  • p[n]=='*'

    '*'可以匹配零个或多个前面的字符,匹配零个和匹配多个有所区别,因此可以将其分开讨论:

    • '*'匹配零个前面的字符,也即可以将前面的字符"消除"掉,这样只需判断s(m)与p(n-2)是否匹配即可:

    • '*'匹配至少一个前面的字符,这样,匹配字串一定是以与p[n-1]相同的字符结尾(但不一定就在p[n-1]处结尾):

      上图紫色框中至少有一个p[n-1]的字符,则灰色框中有零个或多个p[n-1],正好符合'*'的匹配规则,因此上图可以转换为下图:

      我们只需判断s[m]与p[n-1]是否相等以及s(m-1)与p(n)是否匹配即可。

我们使用f[i][j]表示s(i)和p(j)是否匹配(i 即上图 m,j 即上图 n),综合上述讨论,我们可以得到如下的递推关系:

步骤三 寻找边界条件

  • p[j]!='*'
    我们需要对f[i-1][j-1]分情况讨论:
    有可能为0,因此我们需要事先知道f[-1][j-1](j>0)f[i-1][-1](i>0)以及f[-1][-1]的所有值。

    • 如果i=j=0,则f[-1][-1]表示 s 和 p 均为空时的情形,此时两者是匹配的,f[-1][-1]=true
    • 如果i>0,j=0f[i-1][-1]表示 s 不为空,p 为空的情形,此时两者是绝对不匹配的,f[i][-1]=false
    • 如果i=0,j>0f[-1][j-1]表示 s 为空,p 不为空的情形,此时两者是有可能匹配的,因为 p 中可能存在 '*' 将前面的字符消除掉的的情况,如"a*"或"a*b*"等。但是,这种情况符合状态转移方程(2),完全可以根据f[i][j-2]进行判断,而f[i][j-2]同样符合状态转移方程(2)(j-2>0的情况下),直到j-2=-1,而f[-1][-1]=true。因此,此种情况是可以由转移方程得到正确的结果,不需要事先进行判断。
    • 如果i>0,j>0f[i-1][j-1]的值在之前的子问题中已然解决,可直接使用。
  • p[j]='*'
    根据题目条件,'*'是不可能出现在 p 的第一个字符位置的,可推出j>0,即f[i][j-2]j-2的最小值为-1,match(s[i],p[j-1])j-1的最小值为0,而f[i-1][j]中的j-1的最小值为-1,这些值在前面已然讨论。

为了方便对 s 和 p 为空时进行判断,我们将上面讨论的 f 数组的索引全部加1,使其从0开始,即使用f[0][0]表示 s 和 p 均为空时两者是否匹配,使用f[1][1]表示 s 的子串 s(1) 和 p 的子串 p(1) 是否匹配,......。如下图所示,灰色区域是必然为false,红圈处除了f[0][0]必然为true外,要看其字符是否为'*'以及其前方第二个位置的值是否为true,而黄色区域则可以完全使用转移方程来判断。事实上,第一行除了f[0][0]需要特别指定值外,是可以与转移方程的代码合并的。

代码

这里的代码直接使用leetcode题解中的官方代码,自己的代码在判断边界条件方面不够简洁:

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();

        boolean[][] f = new boolean[m + 1][n + 1];
        f[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*') {
                    f[i][j] = f[i][j - 2];
                    if (matches(s, p, i, j - 1)) {
                        f[i][j] = f[i][j] || f[i - 1][j];
                    }
                } else {
                    if (matches(s, p, i, j)) {
                        f[i][j] = f[i - 1][j - 1];
                    }
                }
            }
        }
        return f[m][n];
    }

    public boolean matches(String s, String p, int i, int j) {
        if (i == 0) {
            return false;
        }
        if (p.charAt(j - 1) == '.') {
            return true;
        }
        return s.charAt(i - 1) == p.charAt(j - 1);
    }
}

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted on 2021-10-28 00:27  champock  阅读(261)  评论(0)    收藏  举报

导航