【LeetCode】剑指DP:10. Regular Expression Matching & 44. Wildcard Matching 正则表达式匹配&通配符匹配

一、概述

输入两个字符串:s和p,判断s能否匹配p。

这俩题要求差不多,44题最开始我没用DP,一点一点循环匹配,结果最多过了1600+testcase,剩下的过不去,因为循环匹配无法覆盖所有的边界条件。当时很难受,因为边界条件有很多,我一个一个测出来然后补救,最终发现有一类边界条件无法补救,给我这思路判了死刑。但是没法子。就搁置在一边。现在开始学DP,就把这道题拎出来又做了一遍。没有看Solution,直接自己分析做的。时空复杂度我自己都看乐了。太差了:

这是10题:

这是44题:

简直是我AC的题目里面时空复杂度最差的。但好歹是自己想出来了,还是写下来吧。

二、分析

1、44题我的思路

对这俩题来说,我的思路是类似的。因此以44题来讲述我的思路,10题的区别之后再说。

44题中,p中可以有三种元素:a~z,一个匹配一个;?,匹配一个任意的;*,匹配从空字符到任意长度的字符串,万能。

我之前有点DP的PTSD的,就是那种,一眼看不出用什么方法做的,或者是特别特别麻烦,很可能是字符串题,八成就是DP。一猜一个准。很无奈。

既然是DP,关键就是找递推关系式了。在这部分我先说一下我自己找到的关系式。差是很差,但是至少做出来了不是。

当时我是这样想的:

看见s=“mississippi”,p=“m**is*p*”。于是就开始用我那点贫乏的DP经验:我得找一个符合条件的结果,然后在它基础上扩展。

好嘞。先看s的第一个字符,m;再看p的第一个字符,也是m。这不就是第一个符合条件的结果。

然后看s的第二个字符,i;再看p的第二个字符,*,万能字符。也匹配。这就是第二个符合条件的结果。开始猜想:递推公式是不是就是“长度为i的字符串符合条件,要求长度为i-1的字符串符合条件且第i个字符和p中的字符匹配”?不知道,接着往下看。

之后看s的第三个字符,s;再看p的第三个字符,*,万能字符。也匹配。好像对啊。不对!为什么不对,凭什么s的第三个字符要和p的第三个字符匹配啊,人家p的第二个字符为*,有排面,可以和任意长度的字符匹配,你只给它匹配一个字符,这不是屈才了。那按这个意思,*可以和之后所有字符匹配咯?是啊,所以说“m*”这个字符串,就可以匹配上面的输入了。

有点意思。但还是迷糊。接着往下看。

s的第四个字符,s,再看p的第四个,i。等会,p为啥要看第四个,你p第二个有排面,已经能把s的所有字符都匹配完了。那我啥时候才能看到p的第四个字符啊,它对s有限制啊。咱们看一看i能匹配s中的哪些字符吧:p的第四个是i,s的第二个、第五个、第八个、第十一个是i。那p的这个i在什么情况下能匹配上s的这几个i呢?这就是递推公式的核心问题。

从头开始捋,先看p的i能不能匹配s的第五个i。什么时候能匹配?“p的i之前的字符串能和s的前四个字符串匹配”。这时候才能轮到p的i匹配s的第五个i。那到底能不能匹配呢?“miss”和“m**”,肯定能。

然后看p的i能不能匹配s的第八个i。同样的,什么时候能匹配?“p的i之前的字符串能和s的前七个字符串匹配”。这时候才能轮到p的i匹配s的第八个i。那到底能不能匹配呢?“mississ”和“m**”,肯定能。有点眉目了。

第十一个也一样。能匹配。我们来总结一下,p的i能匹配到这几个位置的元素,都是因为p的i之前的字符串和这几个位置之前的字符串已经匹配了。由于*实在太厉害,能和很多字符匹配,所以i也能匹配好几个。

也就是说,我们把所有的i能匹配的s的子串找出来,在这些子串基础上再往后找,直到找到最后?

