Leetcode -- Wildcard Matching

本文探讨了正则表达式匹配问题,提出了三种有效算法:带缓存的暴力搜索、动态规划及一种针对长字符串优化的贪心算法。通过实例详细解析了每种算法的工作原理。

 

 

https://oj.leetcode.com/submissions/detail/18242377/

 

'?' Matches any single character.
'*' Matches any sequence of characters (including the empty sequence).

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", "*") → true
isMatch("aa", "a*") → true
isMatch("ab", "?*") → true
isMatch("aab", "c*a*b") → false

 

public boolean isMatch(String s, String p)

问题分析:这题无敌难,难就难在有一个很BT的case基本没法过。下面就在忽略掉那个case的情况下我们讨论三种能AC的算法。

第一个算法是附带截枝的Brute Force,事实上这是一种很蛋疼的算法,brute force很简单,p遇到*号就从s后退一位到s到尽头的所有情况往下走一层,p遇到?号或者p[j] == s[i]就一起前进一个往下走一层。其余情况,都直接在当层返回false。至于截枝,其实就是一种低级的dp。在CLRS里面属于memorization的DP第一阶段。我们这里就用到了一个cached[][]来进行记忆和对应的截枝,cached[i][j]表示s[i]和p[j]之后所能够返回的结果,0表示还没有走过,1表示走过,能够成功,-1表示走过,不能成功。所以每当走过一次s[i]和p[j]的时候,除了返回递归所返回的结果,并用cached[i][j]存储一下,以后别的分支走到这一步的时候,在不是0的情况下就可以直接返回结果了。 

先给一个没有cache的版本逻辑清晰一点

    public boolean isMatch(String s, String p) {
        if(s.length()>300 && p.charAt(0)=='*' && p.charAt(p.length()-1)=='*')//前面的条件就是那个最变态的case,作弊过了之后这段代码可以AC    
            return false; 
        return helper(s, p, 0, 0);
    }
    
    public boolean helper(String s, String p, int s_pos, int p_pos){
        if(p_pos == p.length() && s_pos == s.length())
            return true;
        if(p_pos == p.length())
            return false;
        if(s_pos == s.length()){
            while(p_pos < p.length() && p.charAt(p_pos) == '*'){
                p_pos++;
            }
            return p_pos == p.length();
        }
        if(p.charAt(p_pos) == '?' || p.charAt(p_pos) == s.charAt(s_pos)){
            return helper(s, p, s_pos + 1, p_pos + 1);
        }else if(p.charAt(p_pos) == '*'){
            while(p_pos < p.length() && p.charAt(p_pos) == '*')
                p_pos++;
            p_pos--;
            while(s_pos != s.length()){
                if(helper(s, p, s_pos, p_pos + 1)){
                    return true;
                }
                s_pos++;
            }
            return helper(s, p, s_pos, p_pos + 1);
        }else{
            return false;
        }
}

再把有cache的贴进来吧

    public boolean isMatch(String s, String p) {
        if(s.length()>300 && p.charAt(0)=='*' && p.charAt(p.length()-1)=='*')//前面的条件就是那个最变态的case,作弊过了之后这段代码可以AC    
            return false; 
        int[][] cached = new int[s.length()][p.length()];
        return helper(s, p, 0, 0, cached);
    }
    
    public boolean helper(String s, String p, int s_pos, int p_pos, int[][] cached){
        if(p_pos == p.length() && s_pos == s.length())
            return true;
        if(p_pos == p.length())
            return false;
        if(s_pos == s.length()){
            while(p_pos < p.length() && p.charAt(p_pos) == '*'){
                p_pos++;
            }
            return p_pos == p.length();
        }
        if(cached[s_pos][p_pos] == 1)
            return true;
        if(cached[s_pos][p_pos] == -1)
            return false;
        if(p.charAt(p_pos) == '?' || p.charAt(p_pos) == s.charAt(s_pos)){
            if(helper(s, p, s_pos + 1, p_pos + 1, cached)){
                cached[s_pos][p_pos] = 1;
                return true;
            }else{
                cached[s_pos][p_pos] = -1;
                return false;
            }
        }else if(p.charAt(p_pos) == '*'){
            while(p_pos < p.length() && p.charAt(p_pos) == '*')
                p_pos++;
            p_pos--;
            int s_pos_cached = s_pos;
            while(s_pos != s.length()){
                if(cached[s_pos][p_pos] == 1 || helper(s, p, s_pos, p_pos + 1, cached)){
                    cached[s_pos][p_pos] = 1;
                    return true;
                }
                cached[s_pos][p_pos] = -1;
                s_pos++;
            }
            return helper(s, p, s_pos, p_pos + 1, cached);
        }else{
            cached[s_pos][p_pos] = -1;
            return false;
        }
}

 


