[LeetCode - 动态规划] 10. Regular Expression Matching

1 问题

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

2 分析

2.1 该问题的递归性

如果不考虑".""*", 检查两个字符串是否匹配的方法是逐位比较,如果这两个字符串有一个位置上的字符不相等,那么这两个字符串不匹配,如果两个字符串所有位置均对应相等,那么这两个字符串匹配。
接下来考虑存在".""*"的情况:

  • "."匹配任意一个字符,如果待比较的两个字符串某一位置上的字符是".", 则认为这两个位置上的字符相等,继续下一个位置的比较。
  • "*"可以匹配0个或者多个紧邻的前一个字符。即"*"的存在使该字符串变成了多个可能的字符串。因此,必须将"*"能够表示的所有情况与另外一个字符串相比较,如果存在一种匹配的情况,那么这两个字符串匹配,反之,这两个字符串不匹配。
    • "*"存在时,"*"和其紧邻的前一个字符(设为m)有如下三种可能的匹配情况:
    • 空字符串”“
    • 单一字符m
    • 多个字符mm...

在对"*"表示的所有可能情况,进行搜索、确定是否匹配的过程中,如果一种情况匹配失败,就要回溯,选择下一种可能情况来进行操作。因此,该问题具有递归性。

2.2 递归解法

递归,回溯解决该问题的伪代码如下:

isMatch(String s, String p):

  • base case:
    • if p == "": //对应情况:模式字符串匹配完毕
      • if s == "": return true
      • else: return false
    • if s == "": //对应情况:源字符串匹配完毕,模式字符串有剩余
      • if p的长度大于1,且p的第二个字符是*: return isMatch(s,p[2:end])
      • else: return false
  • 递归过程:
    • if p的长度大于1 且 p[1] == "*"://对应情况:str*的匹配
      • if s[0] == p[0] or p[0] == ".":
        • if isMatch(s[1:end],p[2:end]): return true//对应情况:将str*视为单个字符
        • if isMatch(s[1:end], p): return true; //对应情况:将str*视为多个字符
      • if isMatch(s,p[2, end]): return true;//对应情况:将str*视为空
      • return false
    • else if s[0] == p[0] or p[0] == ".":
      • return isMatch(s[1:end], p[1:end])
    • else: return false

匹配字符串str*至少会存在3种子问题,因此, 算法的时间复杂度与模式字符串中"*"的个数有关系,设其为 n ,则递归算法的时间复杂度是O(3n)

2.3 使用动态规划优化代码

  1. 2.1-2.2节分析得出该问题具有递归性,可以分解为多个子问题求解。下面证明:该问题的子问题是重叠的。
    "a*b*c为例:
    重叠子问题
    上图中虚线方框内的子问题在递归过程中被重复求解,因此子问题是重叠的。
  2. 该问题具有最优子结构,如果sp整个字符串匹配,那么它们任何一个对应子字符串匹配。

通过上述分析,可以得出结论: 动态规划可以用于优化代码。
dp[i][j]表示s[0:i],p[0,j]是否匹配。 dp[i][j]可以通过2.2节中分析的子问题关系逐步计算出来,计算方法为:

 If p.charAt(j) == s.charAt(i) :  dp[i][j] = dp[i-1][j-1];
 If p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1];
 If p.charAt(j) == '*':
     if p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2]  
     if p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':
                              dp[i][j] = dp[i-1][j]
                           or dp[i][j] = dp[i][j-1]
                           or dp[i][j] = dp[i][j-2]

经过动态规划优化后算法的时间复杂度为 O(mn) , 其中 m,n 分别表示s,p的长度。

3 代码

3.1 递归解法:

public class Solution {
    public boolean isMatch(String s, String p) {
        System.out.println("S: "+s + " P: " + p);
        //base case
        if(p.length() == 0){
            if(s.length() == 0){
                return true;
            }else{
                return false;
            }
        }

        if(s.length() == 0){
            if(p.length() > 1 && p.charAt(1) == '*'){
                return isMatch(s, p.substring(2));
            }else{
                System.out.println(s+" "+p);
                return false;
            }
        }
        //recursive procedure
        if(p.length() > 1 && p.charAt(1) == '*'){
            if( s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'){
                if(isMatch(s.substring(1), p.substring(2))){
                    return true;
                }
                if(isMatch(s.substring(1), p)){
                    return true;
                }
            }
            if(isMatch(s, p.substring(2))){
                return true;
            }
            return false;
        }else if(s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'){
            return isMatch(s.substring(1), p.substring(1));
        }else{
            return false;
        }
    }
}

3.2 dp解法

public class Solution {
    public boolean isMatch(String s, String p) {    
    if (s == null || p == null) {
        return false;
    }
    boolean[][] dp = new boolean[s.length()+1][p.length()+1];
    dp[0][0] = true;
    for (int i = 0; i < p.length(); i++) {
        if (p.charAt(i) == '*' && dp[0][i-1]) {
            dp[0][i+1] = true;
        }
    }
    for (int i = 0 ; i < s.length(); i++) {
        for (int j = 0; j < p.length(); j++) {
            if (p.charAt(j) == '.') {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == s.charAt(i)) {
                dp[i+1][j+1] = dp[i][j];
            }
            if (p.charAt(j) == '*') {
                if (p.charAt(j-1) != s.charAt(i) && p.charAt(j-1) != '.') {
                    dp[i+1][j+1] = dp[i+1][j-1];
                } else {
                    dp[i+1][j+1] = (dp[i+1][j] || dp[i][j+1] || dp[i+1][j-1]);
                }
            }
        }
    }
    return dp[s.length()][p.length()];
}

[1] dp解法的代码来源于 https://discuss.leetcode.com/topic/40371/easy-dp-java-solution-with-detailed-explanation

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值