【问题标题】:Creating string set from given regular expression in language L从语言 L 中的给定正则表达式创建字符串集
【发布时间】:2019-04-03 08:53:27
【问题描述】:

我正在尝试根据正则表达式(也由用户给出)创建字母表中的单词序列(由用户给出)但无法做到。

示例场景 1:

Alphabet = [a,b,c]

Regex = (a+c)b*

Word Count = 6

Words = ["a", "c", "ab", "cb", "abb", "cbb"]

示例场景 2:

Alphabet = [a,b]

Regex = (a+b)*a

Word Count = 3

Words = ["a", "aa", "ba"]

我尝试将正则表达式转换为后缀/中缀,然后从那里开始,但无法构建引擎算法。

基本上有3个操作;

联合 (+)
连接 ()
闭包 (*)

我为每种运算符类型编写了一个函数;

void union(char* x[], char y)
{
    printf("%s\n%c\n", x, y);

    remainingWordCount -= 2;
}

void concat(char* x[], char* y[])
{
    printf("%s%s\n", x, y);
    remainingWordCount--;
}

void closure(char* x[], char* y[])
{
    while (remainingWordCount > 0)
    {
        concat(x, y);
    }
}

它只适用于大多数基本场景。

所以我的问题是如何在不使用任何正则表达式库的情况下根据给定的正则表达式创建字符串集?有没有已知的算法?

【问题讨论】:

  • 您的语法有点混乱(您所说的 Language 实际上是字母表):(a+c) 直观上是 aca(a*)c,但您似乎将其用作(a|c).
  • @Arkku 感谢您的反馈。将语言更改为字母。是的,我猜运营商与标准有所不同。
  • 你应该生成无限字符串中的哪一个? count 最短的?按字母顺序排列的第一个 count 字符串?任何count 字符串? count 字符串的统一随机样本?当请求为五个时,您的第一个示例显示六个字符串,因此它使问题悬而未决...
  • @Arkku:使用“+”表示交替实际上是计算机科学的标准。
  • 在您的第一个示例中,按字母顺序排列的前 5 个字符串是 aababbabbbabbbb。 (按字母顺序,您永远不会产生以c 开头的字符串。)这不会使标准不正确;它只是表明需要一个精确的规范。

标签: c regex automata fsm computation


【解决方案1】:

“蛮力”解决方案是将正则表达式解析为有限状态机的状态转换图,每个状态都有一个转换列表,每个转换都有相关的符号(字符)和下一个状态。您可以使用没有转换的状态来指示终端状态。

然后遍历这个图,记住转换产生的字符串。当您到达终端状态时,打印单词并减少剩余的字数,当它达到零时停止。如果通过图形的不同路径最终可能产生相同的单词,您还需要记住已经输出的任何单词,如果相同的单词已经存在,则不要打印/递减。

按排序顺序处理路径(使得较短的路径出现在具有相同前缀的较长路径之前,即按照 C 中的 strcmp)。这样可以避免陷入循环,并给出你想要的顺序。

例如对于正则表达式a*(伪代码):

state[0] = { {'a', 0}, {'\0', 1} };
state[1] = { }; // terminal state
paths = { { .state = 0, .string = "" } }; // set initial state

您从状态 0 处的唯一路径开始,然后(分别)附加到状态 0 的两个转换以创建新路径:

paths = { { .state = 1, .string = "" },
          { .state = 0, .string = "a" } };

由于空字符串的路径首先排序(由于空字符串在任何非空字符串之前排序),它首先被处理,并且由于它处于没有转换的终端状态,它打印空字符串并减少字数。然后你走另一条路,再次添加状态0 的转换,最终得到:

paths = { { .state = 1, .string = "a" },
          { .state = 0, .string = "aa" } };

等等

(免责声明:这是完全未经测试的,超出了我的想象,并且可能存在我没有想到的极端情况。另外请注意,对于非平凡的正则表达式,路径的数量会激增。)

【讨论】:

    【解决方案2】:

    基本算法很简单(如果你知道怎么做的话):

    1. Construct a DFA from the regular expression。 (构造 NFA 是不够的,因为如果正则表达式不确定,NFA 将产生重复的字符串。)该链接显示了执行此操作的一种方法,但还有其他方法;如果有的话,您可能会在正式语言教科书中找到更长的说明。

    2. 对从 DFA 生成的(无限)图进行有序游走 ("best-first search"),其中每个节点是一对 (state, prefix),边对应于 DFA 中的转换。在遍历过程中,如果遇到state 正在接受的节点,则生成其prefix

    该基本算法适用于具有前缀属性的字符串之间的任何排序关系:字符串的任何正确前缀都保证小于字符串。 (如果不是这种情况,则可能在一组字符串中没有“最小”元素。例如,如果排序关系将一个字符串放在该字符串的任何前缀之前,但在其他方面是按字典顺序排列的,那么在a* 前面是下一个较长的字符串,会产生无限循环。)

    需要注意的是,节点中的state只是为了方便;它在计算上是多余的,因为它可以通过将prefix 传递给 DFA 来重新生成。因此,该图永远不会包含具有相同prefix 的两个不同节点。这种观察的推论是没有必要维护一组“可见”节点,因为两个不同节点的后继集是不相交的。

    为了实现第2步中的有序搜索,需要知道图中每个节点的最小接受后继,不一定是前缀最少的直接后继。

    例如,词典(“字母”)排序关系由下式给出:

    (S<sub>1</sub>, P<sub>1</sub>) &lt; (S<sub>2</sub>, P<sub>2</sub>) <strong>iff</strong> P<sub>1</sub> &lt;<sub>lexicographic</sub> P<sub>2</sub>

    在这种情况下,最不被接受的后继者肯定有最不直接的后继者作为前缀,因此按前缀对候选者进行排序就足够了。

    通过对比,更常见的“按长度然后按字典”排序由以下方式给出:

    (S<sub>1</sub>, P<sub>1</sub>) &lt; (S<sub>2</sub>, P<sub>2</sub>) <strong>iff</strong> |P<sub>1</sub>| &lt; |P<sub>2</sub>| <strong>or</strong> (|P<sub>1</sub>| &amp;equals; |P<sub>2</sub>| <strong>and</strong> P<sub>1</sub> &lt;<sub>lexicographic</sub> P<sub>2</sub>)

    你不能仅仅通过查看它们的直接后继来预测两个节点中最不被接受的后继的顺序。您还需要知道到达接受节点(或等效地,状态)所需的最少符号数。幸运的是,使用 DFA 上的任何 all-pairs shortest-paths algorithm 很容易预先计算。

    【讨论】:

      【解决方案3】:

      我建议使用“迭代器”设计模式。我看到您正在使用 C,它并不是特别适合面向对象的代码,但是您可以通过使用包含指向 next 函数的指针、指向 restart 函数的指针和指向要传递给这些函数的“上下文”对象的指针,其中“上下文”对象的性质将取决于运算符。

      在类似a 的情况下,next 函数第一次返回"a",第二次返回NULL。 (上下文对象会跟踪"a" 以及它是否已经被返回。)

      在类似...+... 的情况下,next 可以在处理第二个之前耗尽第一个... 的迭代器,或者根据您的喜好在它们之间交替。 (上下文对象保存了指向... 的两个迭代器的指针,以及接下来要调用哪一个。)

      。 . .等等。

      最难的部分是解析表达式以创建所有这些对象,但听起来您已经对解析表达式感到满意了?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-20
        • 2011-10-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多