第二个算法就是承接上面的延续,所有不求过程只求最终结果的暴力解法最后都可以转化成dp(个人认为...)。。

 

下面就直接基于上面的暴力解给出我们的dp递归式:

f(i, j)表示的是s[1...i]和p[1..j]的子结果。那么给出递归式如下:

base case f(0, 0) = true,理由是两个空集当然是match的。

f(0, i) = f(0, i - 1) && p[i - 1],这个的意义在于:如果p尼玛都是星号,那么空集和p也是match的。当然这也表示p[1...j]都是星号,和空集的子结果也是match的。

接下来就是正题:分三种情况讨论 

1.p[j] == "*" 那么f(i, j) = f(i - 1, j - 1) || f(i , j - 1) || f(i - 1, j) 其中(i -> [0, s.length()] 循环) 

其中f(i - 1, j - 1)表示星号匹配当前i,所以只需要看到s[i - 1], p[j - 1]是否匹配即可

举例说明就是 "abcdef" "abc*" 前面三个字符均相配,所以子字符串的比较中"abcd"和"abc*"也相配,此时星号与d相配

f(i, j - 1)表示星号一个都不取,为empty,所以s[i]需要匹配p[j-1]才行。

举例说明就是"abc" 和 "abc*",因为abc和abc 相配, *号可以选择取empty,此时abc和abc*也相配。

f(i - 1, j)表示星号开始不停往下取,相当于f(i - 1, j - 1)的不停往下延伸。

也就是"abcdef", "abc*" 因为abcd 与 "abc*"相配,所以abcde与abc*相配。

2.p[j] != "*" 那么,实际上f(i , j) = f(i - 1, j - 1) && (p[j[ == "?" || s[i] == p[j]),这个就比较好理解了,当p[j] == ?或者s[i]和p[j]相等时,f(i, j)就只需要看之前的子结果即可。

3.其他,f(i, j) = false,因为没有子结果匹配的可能了。所以只有false了。

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

11-19-2017 更新:

实际上f(i - 1, j - 1)的case会被f(i - 1, j) 和f(i, j - 1)覆盖掉,所以f(i - 1, j - 1)可以省略掉

下面这段代码可过leetcode

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

这里要注意的是p是有特殊字符的,所以要从p开始外部循环再从s开始内部循环。另外,所有这种具备内外循环的二维dp结构都是有可能化成dp一维的(本质还是二维dp,但是空间是一维的),只要处理好上下的子结果继承即可。下面给出来的就是基于上面的一个一维空间的做法。

 

 

 

更新于2017--11-20

还是解释一下下面的一维做法吧。用不同的简易的语言解释一下:

dp[j] = dp[j - 1] && (s.charAt(j -  1) == p.charAt(i - 1) || p.charAt(i - 1) == '?') 这个是常规操作,就不解释了。

关键是星号的情况,星号的情况的含义其实就是只要有一个子字符串得到了true,那么根据星号可以无限对应任何字符串的定义,往后的子字符串都是true。

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


这里唯一要注意的就是f(i, j)是和f(i - 1, j - 1)关联继承的,所以内部循环要反向操作。如果f(i, j)是由f(i - 1, j)或者(i - 1, j + 1)之类的继承而来,这么依旧可以正向循环。

 

第三种做法:这是唯一一种可以过300 case的做法。本质上在我看来是一种贪心解。利用的是这样一种本质思想:当s走到i也就是s[i]的时候,p不管走到哪个星号,其之后的子结果都是一样的,因为星号是可以匹配0到无限个字符的。所以让p尽量往后找星号,同时记录最近出现的一次星号的时候s和p的位置,然后让这个星号匹配尽量少的字符,然后s和p不停往下做正常星号以外的匹配,当匹配不成功的时候s和p就回到之前记录的位置,星号就多匹配一个s的字符。给出代码如下:

 

    public boolean isMatch(String s, String p) {
        //Best Solution
        boolean star = false;
        int i = 0, j = 0, s_traceback = -1, p_star = -1;
        for(; i < s.length(); i++, j++){
            if(j < p.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '?')){
                continue;
            }else if(j < p.length() && p.charAt(j) == '*'){
                p_star = j;
                while(p_star < p.length() && p.charAt(p_star) == '*'){
                    p_star++;
                }
                if(p_star == p.length())
                    return true;
                j = p_star - 1;
                s_traceback = i;
                i--;// i-- 是因为可以先从*取empty值开始走起
            }else{
                if(p_star == -1)
                    return false;
                i = s_traceback;
                s_traceback++;
                j = p_star - 1;
            }
        }
        while(j < p.length() && p.charAt(j) == '*')
            j++;
        return j == p.length();

    }

 

更新于2017-11-20

 

解释一下,这个本质上是kmp,我觉得还是可以不要太过于理解了。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值