【问题标题】:Checking if two patterns match one another?检查两个模式是否相互匹配?
【发布时间】:2017-11-30 05:40:27
【问题描述】:

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

ab*c.

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

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

bdbaa.*

可以匹配

bdb.*daa

有没有很好的算法来解决这个模式匹配问题?

【问题讨论】:

  • 您能否提供更多详细信息,或重新表述我无法真正理解的问题。你问是否可以将模式与模式而不是模式与字符串匹配?
  • @Keloo 是的,我就是这个意思
  • 关于带通配符的文件名的类似问题:stackoverflow.com/questions/34009784/…
  • @codecrazer:我想出了一种算法。你能提供一些你认为可能是角落的测试用例吗?在测试它们后,我也会发布我的答案,并将这些测试结果也包含在我的答案中。请告诉我。
  • 你的意思是这两个正则表达式匹配同一组字符串,还是有一个字符串同时匹配?

标签: regex algorithm


【解决方案1】:

这是一种适用于多项式时间的方法。它有点重量级,但可能有更有效的解决方案。

我认为在这里有帮助的第一个观察是重新定义问题。与其问这些模式是否匹配彼此,不如问这个等价的问题:

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

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

您可能已经注意到,您可以使用的各种模式是正则表达式的子集。这很有帮助,因为对于正则表达式及其属性可以做什么有一个非常详尽的理论。因此,与其针对您最初的问题,不如解决这个更普遍的问题:

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

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

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

到目前为止,我们所做的只是重新定义我们想要解决的问题。现在让我们去解决它。

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

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

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

我确信有一种方法可以通过在自动机 N' 上执行某种隐式 BFS 而不实际构造它来隐式地完成所有这些工作,并且应该可以在时间 O(n2)。如果我有更多时间,我会重新审视这个答案并详细说明如何做到这一点。

【讨论】:

  • 你的回答真的很好,但我需要实施,所以我不赏金你的解决方案。我赞成您的解决方案,并非常感谢您对如何解决此问题的想法。
【解决方案2】:

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

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

这个想法是创建一个动态的记忆表,其条目将遵循以下规则:

  1. 如果 pattern1[i] == pattern2[j],dp[i][j] = dp[i-1][j-1]
  2. 如果 pattern1[i] == '.'或 pattern2[j] == '.',然后 dp[i][j] = dp[i-1][j-1]
  3. 诀窍就在这里:如果 pattern1[i] = '*',那么如果 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] == '*',那么如果 pattern1[i] == pattern2[j-1],那么 dp[i][j] = dp[i][j-2] || dp[i-1][j] 否则 dp[i][j] = dp[i][j-2]

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 是给定的两个正则表达式模式的长度。空间复杂度也是如此。

【讨论】:

  • 你能详细说明这是从哪里来的吗?直觉是什么?
  • @templatetypedef:当然。每当我们遇到. 时,我们都需要将它与任何字符匹配。这意味着我们可以在dp 表中找到值排除当前正在比较的字符。当我们遇到* 时,我们可以没有找到前一个字符,或者我们可以有不止一个前一个字符出现。在这种情况下,如果找到字符,我们需要在当前表的同一行中查看 2 步。
  • @templatetypedef:继续前面的注释,如果我们遇到多次出现的前一个元素,这意味着我们需要在其中一个模式中的当前字符和前一个字符之间进行匹配其他模式。如果匹配,我们取当前单元格的单元格值的顶部。然后取到目前为止找到的两个值的||。更多细节请参考同一个 LeetCode 问题的讨论部分。他们为正则表达式和字符串匹配提供了它。我只是为这两种正则表达式模式扩展了相同的想法。
  • @templatetypedef:我希望我清楚这一点?如果您对此解决方案有任何疑问,请告诉我。
  • 那些反对接受的答案的人,至少有礼貌地给出反对的理由。没有任何理由这样做真的很不成熟。
【解决方案3】:

您可以使用针对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. 字符串和模式中的有效起始点是查找* 的第二个字符,只要s 匹配p* 之前的字符,就会匹配一个字符
  2. 元字符. 被用作实际字符的填充。这允许s 中的任何字符匹配p 中的.

【讨论】:

    【解决方案4】:

    你也可以通过回溯来解决这个问题,但效率不是很高(因为相同子字符串的匹配可能会被重新计算多次,这可以通过引入一个查找表来改进,其中保存了所有不匹配的字符串对并计算只有在查找表中找不到它们时才会发生),但似乎可以工作(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* 都可以替换为 .*,因此要获得最大的减少,您必须尝试在 @ 旁边移动尽可能多的星星987654327@(因此将一些星星移到左边可能比全部移到右边更好)。

    【讨论】:

      【解决方案5】:

      IIUC,您在问:“一个正则表达式模式可以匹配另一个正则表达式模式吗?”

      是的,可以。具体来说,. 匹配“任何字符”,当然包括.*。所以如果你有这样的字符串

      bdbaa.*
      

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

      bdbaa..
      

      或者像这样:

      b.*
      

      或者喜欢:

      .*ba*.*
      

      【讨论】:

      • 我发现我对我的想法做出了错误的解释,我知道一个模式可以与另一个模式匹配,但我想问的是我们是否能够确定一个模式可以匹配另一种模式。
      猜你喜欢
      • 1970-01-01
      • 2011-03-14
      • 2013-09-23
      • 2012-09-17
      • 1970-01-01
      • 1970-01-01
      • 2010-12-19
      • 1970-01-01
      • 2012-01-05
      相关资源
      最近更新 更多