58

注意:这是一个关于现代正则表达式风味可能性的问题。这不是使用其他方法解决此问题的最佳方法。它的灵感来自较早的问题,但该问题不限于正则表达式。

问题

在 ASCII“图像”/艺术/地图/字符串中,例如:

....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....

我想找到一个简单的三个Xs 的垂直线形成:

X
X
X

图像中的行数是可变的,每行的宽度也是可变的。

问题

使用正则表达式(PCRE/PHP、Perl、.NET 或类似)可以:

  1. 确定是否存在这样的编队
  2. 计算这种编队的数量/匹配它们的起点(上例中为 4 个)
4

7 回答 7

39

回答问题 1

要回答第一个问题,可以使用:

(?xm)                    # ignore comments and whitespace, ^ matches beginning of line
^                        # beginning of line
(?:
    .                    # any character except \n
    (?=                  # lookahead
        .*+\n            # go to next line
        ( \1?+ . )       # add a character to the 1st capturing group
        .*+\n            # next line
        ( \2?+ . )       # add a character to the 2nd capturing group
    )
)*?                      # repeat as few times as needed
X .*+\n                  # X on the first line and advance to next line
\1?+                     # if 1st capturing group is defined, use it, consuming exactly the same number of characters as on the first line
X .*+\n                  # X on the 2nd line and advance to next line
\2?+                     # if 2st capturing group is defined, use it, consuming exactly the same number of characters as on the first line
X                        # X on the 3rd line

Online demo

此表达式适用于 Perl、PCRE、Java,并且应该适用于 .NET。

该表达式使用带有自引用捕获组的前瞻来为前瞻的每次重复添加一个字符(这用于“计数”)。

\1?+表示如果\1匹配(或已定义)消耗它,并且不归还它(不要回溯)。在这种情况下,它相当于(?(1) \1 ). \1这意味着如果\1定义了匹配。

polygenelubricants他对How can we match a^nb^n with Java regex?的回答中很好地解释了这种带有反向引用的前瞻性。. (他还写过关于 Java 正则表达式的其他令人印象深刻的技巧,涉及反向引用和环视。)

回答问题 2

素色搭配

当仅使用匹配并要求匹配数中的答案(计数)时,问题 2 的答案将是:

不能在具有有限后视功能的正则表达式中直接解决。而像 Java 和 .NET 等其他风格可以(例如在m.buettner 的 .NET 解决方案中)。

因此,在这种情况下,Perl 和 PCRE(PHP 等)中的普通正则表达式匹配不能直接回答这个问题。

(半?)证明

假设没有可用的可变长度后视。

您必须以某种方式计算X.
做到这一点的唯一方法是匹配它们,并且由于没有可变长度的lookbehinds可用,因此您必须(至少)在行首开始匹配。
如果您在一行的开头开始匹配,则每行最多只能获得一个匹配。

由于每行可能出现多次,因此不会全部计算在内,也不会给出正确答案。

长度/间接解

另一方面,如果我们接受答案作为匹配或替换结果的长度,那么第二个问题可以用PCRE 和 Perl(以及其他风格)来回答。

该解决方案基于m.buettner 的“部分 PCRE 解决方案” /受其启发。

可以简单地将以下表达式的所有匹配项替换为$3,得到问题二的答案(兴趣模式的数量)作为结果字符串的长度。

^
(?:
    (?:                   # match .+? characters
        .
        (?=               # counting the same number on the following two lines
            .*+\n
            ( \1?+ . )
            .*+\n
            ( \2?+ . )
        )
    )+?
    (?<= X )              # till the above consumes an X
    (?=                   # that matches the following conditions
        .*+\n
        \1?+
        (?<= X )
        .*+\n
        \2?+
        (?<= X )
    )
    (?=                   # count the number of matches
        .*+\n
        ( \3?+ . )        # the number of matches = length of $3
    )
)*                        # repeat as long as there are matches on this line
.*\n?                     # remove the rest of the line

在 Perl 中可以写成:

$in =~ s/regex/$3/gmx;
$count = length $in;

Online demo

此表达式类似于上面问题 1 的解决方案,但进行了一些修改以包含X在第一次前瞻中匹配的字符中,并用量词包裹并计算量词的匹配数。

除了直接匹配之外,它尽可能接近(除了正则表达式之外的额外代码),并且可能是问题 2 的可接受答案。

