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

来源:互联网 发布:朱梓骁郭敬明 知乎 编辑:程序博客网 时间:2024/06/06 01:48

题目

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") → falseisMatch("aa","aa") → trueisMatch("aaa","aa") → falseisMatch("aa", "a*") → trueisMatch("aa", ".*") → trueisMatch("ab", ".*") → trueisMatch("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
原创粉丝点击