Leetcode Regular Expression Matching O(n^2) DP

来源:互联网 发布:ubantu安装mysql数据库 编辑:程序博客网 时间:2024/05/16 04:38

一些废话,写在前面

这是我在CSDN的第一篇博客,之前看了许多业界大牛的鸡汤文,纷纷强调了记录和写作的重要性:一方面,只有当人在尝试着复述知识的时候才能找到逻辑的缺漏并完善之;另一方面,写作可以明确自己的想法,表达自己的意愿,同时可以让读者有所收获,是一个优秀的工作者必备的能力。因此我决定也开通博客,记录自己的所学所思。
本人在读大三,马上要升入大四、离开安逸的象牙塔。自知起步已晚,定当更加努力才是。这里是第一篇博客,可能表达生疏晦涩,望读者谅解。

来说正事

本文给出了leetcode上Regular Expression Matching一题时间复杂度为O(n^2)的动态规划解法。
原题请戳:https://leetcode.com/problems/regular-expression-matching/

题目描述

完成isMatch(s, p)函数,检验字符串s是否匹配正则表达式p。在正则表达式中:
‘.’匹配任意单个字符
‘*’匹配任意多(0到无穷)个前驱字符

例如

isMatch("aa","a") → falseisMatch("aa","aa") → trueisMatch("aaa","aa") → falseisMatch("aa", "a*") → trueisMatch("aa", ".*") → trueisMatch("ab", ".*") → trueisMatch("aab", "c*a*b") → true

解题思路

一个标准错误思路

使用贪心匹配方法,进行一趟匹配:

若s[i]==p[j]或p[j]=='.'时则i++, j++;若p[j]=='*',p[j-1] == s[i]则i++;若i,j同时为终结符,返回true,否则返回false。

使用以上的各组样例来测试该思路,觉得这个思路似乎靠谱。但仔细思考一下不难发现,*通配符是贪心算法的一个隐患。由于*通配符无法智能地判断应该通配多长,所以在对被通配的字符有严格长度限制的时候会出错。

例如isMatch(“aac”, “a*ac”)经过贪心匹配后,第一个a*将aac中前两个a都进行通配,导致a*匹配完毕后,原字符串只剩下c一个字符,无法继续匹配,函数返回false。但很显然这个字符串应该是成功匹配的。

既然不知道该通配多少,穷举一下就可以了

由此引出该问题的暴力解法,当遇到通配符*时,尝试向后通配0,1,2,…,L个字符,如果能匹配的话,最终总有一个是正确的。

但当数据为s=”aaaaaaaaaaaaaaaaaaaac”, p=”a*a*a*…a*c”时,该暴力算法的时间复杂度是指数级的。

对于通配符*通配每一种长度都进行穷举,并且一条路算到字符串/正则表达式的结尾实在是太耗时了。(如果读者熟悉编译原理,这相当于对一个设计不良的文法实行自顶向下分析时出现的“回溯”问题。)

面对递归穷举,思考是否有最优子结构

假设我们已经有一个字符串s,以及一个成功匹配的模式p。无疑的是,s的任何前缀s[1…i], i < length(s),都必然与p的某个前缀p[1…j], j < length(p)匹配。

从状态机的角度来简单证明,若p是一个FSM,且p可以描述s,则s的任何状态转移都应该在p的某个可行状态中。而若s[1…i]在s[i]时不属于p中的状态,则状态机p不能描述s

用f[i, j]=true表示s[i]与p[j]之前(即s[1…i-1],p[1…j-1])匹配。当检查到s[i-1]和p[j-1]的时候,只需要知道s[i-1]是否在p中有对应即可。由此得到状态转移方程

f[i, j] = f[i-1, j-1] && (p[j-1] == '.' || s[i-1] == p[j-1])                                         (p[j-1] != '\*')f[i, j] = f[i, j-2] ||     (f[i-1, j] && (p[j-2] == '.' || s[i-1] == p[j-2]))                                         (p[j-1] == '\*')

解释:

当p[j-1]不是星号的时候,如果s[i-1]和p[j-1]之前已经匹配好,并且s[i-1]和p[j-1]相等,那么s[i-1]和p[j-1]也可以匹配。

当p[j-1]是星号时,p[j-2]为当前要匹配的字符,成功匹配有两种情况。第一种情况是s[i-1]在p[j-2]前就匹配好了,由于星号可以匹配零个字符,所以本次只要匹配零个字符,不做变化即可;第二种情况是s[i-2]与p[j-1]可以匹配,并且s[i]与当前要匹配的字符p[j-1]刚好匹配。这两种情况任取其一就可以,所以取或。

注意在计算前,s[0]处要进行初始化。由于x*可以匹配零串,所以如果p[1]为*,则f[0, 2] = true,同理如果p[1]为*的情况下p[3]也为*,则f[0, 4]=true,以此类推。

题解

#include<string.h>#include<iostream>using namespace std;class Solution {public:    bool isMatch(string s, string p)    {        int slen = s.size(), plen = p.size();        bool **m;        m = new bool*[slen+1];        for(int i=0;i<=slen;i++)        {            m[i] = new bool[plen+1];            for(int j=0;j<=plen;j++)                m[i][j] = 0;        }        m[0][0] = true;        for(int j=1;j<=plen;j++)        {            if(p[j-1]=='*')m[0][j] = m[0][j-2];        }        for(int i=1;i<=slen;i++)        {            for(int j=1;j<=plen;j++)            {                if(p[j-1] != '*')                    m[i][j] = m[i-1][j-1] && (p[j-1]=='.' || p[j-1]==s[i-1]);                else                        m[i][j] = m[i][j-2] || // match 0                    (m[i-1][j] && (p[j-2]=='.' || p[j-2]==s[i-1])); // match 1+            }        }        return m[slen][plen];    }};
0 0
原创粉丝点击