检查两种模式是否相互匹配?

时间:2017-06-27 08:37:23

标签: regex algorithm

This Leetcode problem是关于如何尽可能有效地匹配模式字符串与文本字符串。模式字符串可以由字母,点和星组成,其中字母仅匹配自身,点匹配任何单个字符,并且星形匹配前一字符的任意数量的副本。例如,模式

ab*c.

将匹配aceabbbbcc。我知道使用动态编程解决这个原始问题是可能的。

我的问题是,是否可以看到两个模式是否相互匹配。例如,模式

bdbaa.*

可以匹配

bdb.*daa

是否有一个很好的算法来解决这种模式匹配问题?

5 个答案:

答案 0 :(得分:5)

这是一种在多项式时间内工作的方法。虽然它有点重量级,但可能有更高效的解决方案。

我认为第一个观察有助于重新解决问题。不要问这些模式是否与彼此匹配,让我们问这个等价的问题:

  

给定模式P1和P2,是否存在字符串w,其中P1和P2各自匹配w?

换句话说,我们不是试图让两个模式相互匹配,而是搜索每个模式匹配的字符串。

您可能已经注意到,您允许使用的各种模式是正则表达式的子集。这很有用,因为有一个非常精细的理论,说明你可以用正则表达式及其属性做些什么。因此,不要瞄准你原来的问题,让我们解决这个更普遍的问题:

  

给定两个正则表达式R1和R2,是否有一个字符串w,R1和R2都匹配?

解决这个更普遍的问题的原因是它使我们能够使用围绕正则表达式开发的理论。例如,在形式语言理论中,我们可以讨论正则表达式的语言,它是正则表达式匹配的所有字符串的集合。我们可以表示这个L(R)。如果有一个字符串由两个正则表达式R1和R2匹配,那么该字符串属于L(R1)和L(R2),所以我们的问题等同于

  

给定两个正则表达式R1和R2,L(R1)∩L(R2)中是否有字符串w?

到目前为止,我们所做的只是重新构建我们想要解决的问题。现在我们来解决它。

这里的关键步骤是可以将任何正则表达式转换为NFA(非确定性有限自动机),以便正则表达式匹配的每个字符串都被NFA接受,反之亦然。更好的是,得到的NFA可以在多项式时间内构造。因此,让我们首先为每个输入正则表达式构建NFA。

现在我们有了这些NFA,我们想回答这个问题:两个NFA都接受一个字符串吗?幸运的是,有一种快速回答这个问题的方法。在NFA上有一个称为产品构造的通用构造,给定两个NFA和N2,构造一个新的NFA N',它接受N1和N2接受的所有字符串,而不接受其他字符串。同样,这种结构在多项式时间内运行。

一旦我们拥有N',我们基本上完成了!我们所要做的就是通过N'状态进行广度优先或深度优先搜索,看看我们是否找到了接受状态。如果是这样,太好了!这意味着N'接受了一个字符串,这意味着N1和N2都接受了一个字符串,这意味着R1和R2都匹配了一个字符串,因此原始问题的答案是“是的!”相反,如果我们无法达到接受状态,那么答案就是“不,这是不可能的。”

我确信有一种方法可以通过在自动机N'上进行某种隐式BFS而无需实际构造它来隐式地完成所有这些操作,并且应该可以在类似时间O(n < SUP> 2 )。如果我有更多时间,我会重温这个答案并扩展如何做到这一点。

答案 1 :(得分:2)

我已经研究过DP的想法,并提出了上述问题的以下实现。如果有人发现任何测试用例失败,请随时编辑代码。在我身边,我尝试了几个测试用例并通过了所有测试用例,我将在下面提到它们。

请注意我已经扩展了用于解决regex模式与使用DP的字符串匹配的想法。要参考该想法,请参阅OP中提供的LeetCode链接并查看讨论部分。他们给出了regex匹配和字符串的解释。