测试用例

上述解决方案的一些测试用例和结果。结果显示数字答案(结果字符串的长度),括号中是替换后的结果字符串。

Test #0:
--------------------
X
X
X

result: 1 (X)


Test #1:
--------------------
..X....
..X....
..X....

result: 1 (.)


Test #2:
--------------------
..X.X..
..X.X..
....X..

result: 1 (.)


Test #3:
--------------------
..X....
..X....
...X...

result: 0 ()


Test #4:
--------------------
..X....
...X...
..X....

result: 0 ()


Test #5:
--------------------
....X..
.X..X..
.X.....

result: 0 ()


Test #6:
--------------------
.X..X..
.X.X...
.X.X...

result: 1 (.)


Test #7:
--------------------
.X..X..
.X..X..
.X..X..

result: 2 (.X)


Test #8:
--------------------
XXX
XXX
XXX

result: 3 (XXX)


Test #9:
--------------------
X.X.X
XXXXX
XXXXX
.X.X.

result: 5 (XXXXX)


Test #10:
--------------------
1....X.......
2..X..X...X....
3X.X...X..X.....
4X....XXXXXX.....
5X..XXX...........
6.....X..........
7.........X....X
8..X......X....X....
9..X......X....X....X...
A....X.....
B.X..X..
C.....
XXX
XXX
XXX
.

result: 8 (3458.XXX)
于 2013-06-18T19:58:31.503 回答
28

编辑

以下解决方案有两个严重的问题:

  1. 由于进展太多,它们无法匹配XXX从同一行开始的多个序列。pos
  2. 第二种解决方案是不正确的:它匹配两个X在彼此之上的连续行。不一定要连续三个。

因此,所有赞成票(和赏金)都应该投给m.buettner综合 .NET 答案或来自Qtax本人的引人入胜的 PCRE 答案。


原始答案

这是使用将 Perl 代码嵌入正则表达式的答案。因为 Perl 正则表达式可以使用代码在正则表达式中断言任意条件或发出部分正则表达式,所以它们不仅限于匹配常规语言或上下文无关语言,还可以匹配 Chomsky 层次结构中更高语言的某些部分。

您要匹配的语言可以用正则表达式描述为

^ .{n} X .*\n
  .{n} X .*\n
  .{n} X

哪里n是一个数字。这与匹配a n b n c n语言一样复杂,这是上下文相关语言的规范示例。

我们可以轻松匹配第一行,并使用一些 Perl 代码为其他行发出正则表达式:

    /^ (.*?) X
       (?: .*\n (??{"." x length($1)}) X){2}
    /mx

那很短!它有什么作用?

  • ^ (.*?) X锚点在一行的开头,匹配尽可能少的非换行符,然后是X. 我们记得排队到Xas capture group $1

  • 我们重复一个组两次,它匹配该行的其余部分,一个换行符,然后注入一个匹配长度相同的字符串的正则表达式$1。之后,必须有一个X.

这个正则表达式现在将匹配每个三个X相互重叠的字符串。

如果我们想提取所有这样的序列,我们必须很漂亮。因为序列可能重叠,例如

.X
XX
XX
X.

下一场比赛开始的位置不得超过第一个X。我们可以通过后视和前瞻来做到这一点。Perl 只支持固定长度的lookbehind,但有 \K提供类似语义的转义。因此

/^ (.*?) \K X
   (?=( (?: .*\n (??{"."x length($1)}) X ){2} ))
/gmx

将匹配三个垂直Xes 的每个序列。测试时间:

$ perl -E'my$_=join"",<>; say "===\n$1X$2" while /^(.*?)\KX(?=((?:.*\n(??{"."x length($1)})X){2}))/gmx' <<'END'
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
END
===
..X..X...X....
X.X...X..X.....
X....XXXXX
===
X.X...X..X.....
X....XXXXXX.....
X
===
X....XXXXXX.....
X..XXX...........
.....X
===
..............X
..X...........X....
..X...........X

注意:这依赖于至少从 Perl 5 v10 开始提供的实验性正则表达式功能。该代码使用 v16 perl 进行了测试。


没有嵌入代码的解决方案

让我们看看线条

...X...\n
...X..\n

我们要断言...每行的前导部分具有相同的长度。我们可以通过使用基本案例递归来做到这一点X.*\n