是这么个理。

有点模糊啊,我怎么知道这俩修饰词这么多的字符串匹不匹配啊。用DP表。这该如何记录?

DP表如何表达出“p的i之前的字符串和这几个位置之前的字符串已经匹配”呢?我们已经知道DP表是一个二维数组(不要问我为什么,我加一块就做了三道DP题,全是二维数组当DP表= =),这个元素有什么说道么?

先把s放在横坐标,那纵坐标也得有点东西啊,先把p的第一个字符放上吧,这m和s的第一个字符匹配啊,加个一标记它俩匹配不过分吧:

那纵坐标第二个我加个*,第三个我也加个*,*和m之后的所有元素都匹配,按上面的理论,这些我都得写个1?那就写呗。

注意由于*可以匹配空元素,因此第二个*也可以和i匹配。

p的第四个i按上面我们的分析写出来:

看不出什么来啊。。。那就再写一个,p的第五个,s。这个s得和哪个匹配呢?我们要看s之前的“m**i”和s的哪些字符串匹配。观察上图,“m**i”和“mi”、“missi”、“mississi”、“mississippi”都匹配。诶?看这几个字符串,不就是之前问题“那p的这个i在什么情况下能匹配上s的这几个i呢?”的答案么。那s能匹配哪些字符就可以知道了:

接下来把p的所有字符放在纵坐标:

当我们把所有的1填入,我们也就得到了递推公式:

DP[i][j]=(DP[i-1][j-1]==1)&&(p[i]==s[j])

成了。那如何判断s和p能不能匹配呢?我们来看一下p中所有能和s匹配的子串在DP中的特点:

m*,和s匹配;m**、m**i、m**i*、m**is*p*。所有的都在这里了。在想一下DP表中1的含义:DP[i][j]=1,表示p的第i个和s的第j个字符匹配,而且它们之前的子串也匹配。那好了,只要最后的DP[p.size][s.size]==1不就说明都匹配么。拿上面几个子串验证一下:嚯,最后果然都是1。这不就解出来了么。代码如下:

class Solution {
    int DP[1050][1050]={0};
public:
    bool isMatch(string s, string p) {
        if(s==""&&(p==""||p=="*"))
            return true;
        if(s==""&&p.size()>0)
            return false;
        if(s.size()>0&&p=="")
            return false;
        if(p[0]=='*')
            for(int j=0;j<=s.size();j++)
                DP[0][j]=1;
        else if(p[0]=='?'||p[0]==s[0])
            DP[0][1]=1;
        else
            return false;
        for(int i=1;i<p.size();++i)
        {
            for(int j=0;j<=s.size();++j)
            {
                if(p[i]=='*'&&DP[i-1][j]==1)
                {
                    while(j<=s.size())
                    {
                        DP[i][j]=1;
                        j++;
                    }
                    break;
                }
                else if(p[i]=='?')
                    DP[i][j+1]=(DP[i-1][j]==1);
                else
                    DP[i][j+1]=((DP[i-1][j]==1)&&(s[j]==p[i]));
            }
        }
        if(DP[p.size()-1][s.size()]==1)
            return true;
        return false;
    }
};

注意以下几点:

第一,递推的初始条件,也就是DP的第一行是要我们自己提前写出来的:分情况填入不同个数个1即可;

第二,DP[i-1][j-1]不好实现,对于j=0的情况不友好,因此我将递推条件改为

DP[i][j+1]=(DP[i-1][j]==1)&&(p[i]==s[j])了。对应的判断成功匹配也要重新改一下。

我这种时空复杂度都是O(mn),可以说是很差了。

2、10题我的思路

10题和44题不同的一点是,*代表它前面的元素会重复0~n次,.和44题中的?一样。这个*的不同可折磨死我了。最后我用了一种很笨的方法来转换:

首先,将两个及以上的*压缩为一个*。由于*的特性,你写个a**和a*没有区别。