我们的想法是创建一个动态的memoization表,其条目将遵循以下规则:

  1. 如果pattern1 [i] == pattern2 [j],dp [i] [j] = dp [i-1] [j-1]
  2. 如果pattern1 [i] ==&#39;。&#39;或pattern2 [j] ==&#39;。&#39;,然后dp [i] [j] = dp [i-1] [j-1]
  3. 诀窍在于:如果pattern1 [i] =&#39; *&#39;,那么如果存在dp [i-2] [j],那么 dp [i] [j] = dp [i-2] [j] || dp [i] [j-1]否则dp [i] [j] = dp [i] [j-1]。
  4. 如果pattern2 [j] ==&#39; *&#39;,那么如果pattern1 [i] == pattern2 [j-1],那么 dp [i] [j] = dp [i] [j-2] || DP [I-1] [j]的 否则dp [i] [j] = dp [i] [j-2]
  5. pattern1逐行排列,pattern2逐列排列。此外,请注意,此代码也适用于与任何给定字符串匹配的正常正则表达式模式。我已经通过在LeetCode上运行它来验证它,它通过了所有可用的测试用例!

    以下是上述逻辑的完整工作实现:

    boolean matchRegex(String pattern1, String pattern2){
        boolean dp[][] = new boolean[pattern1.length()+1][pattern2.length()+1];
        dp[0][0] = true;
                //fill up for the starting row
        for(int j=1;j<=pattern2.length();j++){
            if(pattern2.charAt(j-1) == '*')
                dp[0][j] = dp[0][j-2];
    
        }
                //fill up for the starting column
        for(int j=1;j<=pattern1.length();j++){
            if(pattern1.charAt(j-1) == '*')
                dp[j][0] = dp[j-2][0];
    
        }
    
                //fill for rest table
        for(int i=1;i<=pattern1.length();i++){
            for(int j=1;j<=pattern2.length();j++){
                            //if second character of pattern1 is *, it will be equal to 
                            //value in top row of current cell
                            if(pattern1.charAt(i-1) == '*'){
                                dp[i][j] = dp[i-2][j] || dp[i][j-1];
                            }
    
                            else if(pattern1.charAt(i-1)!='*' && pattern2.charAt(j-1)!='*' 
                                        && (pattern1.charAt(i-1) == pattern2.charAt(j-1) 
                                        || pattern1.charAt(i-1)=='.' || pattern2.charAt(j-1)=='.'))
                    dp[i][j] = dp[i-1][j-1];
                            else if(pattern2.charAt(j-1) == '*'){
                    boolean temp = false;
                    if(pattern2.charAt(j-2) == pattern1.charAt(i-1) 
                                                || pattern1.charAt(i-1)=='.' 
                                                || pattern1.charAt(i-1)=='*' 
                                                || pattern2.charAt(j-2)=='.') 
    
                        temp = dp[i-1][j];
                    dp[i][j] = dp[i][j-2] || temp;
    
                }
            }
        }
                //comment this portion if you don't want to see entire dp table
                for(int i=0;i<=pattern1.length();i++){
                    for(int j=0;j<=pattern2.length();j++)
                        System.out.print(dp[i][j]+" ");
                    System.out.println("");
                }
        return dp[pattern1.length()][pattern2.length()];
    }
    

    驱动程序方法:

    System.out.println(e.matchRegex("bdbaa.*", "bdb.*daa"));
    
    Input1: bdbaa.* and bdb.*daa
    Output1: true
    
    Input2: .*acd and .*bce
    Output2: false
    
    Input3: acd.* and .*bce
    Output3: true
    

    时间复杂度:O(mn)其中mn是给定的两个正则表达式模式的长度。同样是空间复杂性。

答案 2 :(得分:2)

您可以使用针对此Thompson NFA样式正则表达式子集量身定制的动态方法,仅实现.*

你可以使用动态编程(在Ruby中):

def is_match(s, p)
    return true if s==p 
    len_s, len_p=s.length, p.length
    dp=Array.new(len_s+1) { |row| [false] * (len_p+1) }
    dp[0][0]=true 
    (2..len_p).each { |j| dp[0][j]=dp[0][j-2] && p[j-1]=='*' }

    (1..len_s).each do |i|
        (1..len_p).each do |j|
           if p[j-1]=='*' 
               a=dp[i][j - 2]
               b=[s[i - 1], '.'].include?(p[j-2])
               c=dp[i - 1][j]
               dp[i][j]= a || (b && c)   
           else
               a=dp[i - 1][j - 1]
               b=['.', s[i - 1]].include?(p[j - 1])
               dp[i][j]=a && b
           end 
        end
    end
    dp[len_s][len_p]      