(X.*\n|.(?-1).)X

如果我们将它锚定在一行的开头,我们可以匹配两个垂直的Xes。要匹配多于两行,我们必须先行进行递归,然后将匹配位置推进到下一行,然后重复。为此,我们简单地匹配.*\n

这导致以下正则表达式可以匹配具有三个垂直Xes 的字符串:

/ ^
  (?:
    (?=( X.*\n | .(?-1). ) X)
    .*\n # go to next line
  ){2}
/mx

但这还不够好,因为我们想要匹配所有这样的序列。为此,我们实质上将整个正则表达式置于前瞻中。正则表达式引擎确保每次都推进位置以产生新的匹配。

/ ^
  (?=
    (
      (?:
          (?= (X.*\n | .(?-1). ) X)
          .*\n # go to next line
      ){2}
      .* # include next line in $1
    )
  )
/mx

测试时间:

$ perl -E'my$_=join"",<>; say "===\n$1" while /^(?=((?:(?=(X.*\n|.(?-1).)X).*\n){2}.*))/gmx' <<'END'
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
END
===
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
===
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
===
X....XXXXXX.....
X..XXX...........
.....X..........
===
..............X
..X...........X....
..X...........X....X...

所以这和嵌入代码的解决方案一样有效,也就是说,它将每组行与垂直Xes 匹配,而不是每组Xes。(实际上,这个解决方案对我来说似乎比嵌入式代码更脆弱)

于 2013-06-12T08:11:05.477 回答
27

首先:精彩的问题。我认为尝试将正则表达式引擎发挥到极致是很有启发性的。

基本的 .NET 解决方案

你们在评论中说使用.NET 会很容易,但是由于还没有答案,我想我会写一个。

您可以使用 .NET 的可变长度后视和平衡组来解决问题 1. 和 2.。大部分工作由平衡组完成,但可变长度的后视对于能够检测到从同一行开始的多个匹配项至关重要。

无论如何,这是模式:

(?<=                  # lookbehind counts position of X into stack
  ^(?:(?<a>).)*       # push an empty capture on the 'a' stack for each character
                      # in front of X
)                     # end of lookbehind

X                     # match X

