【问题标题】:"vertical" regex matching in an ASCII "image"ASCII“图像”中的“垂直”正则表达式匹配
【发布时间】:2013-06-07 00:58:45
【问题描述】:

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

问题

在 ASCII“图像”/art/map/string 中,例如:

....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 个)

【问题讨论】:

  • @HamZa,这应该是一个独立于输入的通用正则表达式。所以这样的伎俩就是作弊。 ;-)
  • 不修改输入(例如旋转)并且没有“自动”生成解决方案,我会说祝你好运 :-)
  • @HamZa 挑战已接受。 ;-) 1 至少在 .NET 中很容易,而且 PCRE 也是可能的。 2. 另一方面,我不确定这是否可能。
  • XXXX 算作 3 个Xs 的 0、1 或 2 个序列吗?
  • @hakre 已经有两个答案表明了这一点。然而,这不是一个纯正则表达式的解决方案。这个问题更像是一个学术性质的问题,看看一些正则表达式的界限可以推到多远。当然......如果你可以单独使用正则表达式转置图像,那也会令人印象深刻;)

标签: php .net regex perl pcre


【解决方案1】:

问题 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

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

回答问题 2

普通匹配

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

它可以直接在具有有限后视的正则表达式风格中解决。而 Java 和 .NET 等其他风格则可以(例如在 m.buettner's .NET solution 中)。

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

(半?)证明

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

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

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

长度/间接解

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

此解决方案基于m.buettner's nice "partial PCRE solution"/受其启发。

可以简单地将以下表达式的所有匹配项替换为$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)

【讨论】:

  • +1 ...所有格-可选的自我引用组...今天学到了一些新东西! :) ... 那是一些非常好的正则表达式魔术;) ... 也感谢通过 polygenelubricants 链接该系列 - 我以前没有遇到过,现在知道 Java 不小心确实支持无限长度的lookbehinds。
  • 您对 Q2 的部分 PCRE 解决方案感兴趣吗?我有一个解决方案可以为您提供正确的计数(尽管重叠计数过多)。但是,它不会以 n 匹配的形式执行此操作。相反,您会为确实包含至少一列开头的每一行获得一个匹配,然后有一个捕获,其长度表示该行中有多少列开始。因此,通常该捕获将始终包含一个字符,但如果您在一行中有两列,则该行的匹配捕获将包含 2 个字符,依此类推。如果你有兴趣我会在明天发布。
  • @m.buettner,如果您找到了一种方法来指示(以任何方式)每行有多少匹配项(使用 PCRE),我想看看。 :-)
  • 哦,这让事情比我的解决方案简单多了。
  • 你在下一行的计数非常有创意和有用。但我认为你也可以通过重用你的解决方案 1 并将最后 5 行与 X 的实际匹配项包装在前瞻中来计算匹配项。这会给你 4 场比赛。
【解决方案2】:

问题 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)

In action on regex101

我可能应该在其中添加一些 cmets!在这里,对于那些感兴趣的人:

\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。

  • What if we have a large ASCII grid with a small "\nX\nX\nX" at the end? 如果我们将对象从一个阵型消耗到另一个阵型,我们将开始吃我们的柜台。 解决方案:一次只消耗一个字符,并将编队搜索逻辑封装在前瞻中。

  • 但是,如果我们不从一个编队消耗到下一个编队,我们怎么知道在哪里继续查看非捕获组的下一次迭代? 解决方案:当找到一个编队时,从该位置捕获字符到主题的最结束 - 一个始终可以参考的固定点。这与我使用match nested brackets without recursion 的技巧相同,它真正体现了前向引用的力量。

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

【讨论】:

  • 好主意。它看起来有点像公认的答案,但不需要替换。我不确定最后一个 OR 是关于什么的。我试图简化您的正则表达式以使用字符串,而不是回溯到最后一个匹配 here。它的性能似乎略高一些。
【解决方案3】:

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

基本的 .NET 解决方案

你们在 cmets 中说使用 .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 一起使用以使freespacing 模式起作用)。

这里有一些额外的 cmets:

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

其余的依赖于对平衡组的良好掌握。我不会在这里详细介绍,因为它适用于quite long answers in itself。 (有关更多信息,请参阅MSDNthis blog post

lookbehind 只匹配^.*,所以直到行首的所有内容,但对于每个.,我们将一个空捕获压入堆栈a,从而将X 的位置计算为堆栈。

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

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

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

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

完整的 .NET 解决方案

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

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

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

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

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

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

因此,当您阅读以下表达式(以及我的注释​​)时,请从最外层的末尾开始(您必须滚动一点) - 即就在唯一的顶级 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

Working demo on RegexHero.net.

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

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

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

部分PCRE解决方案

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

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

Qtax(OP)为他的第一个问题(检查字符串是否包含任何X-column)提供了一个绝妙的解决方案,使用自引用组进行计数。这是一个非常优雅和紧凑的解决方案。但是因为每个匹配从行的开头到开始列的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})+ 的事情——否则零宽度组件只会被尝试一次;这是几乎所有引擎都应用的必要优化,以避免出现(a*)+ 等模式的无限循环。

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

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