end    
# 139 ms on Leetcode

或递归:

def is_match(s,p,memo={["",""]=>true})
    if p=="" && s!="" then return false end
    if s=="" && p!="" then return p.scan(/.(.)/).uniq==[['*']] && p.length.even? end
    if memo[[s,p]]!=nil then return memo[[s,p]] end

    ch, exp, prev=s[-1],p[-1], p.length<2 ? 0 : p[-2]
    a=(exp=='*' && (
           ([ch,'.'].include?(prev) && is_match(s[0...-1], p, memo) || 
                                       is_match(s, p[0...-2], memo))))
    b=([ch,'.'].include?(exp) && is_match(s[0...-1], p[0...-1], memo))
    memo[[s,p]]=(a || b)
end
# 92 ms on Leetcode

在每种情况下:

  1. 字符串和模式中的操作起点位于第二个字符,寻找*,并且只要sp中的字符匹配,就匹配一个字符*
  2. 元字符.用作实际字符的填充。这允许s中的任何字符与.
  3. 中的p匹配

答案 3 :(得分:1)

你也可以通过回溯来解决这个问题,而不是非常有效(因为可以重复计算相同子串的匹配,这可以通过引入一个查找表来改进,其中所有不匹配的字符串对都被保存并且计算只有当它们在查找表中找不到时才会发生,但似乎有效(js,算法假定简单的正则表达式是有效的,这意味着不以*开头,没有两个相邻的* [try it yourself]):

function canBeEmpty(s) {
    if (s.length % 2 == 1)
        return false;
    for (let i = 1; i < s.length; i += 2)
        if (s[i] != "*")
            return false;
    return true;
}

function match(a, b) {
    if (a.length == 0 || b.length == 0)
        return canBeEmpty(a) && canBeEmpty(b);
    let x = 0, y = 0;
    // process characters up to the next star
    while ((x + 1 == a.length || a[x + 1] != "*") &&
           (y + 1 == b.length || b[y + 1] != "*")) {
        if (a[x] != b[y] && a[x] != "." && b[y] != ".")
            return false;
        x++; y++;
        if (x == a.length || y == b.length)
            return canBeEmpty(a.substr(x)) && canBeEmpty(b.substr(y));
    }
    if (x + 1 < a.length && y + 1 < b.length && a[x + 1] == "*" && b[y + 1] == "*")
        // star coming in both strings
        return match(a.substr(x + 2), b.substr(y)) || // try skip in a
               match(a.substr(x), b.substr(y + 2)); // try skip in b
    else if (x + 1 < a.length && a[x + 1] == "*") // star coming in a, but not in b
        return match(a.substr(x + 2), b.substr(y)) || // try skip * in a
               ((a[x] == "." || b[y] == "." || a[x] == b[y]) && // if chars matching
                      match(a.substr(x), b.substr(y + 1))); // try skip char in b
    else // star coming in b, but not in a
        return match(a.substr(x), b.substr(y + 2)) || // try skip * in b
               ((a[x] == "." || b[y] == "." || a[x] == b[y]) && // if chars matching
                      match(a.substr(x + 1), b.substr(y))); // try skip char in a
}

为了进行一些优化,您可以首先规范化字符串:

function normalize(s) {
    while (/([^*])\*\1([^*]|$)/.test(s) || /([^*])\*\1\*/.test(s)) {
        s = s.replace(/([^*])\*\1([^*]|$)/, "$1$1*$2"); // move stars right
        s = s.replace(/([^*])\*\1\*/, "$1*"); // reduce
    }
    return s;
}
// example: normalize("aa*aa*aa*bb*b*cc*cd*dd") => "aaaa*bb*ccc*ddd*"

可以进一步减少输入:x*.*.*x*都可以替换为.*,因此为了获得最大的减少,您必须尝试移动尽可能多的.*旁边的星星(因此向左移动一些星星比向右移动所有星星更好)。

答案 4 :(得分:-2)

IIUC,你问:“正则表达式模式可以与另一种正则表达式模式相匹配吗?”

是的,它可以。具体而言,.匹配“任何字符”,当然包括.*。所以,如果你有一个像这样的字符串

bdbaa.*

你怎么能匹配它?好吧,你可以像这样匹配它:

bdbaa..

或者像这样:

b.*

或者喜欢:

.*ba*.*