(?=.*\n               # lookahead checks that there are two more Xs right below
  (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
  (?(a)(?!))          # make sure that stack 'a' is empty
  X.*\n               # match X and the rest of the line
  (?:(?<-b>).)*       # while we can pop an element from stack 'b', and consume
                      # a character
  (?(b)(?!))          # make sure that stack 'b' is empty
  X                   # match a final X
)                     # end of lookahead

此模式必须与 一起使用RegexOptions.Multiline^匹配行的开头(显然与RegexOptions.IgnorePatternWhitespace自由空间模式一起工作)。

以下是一些额外的评论:

通过将除初始 X 之外的所有内容放入环视中,我们不会遇到重叠匹配甚至从同一行开始的匹配的问题。但是,lookbehind 必须是可变长度的,这肯定会将任何此类解决方案限制在 .NET 中。

其余的依赖于对平衡组的良好掌握。我不会在这里详细讨论这个问题,因为它本身就给出了相当长的答案。(有关更多信息,请参阅MSDN此博客文章)

向后看只是匹配^.*,所以直到行首的所有内容,但是对于每一次.我们将一个空捕获推入 stack a,从而将我们的位置计算X为堆栈的大小。

然后在使用前瞻中的其余行之后,我们再次匹配 just .*,但在使用 each 之前.,我们从堆栈中弹出一个元素a(这会导致失败,一次a为空)并将一个空捕获推送到b(这样我们就不会'不要忘记第三行必须有多少个字符)。

为了确保我们真正清空整个堆栈,我们使用(?(a)(?!)). (?!)这是一个条件模式,如果堆栈不为空,则尝试匹配a(否则简单地跳过)。并且(?!)是一个空的否定前瞻,它总是失败。因此,这只是编码,“a不是空的?失败。否则,继续”。

现在知道我们已经在新行中使用了正确数量的字符,我们尝试匹配 aX和该行的其余部分。然后我们用 stack 再次重复相同的过程b。现在不需要压入任何新堆栈,因为如果这可行,我们就完成了。在此之后我们检查它b是否为空,并匹配第三个X

最后,一个优化方面的说明:如果所有重复都包含在原子组中(从而模拟所有格量词,.NET 不支持),这种模式仍然有效!这将节省大量的回溯。此外,如果我们至少将堆栈弹出量词放在原子组中,我们可以摆脱这两个(?(...)(?!))检查(因为这些检查仅在前面的重复必须回溯的情况下才需要)。

完整的 .NET 解决方案

(只有最勇敢的冒险者才能跟随我进入我即将进入的真正黑暗的洞穴......)

正如评论中所讨论的,这个解决方案有一个缺点:它计算重叠匹配。例如

..X..
..X..
..X..
..X..

给出两个匹配项,一个在第一行,一个在第二行。我们想避免这种情况,只报告一场比赛(如果有 6 到 8X秒,则报告两场,如果有 9 到 11X秒,则报告三场,依此类推)。此外,我们要报告第 1、4、7、... 的比赛X

我们可以通过要求第一个X前面是X满足我们要求的 3 个 other 的整数倍来调整上述模式以允许此解决方案。检查 this 的基本思想使用与以前相同的堆栈操作(除了我们在 3 个堆栈之间移动东西,以便在找到三个Xs 后我们结束了我们开始的地方)。为此,我们必须稍微调整一下lookbehind。

不过有一个问题。.NET 的可变长度lookbehind 使用另一个.NET 独有的功能,RightToLeftMode其中模式是从右到左读取(和匹配)的。通常这不需要打扰我们,但是当我们将其与平衡组结合使用时,我们可能会遇到一些令人不快的惊喜。特别是,在考虑我们的捕获堆栈如何演变时,我们还需要从右到左(或从下到上)构造(并读取)表达式。

因此,当您阅读以下表达式(以及我的注释​​)时,从最外面的lookbehind 的末尾开始(您必须滚动一点) - 即就在唯一的顶级X; 然后一直阅读到顶部。然后在后视之后继续。

(?<=                  
  # note that the lookbehind below does NOT affect the state of stack 'a'!
  # in fact, negative lookarounds can never change any capturing state.
  # this is because they have to fail for the engine to continue matching.
  # and if they fail, the engine needs to backtrack out of them, in which
  # case the previous capturing state will be restored.
  (?<!                # if we get here, there is another X on top of the last
                      # one in the loop, and the pattern fails
    ^                 # make sure we reached the beginning of the line
    (?(a)(?!))        # make sure that stack 'a' is empty
    (?:(?<-a>).)*     # while we can pop an element from stack 'a', and consume
                      # a character
    X.*\n             # consume the next line and a potential X
  )
  # at this point we know that there are less than 3 Xs in the same column
  # above this position. but there might still be one or two more. these
  # are the cases we now have to eliminate, and we use a nested negative
  # lookbehind for this. the lookbehind simply checks the next row and
  # asserts that there is no further X in the same column.
  # this, together with the loop, below means that the X we are going to match
  # is either the topmost in its column or preceded by an integer multiple of 3
  # Xs - exactly what we are looking for.
  (?:

    # at this point we've advanced the lookbehind's "cursor" by exactly 3 Xs
    # in the same column, AND we've restored the same amount of captures on
    # stack 'a', so we're left in exactly the same state as before and can
    # potentially match another 3 Xs upwards this way.
    # the fact that stack 'a' is unaffected by a full iteration of this loop is
    # also crucial for the later (lookahead) part to work regardless of the
    # amount of Xs we've looked at here.

    ^                 # make sure we reached the beginning of the line
    (?(c)(?!))        # make sure that stack 'a' is empty
    (?:(?<-c>)(?<a>).)* # while we can pop an element from stack 'c', push an
                      # element onto 'a' and consume a character
    X.*\n             # consume the next line and a potential X
    (?(b)(?!))        # make sure that stack 'b' is empty
    (?:(?<-b>)(?<c>).)* # while we can pop an element from stack 'b', push an
                      # element onto 'c' and consume a character
    X.*\n             # consume the next line and a potential X
    (?(a)(?!))        # make sure that stack 'a' is empty
    (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
    X.*\n             # consume the next line and a potential X
  )*                  # this non-capturing group will match exactly 3 leading
                      # Xs in the same column. we repeat this group 0 or more
                      # times to match an integer-multiple of 3 occurrences.
  ^                   # make sure we reached the beginning of the line
  (?:(?<a>).)*        # push an empty capture on the 'a' stack for each
                      # character in front of X
)                     # end of lookbehind (or rather beginning)

# the rest is the same as before    

X                     # match X
(?=.*\n               # lookahead checks that there are two more Xs right below
  (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
  (?(a)(?!))          # make sure that stack 'a' is empty
  X.*\n               # match X and the rest of the line
  (?:(?<-b>).)*       # while we can pop an element from stack 'b', and consume
                      # a character
  (?(b)(?!))          # make sure that stack 'b' is empty
  X                   # match a final X
)                     # end of lookahead

RegexHero.net 上的工作演示。

这次我把所有的解释都正确地穿插在了模式中。因此,如果您按照我上面推荐的方式阅读该模式,您就会在需要时得到正确的解释......

现在那真是一头野兽。但它现在满足了整个规范,并展示了 .NET 的正则表达式的强大功能。而且,虽然这看起来很可怕,但我认为(一旦你意识到从右到左的东西)这比 PCRE 的可比解决方案(使用递归或其他方式)更容易理解。

正如 Kobi 在下面的评论中提到的那样,如果您接受在单个匹配的多个捕获中找到您的结果(例如,如果您有 7X的列,您只会得到一个匹配,但使用2 在某个组中捕获)。您可以通过将主要(前瞻)部分重复 1 次或多次并捕获初始部分X(尽管将所有内容放在前瞻中)来做到这一点。然后后视不需要计算Xs 的三倍,而只需要检查没有前导X。这可能会将图案的大小减半。

部分 PCRE 解决方案

(如果只有最勇敢的冒险者跟随我完成最后的解决方案,我可能在接下来的旅程中只剩下疯子......)

为了证明我刚才所说的上述解决方案与 PCRE 的比较,让我们看看我们如何甚至可以远程解决 PCRE 中的全部问题。如果没有可变长度的后视和平衡组,我们将不得不更加努力地工作。

Qtax(OP)为他的第一个问题(检查字符串是否包含任何-column)提供了一个绝妙的解决方案,X使用自引用组进行计数。这是一个非常优雅和紧凑的解决方案。但是因为每个匹配都是从行的X开头到列的开头,并且匹配不能重叠,所以我们不能在每行得到多个匹配。我们可以尝试将所有内容都放在一个前瞻中(这样实际上没有匹配),但是两个零宽度匹配也永远不会从同一位置开始——所以我们仍然会在每个候选行中得到一个匹配。

然而,至少可以用 PCRE 解决问题 2 的第一部分:计算从每行开始的列数(因此计算X列的总数)。由于我们无法以单个匹配的形式获得此计数(请参阅上一段),并且我们无法以单个组或捕获的形式获得此计数(因为 PCRE 仅提供固定且有限数量的捕获,而不是 .NET )。我们可以做的是对匹配中的列数进行编码。

方法如下:对于每一行,我们检查是否有一列开始。如果是这样,我们在某个捕获组中包含一个字符。然后,在报告成功匹配之前,我们尝试找到尽可能多的列 - 为每个列添加一个字符到该特定组。通过这样做,我们在特定捕获的长度中对从每行开始的列数进行编码。

实际上,在正则表达式中实现这个概念比听起来要复杂得多(而且听起来已经相当复杂)。无论如何,这里是:

^                        
(?:(?|
  (?(5)(?![\s\S]*+\5))      
  (?!(?!)()()) 
  (?=
    (?:
      .                  
      (?=                
        .*+\n            
        ( \3? . )   
        .*+\n        
        ( \4? . )    
      )
    )*?              
    X .*+\n          
    \3               
    X .*+\n          
    \4               
  )
  ()
|
  (?(5)(?=[\s\S]*+\5)|(?!))
  (?:
    .
    (?=
      .*+\n
      ( \1? .)
      .*+\n
      ( \2? .)
    )
  )+?
  (?=
    (?<=X).*+\n
    (\1)         
    (?<=X).*+\n
    (\2)         
    (?<=X)     
  )
  (?=
    ([\s\S])   
    [\s\S]*
    ([\s\S] (?(6)\6))
  )
){2})+

(实际上,它比这更容易一些 - 请参阅 Qtax 的答案以了解如何简化此方法。出于学术原因,无论如何我都会将这种方法留在这里,因为可以从中学到一些非常先进和有趣的技术 - 请参阅摘要结尾。)

是的,没有注释。我想,无论如何,没有人会真正阅读它们,所以我将尝试将这个表达式分解为多个部分(我将采用自上而下的方法)。

那么我们来看看来自地狱的洋葱的外层:

^                        
(?:(?|
  checkForNextColumn
|
  countAndAdvance
){2})+

因此,我们的匹配再次锚定在行的开头。然后我们有一个(?:...{2})+表示偶数重复的东西。这就是两个子模式的交替。这些子模式代表了我上面提到的步骤。第一个检查是否有从当前行开始的另一列,第二个注册一个计数并为第一个子模式的另一个应用程序准备引擎的状态。所以控制权交给了第二个模式——第一个只是使用前瞻检查另一列,因此是一个零宽度模式。这就是为什么我不能简单地把所有东西都包起来,+而是必须做{2})+thing - 否则零宽度组件将只尝试一次;这是几乎所有引擎都应用的必要优化,以避免像(a*)+.

还有一个(非常重要的细节):我用于(?|...)交替。在这种分组中,每个备选方案都以相同的组号开始。因此在/(?|(a)|(b))/两者中ab都可以被捕获到 group1中。这是允许子模式之间“通信”的关键技巧,因为它们可以修改相同的组。

无论如何......所以我们有这两个子模式。我们想确保控制在它们之间真正交替。因此,如果它是最后一个匹配的组,则每个组都会失败。我们通过将模式包装在一些分组和引用魔法中来做到这一点:

^(?:(?|
  (?(5)(?![\s\S]*+\5))       # if group 5 has matched before make sure that
                             # it didn't match empty
  checkForNextColumn         # contains 4 capturing groups
  ()                         # this is group 5, match empty
|
  (?(5)(?=[\s\S]*+\5)|(?!))  # make sure that group 5 is defined and that it
                             # matched empty
  advanceEngineState         # contains 4 capturing groups
  (?=
    ([\s\S])                 # this is group 5, match non-empty
    [\s\S]*                  # advance to the end very end of the string
    ([\s\S] (?(6)\6))             # add a character from the end of the string to
                             # group 6
  )
){2})+

因此,在每个备选方案结束时,我们将使该备选方案的条件无效,甚至开始匹配。在第二个备选方案的最后,我们还将6使用 Qtax 概述的技术将一个字符包含到 group 中。这是计数步骤。即,组6将包含与从当前行开始的列一样多的字符。

现在checkForNextColumn真的只是 Qtax 在前瞻中的解决方案。不过,它需要再做一次修改,为了证明这一点,我们将advanceEngineState首先研究。

让我们考虑一下我们希望如何修改状态,以使 Qtax 的解决方案匹配行中的第二列。假设我们有输入

..X..X..
..X..X..
..X..X..

我们想找到第二列。这可以通过从第一个之后的位置开始匹配X并具有组\1并且\2已经分别初始化为第..X2 行和第 3 行的前三个字符 ( ) 来完成(而不是它们为空)。

现在让我们尝试这样做:匹配所有内容,包括X开始一列的下一个,然后用相应的行前缀填充两个组以在checkForNextColumn模式中使用。这又是 Qtax 的模式,除了我们计算Xin(而不是在它之前停止),并且我们需要将捕获添加到一个单独的组中。所以这里是advanceEngineState

(?:
  .
  (?=
    .*+\n
    ( \1? .)
    .*+\n
    ( \2? .)
  )
)+?
(?=
  (?<=X) .*+\n
  (\1)        
  (?<=X) .*+\n
  (\2)        
  (?<=X)
)

请注意我是如何将Xs 变成后视的,以进一步了解一个字符,以及我如何有效地复制\1into\3\2into的最终内容\4

因此,如果我们现在像前瞻一样使用 Qtax 的解决方案checkForNextColumn,使用组\3and\4而不是\1and \2,我们应该完成了。

但是我们如何创建这些组\3\4不是\1and\2呢?我们可以用 开始模式()(),它总是匹配,而不影响引擎的光标,但是将组计数增加 2。但是,这是有问题的:这会将组1和空字符串重置2为空字符串,如果我们找到第二列,advanceEngineState将处于不一致的状态(因为引擎的全局位置已经提前,但计数组再次为零)。所以我们想让这两个组进入模式,但不影响他们当前正在捕获的内容。我们可以通过使用我已经提到的 .NET 解决方案来做到这一点:负面环视中的组不会影响捕获的内容(因为引擎需要从环视中回溯才能继续)。因此,我们可以使用(?!(?!)()())(永远不会导致模式失败的负前瞻)在我们的模式中包含两组从未使用过的括号。这允许我们使用组34在我们的第一个子模式中,同时保持组12下一次迭代的第二个子模式保持不变。总之,这是checkForNextColumn

(?!(?!)()()) 
(?=
  (?:
    .                  
    (?=                
      .*+\n            
      ( \3? . )   
      .*+\n        
      ( \4? . )    
    )
  )*?              
  X .*+\n          
  \3               
  X .*+\n          
  \4               
)

其中,在大多数情况下,实际上看起来真的很熟悉。

所以就是这样。针对某些输入运行此命令将为我们提供一个组,该组6包含一个捕获,每行都有一个以列开头的行 - 捕获的长度将告诉我们从那里开始有多少列。

是的,它确实有效(现场演示)。

请注意,这(就像基本的 .NET 解决方案一样)会多计超过 3X秒长的列。我想可以通过前瞻来纠正这个计数(以类似于完整.NET解决方案的后视的方式),但这留给读者作为练习。

有点不幸的是,这个解决方案的基本问题已经非常复杂并且使解决方案膨胀(75% 的行大多只是 Qtax 解决方案的副本)。因为周围的框架有一些非常有趣的技术和教训:

  • 我们可以有多个子模式来完成特定的匹配/计数任务,并让它们通过相互捕获组“通信”,方法是将它们(?|...)交替放置并循环它们。
  • 我们可以强制零宽度替代方案一遍又一遍地执行,方法是将它们包装在一个有限量词中,就像{2}在将所有内容放入之前一样+
  • 可以在一个子模式中跳过组号(不影响捕获的内容),方法是将它们放入永不失败的负前瞻中,例如(?!(?!)()).
  • 控制可以在子模式之间来回传递,方法是在进入交替时检查的某个组中捕获某些东西或什么都没有。

这允许进行一些非常强大的计算(我已经看到 PCRE 实际上是图灵完备的)——尽管这对于生产性使用来说肯定是错误的方法。但是仍然试图理解(并提出)这样的解决方案可能是一个非常具有挑战性的问题,并且在某种程度上是有益的解决问题的练习。

于 2013-06-18T18:58:38.643 回答
11

如果你想找到一个单一的“垂直”模式,这里有一个解决方案。如果您还想匹配“水平”模式,请尝试使用单独的匹配进行匹配,也许检查重叠的匹配位置。请记住,计算机不知道线是什么。是人随意编造的东西。字符串只是一个一维序列,我们将某些字符表示为行尾。

#!/usr/local/perls/perl-5.18.0/bin/perl
use v5.10;

my $pattern = qr/XXX/p;

my $string =<<'HERE';
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
HERE


$transposed = transpose_string( $string );

open my $tfh, '<', \ $transposed;
while( <$tfh> ) {
    while( /$pattern/g ) {
        my $pos = pos() - length( ${^MATCH} );
        push @found, { row => $pos, col => $. - 1 };
        pos = $pos + 1; # for overlapping matches
        }
    }

# row and col are 0 based
print Dumper( \@found ); use Data::Dumper;

sub transpose_string {
    my( $string ) = @_;

    open my $sfh, '<', \ $string;

    my @transposed;
    while( <$sfh> ) {
        state $row = 0;
        chomp;
        my @chars = split //;

        while( my( $col, $char ) = each @chars ) {
            $transposed[$col][$row] = $char;
            }

        $row++;
        }

    my @line_end_positions = ( 0 );
    foreach my $col ( 0 .. $#transposed ) {
        $transposed .= join '', @{ $transposed[$col] };
        $transposed .= "\n";
        }
    close $sfh;

    return $transposed;
    }
于 2013-06-12T08:50:18.173 回答
5

问题 2 的工作解决方案

在 Perl/PCRE 中完全有可能!:)

抱歉,我参加聚会有点迟到了,但我想指出的是,您实际上可以计算找到的 XXX 编队的数量;也就是说,构造表达式,使得在执行全局匹配时,每个结构恰好有一个匹配。不过,这很棘手。

这里是:

\A(?:(?=(?(3)[\s\S]*(?=\3\z))(?|.(?=.*\n(\1?+.).*\n(\2?+.))|.*\n()())+?(?<=X)(?=.*\n\1(?<=X).*\n\2(?<=X))(?=([\s\S]*\z)))(?=[\s\S]*([\s\S](?(4)\4)))[\s\S])+[\s\S]*(?=\4\z)|\G(?!\A|[\s\S]?\z)

在 regex101 上运行

我可能应该对此添加一些评论!在这里,对于那些感兴趣的人:

\A(?:
    (?=
        (?(3)[\s\S]*(?=\3\z))                   # Resume from where we ended last iteration

        (?|                                     # Branch-reset group used to reset \1
            .(?=.*\n(\1?+.).*\n(\2?+.))         # and \2 to empty values when a new line
            |                                   # is reached. ".*\n" is used to skip the
            .*\n()()                            # rest of a line that is longer than the
        )+?                                     # ones below it.

        (?<=X)(?=.*\n\1(?<=X).*\n\2(?<=X))      # Find a XXX formation

        (?=([\s\S]*\z))                         # Save the rest of the line in \3 for 
    )                                           # when we resume looking next iteration

    (?=[\s\S]*([\s\S](?(4)\4)))                 # For every formation found, consume another 
                                                # character at the end of the subject

    [\s\S]                                      # Consume a character so we can move on

    )+

[\s\S]*(?=\4\z)                                 # When all formations around found, consume
                                                # up to just before \4 at the subject end.

|

\G(?!\A|[\s\S]?\z)                              # Now we just need to force the rest of the 
                                                # matches. The length of \4 is equal to the 
                                                # number of formations. So to avoid overmatching,
                                                # we need to exclude a couple of cases.

我很害怕; 发生了什么??

基本上,我们在一个重复的非捕获组中通过整个主题,从一个 XXX 编队移动到下一个编队。对于找到的每个编队,将另一个字符附加到主题末尾的计数器上(存储在 \4 中)。有几个障碍需要克服:

  • 如果我们一次性匹配所有内容,我们如何从一行移动到下一行?解决方案:遇到新行时,使用分支重置组重置 \1 和 \2。

  • 如果我们有一个大的 ASCII 网格,最后有一个小的 "\nX\nX\nX" 怎么办?如果我们从一个队形到另一个队形消耗对象,我们将开始吃我们的柜台。解决方案:一次只消耗一个字符,并将编队搜索逻辑包装在前瞻中。

  • 但是,如果我们不从一个编队消耗到下一个编队,我们怎么知道在哪里继续查看非捕获组的下一次迭代呢?解决方案:找到编队后,从该位置捕捉人物到主题的最后——一个始终可以参考的固定点。这与我用来匹配嵌套括号而不使用递归的技巧相同,这确实体现了前向引用的力量。

这很有趣,我很想看到更多这样的帖子!

于 2018-09-02T14:39:02.797 回答
2

您可以旋转图像,然后搜索XXX.

于 2013-06-11T08:35:47.573 回答
0

我使用 PHP 匹配垂直模式的方法。

首先,让我们将输入旋转 90°:

// assuming $input contains your string
$input = explode("\n", $input);
$rotated = array();
foreach ($input as $line)
{
    $l = strlen($line);
    for ($i = 0; $i < $l; $i++)
    {
        if (isset($rotated[$i]))
            $rotated[$i] .= $line[$i];
        else
            $rotated[$i] = $line[$i];
    }
}
$rotated = implode("\n", $rotated);

这导致

..XXX.....
..........
.XX....XX.
....X.....
X...X....X
.X.XXX....
..XX......
...X......
...X......
.XXX......
...X.....
.........
........
........
....XXX
.....
...
..
..
X
.
.
.

现在这可能看起来很奇怪,但实际上让我们更接近,因为我们现在可以简单地preg_match_all()克服它:

if (preg_match_all('/\bXXX\b/', $rotated, $m))
var_dump($m[0]);

瞧:

array(4) {
  [0] =>
  string(3) "XXX"
  [1] =>
  string(3) "XXX"
  [2] =>
  string(3) "XXX"
  [3] =>
  string(3) "XXX"
}

如果您还想匹配相同的水平模式,只需在非旋转输入上使用相同的表达式。

于 2013-06-24T17:16:09.927 回答