^(?:(?|
  (?(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})+

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

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

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

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

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

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

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

请注意我是如何将Xs 转换为lookbehinds 的,以进一步了解一个字符,以及我如何有效地将\1 的最终内容复制到\3 以及将\2 的最终内容复制到\4

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

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

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

其中大部分看起来真的很熟悉。

原来如此。针对某些输入运行此操作将为我们提供一个组 6,其中包含一个捕获,每行都有一个以列开头的行 - 捕获的长度将告诉我们从那里开始的列数。

Yes, it really works (live demo).

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

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

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

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

【讨论】:

  • +1,两个问题都回答了,太好了!尽管它会计算重叠匹配,但我并不那么挑剔(因为它没有指定)。平衡组确实使计数变得容易。正如您所说,可变长度的后视在这里至关重要(至少对于第二季度而言)。现在让我们看看是否有人发布了 PCRE 或 Java 答案(在我之前)。 :-p
  • @Qtax 在 PCRE 上工作 ;) ...我认为 Java 是不可能的,但我们拭目以待。您可以通过检查是否还有 3 个以下 Xs 的倍数来消除重叠匹配;)(因此,如果您有 4 或 7 个 Xs,您将计算底部的 3 或 6 个并忽略顶部的) ...或者可能将其放在后面以确保之前有 3 个 Xs 的倍数(从而计算前 3 个或 6 个,并忽略底部的那个)...我认为应该是可能的,但是这种模式会非常复杂。
  • 我会说再往下不是问题,而是在上面。因为我们(大概)喜欢找到第一个组合,而不是最后一个。 (如果有 4 个 Xs,第一个 X 会匹配,而最后一个不会是任何“形成”的一部分。)Java 是可能的,我想我会在你偷走所有乐趣之前发布它。 ;) 如果你找到一个使用递归的解决方案,我想看看。
  • @Qtax 是的,我想忽略底部的会更有用,但可变长度的后视不应该成为问题。稍后我会考虑递归解决方案,我现在必须离开。我很想知道 Java 带来的哪些功能可以与 .NET 的平衡组和 PCRE 的递归相匹配。 ;)
  • @Qtax 你去。没有更多的重叠匹配。
【解决方案4】:

我使用 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"
}

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

【讨论】:

  • 我刚刚看到“只允许正则表达式”。好游戏。
  • 也会误报匹配,如果一列有两个X,那么下一行比那两个Xs短,然后是第四列再次足够长并包含另一个X(因为在旋转之前您基本上要做的是将所有列折叠到顶部,如果它们有“孔”)
【解决方案5】:

编辑

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

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

因此,所有赞成票(和赏金)都应该投给m.buettnercomprehensive .NET answerQtax 本人的fascinating PCRE answer


原答案

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

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

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

n 是一个数字。这与匹配 anbncn 语言一样复杂,这是 a上下文相关的语言。

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

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

太短了!它有什么作用?

  • ^ (.*?) X 锚定在一行的开头,匹配尽可能少的非换行符,然后是X。我们记得X 之前的行作为捕获组$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。 (实际上,这个解决方案在我看来比嵌入式代码更脆弱)

【讨论】:

  • 如果你仔细看,有一个bug:这不能匹配多个在同一行开始的XXX cols。我不太确定如何解决这个问题。也许有pos 分配?后视镜中的后视镜?
  • 非常好。当我进入 Mastering Perl 更新的正则表达式章节时,我必须记住这一点:)
  • 很好,虽然我认为这个作弊是因为它使用代码(可以做任何事情)而不是纯正则表达式(这是棘手的部分)。 ;-)
  • “这和匹配上下文相关语言的典型示例 a^nb^nc^n 语言一样复杂。” 我想说这个问题用正则表达式比a^n b^n c^n 更难解决。 Some regex solutions to the latter can be seen here。正如您所指出的,这不适用于/回答问题 2。我想我可以为 regex/PCRE 无法回答问题 2 制定一个(弱?)证据。
  • 没有嵌入代码的解决方案不起作用。请参阅:ideone.com/UxVxJ9 +1 尝试,但它比这更棘手。 ;-)
【解决方案6】:

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

#!/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;
    }

【讨论】:

  • 我确实喜欢先转置图像的简单性,而不是深奥的正则表达式...
  • 这些是运用正则表达式引擎的惊人示例(.NET - 谁知道?),我印象深刻。在实际(实际)应用程序中,轮换是否比这些相当巴洛克式的解决方案更受欢迎,因为它更简单,因此更不“脆弱”并且更容易记录?只是问问。
【解决方案7】:

你可以旋转图片,然后搜索XXX

【讨论】:

  • 好点,但没有回答问题(正则表达式解决方案)。除非你也可以用正则表达式旋转这样的图像。 ;-)
  • @Qtax:如果你不能旋转,正则表达式将不得不寻找三个X,它和前面的换行符之间的字符数相同。对于有限的图片尺寸可以这样做,但不实用。
  • 图片大小不受限制(或者你可能不会在表达式中假设这样的限制)。
  • 我想避免避免输入转换。因为简单地向搜索模式添加一些水平值会破坏这种解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-17
  • 2011-01-08
  • 2013-10-17
  • 2013-06-27
  • 1970-01-01
  • 2023-03-28
相关资源
最近更新 更多