【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、判斷什麼的。以及減少空間複雜度的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章