[Leetcode] 10. Regular Expression Matching 解题报告

题目

Implement regular expression matching with support for '.' and '*'.

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

思路

         这是一道相当难的题目,可以体现动态规划和回溯法的精髓。其中最难的部分是对‘*’的处理。

1、动态规划:定义dp[i][j]表示原字符串s的前i个字符和模式字符串p的前j个字符是否可以匹配。则递推式可以分为如下三种情况:

        1)p[j] == '.' || p[j] == s[i]:此时dp[i+1][j+1] = dp[i][j],因为s和p的最后一个字符是匹配的。

        2)p[j] == '*':此时p[j-1]可以选择在模式匹配中出现任意次,可以分为两种情况处理:

              a)无论p[j-1]和s[i]是否匹配,p[j-1]都可以选择在模式匹配时出现0次。此时一旦dp[i+1][j-1]  == true,则dp[i+1][j+1]为true。

              b)如果p[j-1]和s[i]匹配,那么此时p[j-1]还可以选择在模式匹配时出现至少1次,可以分为两种情况:

                    情况1:p[j-1] 在模式匹配时出现1次,此时dp[i+1][j+1] = dp[i+1][j];

                    情况2:p[j-1]在模式匹配时出现多于1次,此时由于p[j-1]和s[i-1]至少匹配1次,故有dp[i+1][j+1] = dp[i][j+1]。

       需要注意空串和空串能够匹配,即dp[0][0] = true,并基于该基础递推地更新dp[0][j] (0 < j <= n)。

       由以上分析可知,dp[i+1][j+1]的计算依赖于dp[i][j], dp[i+1][j], dp[i][j+1], dp[i+1][j-1],因此i和j都可以按照递增次序计算,时间复杂度为O(m*n),其中m和n分别是s和p的长度。注意到dp[i+1][j+1]的计算仅仅依赖于其上一行的dp[i][j]和dp[i][j+1],以及本行的dp[i+1][j-1]和dp[i+1][j],所以我感觉理论上来讲可以将空间复杂度进一步优化到O(n),但是我目前实现的算法还是有问题,随后有空再debug(欢迎有兴趣的同学留言讨论)。目前AC代码中的空间复杂度依然为O(n*m)。


2、回溯法:其基本思路和动态规划基本一致,但是采用递归来实现。通过观察代码可知,在p[j] == '*'的时候,代码存在回溯的情况,因此一些中间状态有可能被重复计算。可是实际情况是在该测试用例中,回溯法竟然比动态规划要快很多!作者推测可能对于本题而言,重复计算所增加的代价要小于冗余计算所增加的代价(在该题的动态规划中,由于dp[i+1][[j+1]的状态来源不一,具体取决于s[i]和p[j]是否匹配以及p[j]是否为‘*’,所以动态规划中某些中间状态的计算是不必要的)。

        为了进一步比较回溯法和动态规划法的性能,作者在回溯法的基础上记录状态(回溯法+记忆),以免重复计算中间状态。结果惊奇地发现,无论在回溯法中是否增加记忆,在Leetcode上的运行时间都是6ms!但是作者认为至少可以证明回溯法+记忆的策略要明显好于动态规划,因为在相同的空间复杂度下,前者的运行时间大大减少,说明回溯法+记忆确实有效避免了动态规划中不必要的中间状态计算。进一步验证发现,采用单纯回溯法,中间状态确实有被重复计算的情况,不过重复次数并不过多。所以结论是:是否增加记忆就是一个空间和时间之间的tradeoff了。


代码

1、动态规划:

class Solution {
public:
    bool isMatch(string s, string p) 
    {
        if(s.length() == 0 && p.length() == 0)
            return true;
        vector<vector<bool>> dp(s.length() + 1,  vector<bool>(p.length() + 1, false));
        dp[0][0] = true;
        for(int j = 0; j < p.length(); ++j)     // for the case "c*"
            dp[0][j + 1] = (p[j] == '*' && dp[0][j - 1]);
        for(int i = 0; i < s.length(); ++i)
        {
            for(int j = 0; j < p.length(); ++j)
            {
                if(p[j] == '.' || p[j] == s[i])
                {
                    dp[i + 1][j + 1] = dp[i][j];
                }
                else if(p[j] == '*')
                {
                    if(dp[i + 1][j - 1])                    // match 0 time is OK
                    {
                        dp[i + 1][j + 1] = true;
                        continue;
                    }
                    if(p[j-1] == s[i] || p[j-1] == '.')     // can match at least 1 time
                    {
                        // dp[i+1][j] means match 1 time, dp[i][j+1] means match more than 1 time
                        dp[i + 1][j + 1] = (dp[i + 1][j] || dp[i][j + 1]);
                    }
                }
            }
        }
        return dp[s.length()][p.length()];
    }
};

2、回溯法(无状态记忆):

class Solution {
public:
    bool isMatch(string s, string p) 
    {
        int m = s.length(), n = p.length();
        return backtracking(s, m, p, n);
    }
private:
    bool backtracking(string &s, int i, string &p, int j)
    {
        // base case
        if(i == 0 && j == 0)    return true;
        if(i != 0 && j == 0)    return false;
        
        // deduction case
        if(i == 0 && j != 0)    // s is finished, but p is not finished
        {
            if(p[j - 1] == '*') // only p == "c*c*c*" pattern can match null string
                return backtracking(s, i, p, j-2);
            return false;
        }
        if(s[i-1] == p[j-1] || p[j-1] == '.')
        {
            return backtracking(s, i-1, p, j-1);
        }
        else if(p[j-1] == '*')
        {
            if(backtracking(s, i, p, j-2))              // p[j-2]* matches zero characters of s
                return true;
            if(p[j-2] == s[i-1] || p[j-2] == '.')       // p[j-2]* matches at least one time
                return backtracking(s, i-1, p, j);
            else
                return false;
        }
        else
        {
            return false;
        }
    }
};

3、回溯法(有状态记忆):

class Solution {
public:
    bool isMatch(string s, string p) 
    {
        int m = s.length(), n = p.length();
        states.resize(m + 1, vector<int>(n + 1, -1));
        return backtracking(s, m, p, n) == 1;
    }
private:
    bool backtracking(string &s, int i, string &p, int j)
    {
        if(states[i][j] >= 0)
            return states[i][j];
            
        // base case
        if(i == 0 && j == 0)        return states[i][j] = 1;
        else if(i != 0 && j == 0)   return states[i][j] = 0;
        
        // deduction case
        if(i == 0 && j != 0)    // s is finished, but p is not finished
        {
            if(p[j - 1] == '*') // only p == "c*c*c*" pattern can match null string
                return states[i][j] = backtracking(s, i, p, j-2);
            return states[i][j] = 0;
        }
        if(s[i-1] == p[j-1] || p[j-1] == '.')
        {
            return states[i][j] = backtracking(s, i-1, p, j-1);
        }
        else if(p[j-1] == '*')
        {
            if(backtracking(s, i, p, j-2))          // p[j-2]* matches zero characters of s
                return states[i][j] = 1;
            if(p[j-2] == s[i-1] || p[j-2] == '.')   // p[j-2]* matches at least one time
                return states[i][j] = backtracking(s, i-1, p, j);
            else
                return states[i][j] = 0;
        }
        else
        {
            return states[i][j] = false;
        }
    }
    vector<vector<int>> states;                     // we may also use hash map instead
};


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值