其次,对于两个字符,如果后面的是*,那么就相当于把前面的字符“强化”,比如说“a*”,强化为“A”,A可以匹配任意长度的a。“.*”就牛逼了,和44题中的*一样,能匹配所有字符串,我们将其强化为“?”。

最后,用强化后的p去和s匹配。匹配算法类似44题。代码如下:

class Solution {
    int DP[1050][1050]={0};
public:
    bool isMatch(string s, string p) {
        if(p==".*")
            return true;
        if(s==""&&(p==""))
            return true;
        if(s.size()>0&&p=="")
            return false;
        string new_p="";
        int i=0;
        while(p[i]=='*')
            i++;
        for(;i<p.size();i++)
        {
            if(new_p.size()==0)
            {
                new_p+=p[i];
                continue;
            }
            if(p[i]=='*')
            {
                if(new_p[new_p.size()-1]=='*')
                    continue;
                else
                    new_p+=p[i];
            }
            else
                new_p+=p[i];
        }
        cout<<new_p<<'\n';
        string last_p="";
        for(int i=0;i<new_p.size()-1;i++)
        {
            if(new_p[i+1]!='*')
                last_p+=new_p[i];
            else
            {
                if(new_p[i]>='a'&&new_p[i]<='z')
                {
                    last_p+=new_p[i]-'a'+'A';
                    i++;
                }
                else if(new_p[i]=='.')
                {
                    last_p+='?';
                    i++;
                }
            }
        }
        if(new_p[new_p.size()-1]!='*')
            last_p+=new_p[new_p.size()-1];
        cout<<last_p<<'\n';
        if(last_p[0]=='.')
            DP[0][1]=1;
        else if(last_p[0]=='?')
        {
            for(int i=0;i<=s.size();i++)
                DP[0][i]=1;
        }
        else if(last_p[0]>='a'&&last_p[0]<='z')
        {
            if(last_p[0]==s[0])
                DP[0][1]=1;
            else
                return false;
        }
        else
        {
            DP[0][0]=1;
            for(int i=0;i<=s.size();i++)
            {
                if(last_p[0]-'A'+'a'==s[i])
                {
                    DP[0][i+1]=1;
                }
                else
                    break;
            }
        }
        for(int i=1;i<last_p.size();++i)
        {
            for(int j=0;j<=s.size();++j)
            {
                if(last_p[i]=='?'&&DP[i-1][j]==1)
                {
                    while(j<=s.size())
                    {
                        DP[i][j]=1;
                        j++;
                    }
                    break;
                }
                else if(last_p[i]=='.')
                    DP[i][j+1]=(DP[i-1][j]==1);
                else if(last_p[i]>='a'&&last_p[i]<='z')
                    DP[i][j+1]=((DP[i-1][j]==1)&&(s[j]==last_p[i]));
                else if((last_p[i]>='A'&&last_p[i]<='Z')&&(DP[i-1][j]==1))
                {
                    while(DP[i-1][j]==1)
                    {
                        DP[i][j]=1;
                        j++;
                    }
                    j--;
                    while(j<=s.size())
                    {
                        DP[i][j+1]=((s[j]==last_p[i]-'A'+'a'));
                        if(DP[i][j+1]==0)
                            break;
                        j++;
                    }
                }
            }
        }
        for(int i=0;i<last_p.size();i++)
        {
            for(int j=0;j<=s.size();j++)
                cout<<DP[i][j];
            cout<<'\n';
        }
            
        if(DP[last_p.size()-1][s.size()]==1)
            return true;
        return false;
    }
};

太丑陋了,真是太丑陋了,我这道题应该是迄今为止我自己error最多的了,我测出了不下十个测试样例。哭瞎。

最难的是A应该如何根据上文对DP进行赋值:由于A可以为0,所以事情很难办:我们以s=“mississippi”,p=“mis*is*p*.”为例:

