1. 程式人生 > 其它 >Leetcode第10題 正則表示式匹配 C++動態規劃解法

Leetcode第10題 正則表示式匹配 C++動態規劃解法

技術標籤:# leetcodeg回顧動態規劃leetcode動態規劃字串正則表示式

這道題難為了我快一天了。看了官方的解法,不得的說,他的解法既巧妙,又複雜。實際上這道題的第五個條件很重要。
5、保證每次出現字元 * 時,前面都匹配到有效的字元。
也就是說不可能出現p字串第一個字元為 * 的情況。
得出動態轉移方程也並不難,但是這個方程裡面是有幾個坑的。而官方解法是採用了matches去避免。
這裡我們就直接上圖說明。
在這裡插入圖片描述
如果p[i]=* , f[i][j]=f[i-1][j] or f[i][j-2] 。這一步大家都能理解,但是坑也就在這裡。
如果s=“c”,p=“a*c”。這種情況如何呢?

很顯然,當匹配進行到p的字元c時,f[i=1][j=1]=f[i-1][j-1]。問題來了i-1行的時候,s是沒有字串的。是的,我們一般開闢的空間是m+1,n+1。所以第0行是我們空出來的。
如果我們正常的初始化,是對f[0][0]=1進行了初始化,但是外層迴圈卻是從i=0開始,實際上,這裡就出現了f[0][2]=1的情況。
但問題又來了,如 果我們是從0行開始的迴圈,我們對p[i]!= * 的時候,是要進行s[i-1]==p[j-1] || p[j-1] ==’.'的判斷的,現在你讓i從0開始,那不是會越界嗎?所以官方的這個matches及其巧妙。
當然也要配上迴圈使用,我們可以看到,在第0行的時候,如果p的偶數位字元是 * ,那麼將會非常神奇的出現了f[0][2]=f[0][4]…f[0][2n]=1的現象,而因為matches的限制下面的語句,但它的值已經是1了。而對於或者奇數位為 * 的情況則不會發生(你不必擔心第一位是 * ,因為在前提條件5裡面,已經規避了這一情況),,因為只有f[0][0]=1。而對於不為 * 的情況,第0行會直接遮蔽掉下面f[i][j] |= f[i - 1][j - 1]的運算,也不會發生越界的現象。
所以如果沒有這個官方回答的精髓也就在於此,只有一個狀態轉移方程是無法實現這種操作的。

auto matches = [&](int i, int j) {
    if (i == 0) {
        return false;
    }
    if (p[j - 1] == '.') {
        return true;
    }
    return s[i - 1] == p[j - 1];
};

    if (p[j - 1] == '*') {
        f[i][j] |= f[i][j - 2];
        if (matches(i,
j - 1)) { f[i][j] |= f[i - 1][j]; } } else { if (matches(i, j)) { f[i][j] |= f[i - 1][j - 1]; }

如果不用matches呢?當然你就要在某些句子裡附加一個i>0的判斷。我的寫法如下。所以我個人認為這是極蠢的做法。增加了複雜度。

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

而當我看到了下面一種解法的時候,我覺得實在是太妙了!
它在s和p前面加了一個空格,實際上加任何相同字元都可以。這樣就巧妙的解決了s=“c”,p="a*c"出現的越界問題。
而且也解決了前提5裡面的一個隱含條件的顯示,即p的第一個字元不能為 * ,這種解法下,即使p首字元為 * 也可以通過。

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

那麼有沒有不借用這種的操作呢?答案是有的,使用帶備忘錄的動態規劃即可。
這裡先空著不談。

以上是自底向上,當然也可以自頂向底來實現。但同樣需要考慮陣列越界的情況了。這可太男了~

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