我为了规避j=0的情况,将DP中为1的含义进行了改变:在之前,DP[i][j]==1表示s[j]==p[i],但我更改之后,表示s[j-1]==p[i]。这有点不直观,而且AB连续的时候会带来一些问题,混在一起我就有点晕。

对于上图,我们主要观察SP在一起的情况:从0开始计数,S的第5、6、7列为1,P的第5、6、7列也为1。这是为什么?先看S的,S[5]=1是因为上面的i[5]=1,因为S可以匹配0长度的字符串,所以s(输入的待匹配字符串)的第5个“s”,应该让S之后的P也有资格匹配。若P匹配“s”,说明S匹配“0长度字符串”。体现出来就是上一行的1全落到了下一行。落下来的1之后的元素怎么判断值呢?直接看s[j-1]是不是和p[i]相等啊。比如说我们看S和它上面的i,i的1落下来,因此S[5]=1;然后由于s[5]==p[5],所以S[6]=1;同理S[7]=1。S[8]=0因为S和i不匹配。

这里一定要弄清楚。否则代码会有无数的边界条件错误。

第一行的初始化也是一样。

哇这两道题修修补补,真恶心死我了。

3、44题、10题优秀的DP

思路和我的相同,因此我就把我自己的优化一下,有两处改动:

class Solution {
    bool DP[1050][1050];
public:
    bool isMatch(string s, string p) {
        if(!p.size()) return s.size() == 0;
        if(p[0]=='*')
            for(int j=0;j<=s.size();j++)
                DP[0][j]=true;
        else if(p[0]=='?'||p[0]==s[0])
            DP[0][1]=1;
        else
            return false;
        for(int i=1;i<p.size();++i)
        {
            DP[i][0]=p[i]=='*'&&DP[i-1][0];
            for(int j=1;j<=s.size();++j)
            {
                if(p[i]=='*')
                    DP[i][j]=(DP[i][j-1]||DP[i-1][j]);
                else
                    DP[i][j]=(DP[i-1][j-1]&&((p[i]=='?')||(s[j-1]==p[i])));
            }
        }
        if(DP[p.size()-1][s.size()])
            return true;
        return false;
    }
};

最重要的:DP用bool不用int,时间缩短五倍以上!!!!然后就是把if判断放在=的右边,直接判断对错。

同时我们观察到,其实DP[i+1]仅仅与DP[i]有关,因此DP表仅需要两行即可,用不到那么多行,这个我就懒得写了。

同理第十题,我们仅将DP由int换为bool,就可以同样获得五倍的提升:

这样看来,DP表一定要是bool才好了。

10题还有一种优秀的DP,不需要像我这样先对p做处理,如下:

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size(), n = p.size();
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        dp[0][0] = true;
        for (int i = 0; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (p[j - 1] == '*') {
                    dp[i][j] = dp[i][j - 2] || (i && dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
                } else {
                    dp[i][j] = i && dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
                }
            }
        }
        return dp[m][n];
    }
};

就是如果前一个是*那么就看前一个的前一个,即使这个是*也没关系。

4、44题优秀的指针法

代码如下:

bool IsWildcardMatch(string s, string p)
  {
    int slen = s.size(), plen = p.size(), i, j, iStar = -1, jStar = -1;

    for (i = 0, j = 0; i < slen; i++, j++)
    {
	if (j < plen && p[j] == '*')
	{ //meet a new '*', update traceback i/j info
		iStar = i;
		jStar = j;
		--i;
	}
	else
	{
		if (j >= plen || (p[j] != s[i] && p[j] != '?'))
		{  // mismatch happens
			if (iStar >= 0)
			{ // met a '*' before, then do traceback
				i = iStar++;
				j = jStar;
			}
			else return false; // otherwise fail
		}
	  }

  }
  while (j<plen && p[j] == '*') ++j;
  return j == plen;
}

具体思路就是将p以*作为分隔符分隔出多个子串,若是在s中找到了这些子串的对应,则p和s对应,举例如下:

missisibisp和m*ib*p:

m和m对应;之后遇到*,开始循环:先看ib和is,然后ib和ss、si、is、si、ib,ib和ib对应上了。为什么是ib?因为ib是由*分隔出的子串,找它的对应即可,然后看p和i、s、p,对应上了,退出循环。

又快又好的方法。

10题没法用这个方法。因为这个方法是基于*可以匹配无限的,所以才能用*来分隔子串,不用管*是否匹配成功,因为必定匹配成功。而10题中的*没有这么厉害,因此不能用。

5、10题和44题优秀的递归方法

10题的:

class Solution {
public:
    bool isMatch(string s, string p) {
        if (p.empty())    return s.empty();
        
        if ('*' == p[1])
            // x* matches empty string or at least one character: x* -> xx*
            // *s is to ensure s is non-empty
            return (isMatch(s, p.substr(2)) || !s.empty() && (s[0] == p[0] || '.' == p[0]) && isMatch(s.substr(1), p));
        else
            return !s.empty() && (s[0] == p[0] || '.' == p[0]) && isMatch(s.substr(1), p.substr(1));
    }
};

class Solution {
public:
    bool isMatch(string s, string p) {
        /**
         * f[i][j]: if s[0..i-1] matches p[0..j-1]
         * if p[j - 1] != '*'
         *      f[i][j] = f[i - 1][j - 1] && s[i - 1] == p[j - 1]
         * if p[j - 1] == '*', denote p[j - 2] with x
         *      f[i][j] is true iff any of the following is true
         *      1) "x*" repeats 0 time and matches empty: f[i][j - 2]
         *      2) "x*" repeats >= 1 times and matches "x*x": s[i - 1] == x && f[i - 1][j]
         * '.' matches any single character
         */
        int m = s.size(), n = p.size();
        vector<vector<bool>> f(m + 1, vector<bool>(n + 1, false));
        
        f[0][0] = true;
        for (int i = 1; i <= m; i++)
            f[i][0] = false;
        // p[0.., j - 3, j - 2, j - 1] matches empty iff p[j - 1] is '*' and p[0..j - 3] matches empty
        for (int j = 1; j <= n; j++)
            f[0][j] = j > 1 && '*' == p[j - 1] && f[0][j - 2];
        
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++)
                if (p[j - 1] != '*')
                    f[i][j] = f[i - 1][j - 1] && (s[i - 1] == p[j - 1] || '.' == p[j - 1]);
                else
                    // p[0] cannot be '*' so no need to check "j > 1" here
                    f[i][j] = f[i][j - 2] || (s[i - 1] == p[j - 2] || '.' == p[j - 2]) && f[i - 1][j];
        
        return f[m][n];
    }
};

44题:

class Solution {
private:
    bool helper(const string &s, const string &p, int si, int pi, int &recLevel)
    {
        int sSize = s.size(), pSize = p.size(), i, curLevel = recLevel;
        bool first=true;
        while(si<sSize && (p[pi]==s[si] || p[pi]=='?')) {++pi; ++si;} //match as many as possible
        if(pi == pSize) return si == sSize; // if p reaches the end, return
        if(p[pi]=='*')
        { // if a star is met
            while(p[++pi]=='*'); //skip all the following stars
            if(pi>=pSize) return true; // if the rest of p are all star, return true
            for(i=si; i<sSize; ++i)
            {   // then do recursion
                if(p[pi]!= '?' && p[pi]!=s[i]) continue;
                if(first) {++recLevel; first = false;}
                if(helper(s, p, i, pi, recLevel)) return true;
                if(recLevel>curLevel+1) return false; // if the currently processed star is not the last one, return
            }
        }
        return false;
    }
public:
    bool isMatch(string s, string p) {
        int recLevel = 0;
        return helper(s, p, 0, 0, recLevel);
    }
};

关键是剪枝。

三、总结

注意DP的写法。以及一些常见的技巧。bool、int、判断什么的。以及减少空间复杂度的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值