Boyer-Moore好后缀启发式算法

22

我了解坏字符启发式如何工作。当你找到不匹配的字母x时,只需将模式向右移动,以使模式中最右边的x与字符串中的x对齐。而且在代码中很容易实现。

我认为我也理解好后缀启发式的工作原理。当我们找到一个好后缀s时,在模式中找到不同位置上相同的后缀并将其向右移动,以使模式中的s与字符串中的s对齐。但我不知道如何在代码中实现。我们如何查找另一个地方是否存在相同的后缀?我们怎么知道它的位置?代码如下:

void bmPreprocess1()
{
    int i=m, j=m+1;
    f[i]=j;
    while (i>0)
    {
        while (j<=m && p[i-1]!=p[j-1])
        {
            if (s[j]==0) s[j]=j-i;
            j=f[j];
        }
        i--; j--;
        f[i]=j;
    }
}

来自http://www.iti.fh-flensburg.de/lang/algorithmen/pattern/bmen.htm的内容对我来说不太合理...能否有人写出尽可能简单的伪代码来完成这个任务?或者做些解释?


如果您有任何问题,请留下评论。 - CS Pei
1个回答

18

首先,我将使用p[i]表示模式中的一个字符,m表示模式长度,$表示模式中的最后一个字符,即$ = p[m-1]

好后缀启发式算法有两种情况,第一种情况如下:

情况1

考虑以下例子:

    leading TEXT cXXXbXXXcXXXcXXX rest of the TEXT
         cXXXbXXXcXXXcXXX
                     ^ 
                     | mismatch here

因此,在模式 cXXXbXXXcXXXcXXX 中,子字符串 XXX 是好的后缀。不匹配发生在字符 c 上。那么在不匹配后,我们应该向右移动4还是8个位置?

如果像下面一样向右移动4个位置,则会再次出现相同的不匹配( b 与 c 不匹配),

    leading TEXT cXXXbXXXcXXXcXXX rest of the TEXT
             cXXXbXXXcXXXcXXX
                     ^ 
                     | mismatch occurs here again

因此,在这种情况下,我们实际上可以将8个字符向右移动。

情况2

让我们看另一个例子。

    leading TEXT cXXXcXXXbXXXcXXX rest of the TEXT
            cXXXXcXXXbXXXcXXX
                         ^ 
                         | mismatch happens here

我们可以将其向右移动4或8个字符吗?显然,如果我们移动8个或更多的字符,我们将错过使用文本匹配模式的机会。因此,在这种情况下,我们只能将4个字符向右移动。

那么这两种情况有什么区别呢?

区别在于,在第一种情况下,不匹配的字符 c 加上良好后缀XXX,即 cXXX ,与下一个(从右数)匹配XXX的相同,加上前一个字符。而在第二种情况下, cXXX 与下一个匹配(从右数),加上该匹配项之前的字符不同。

因此,对于任何给定的良好后缀(让我们称其为XXX),我们需要确定这两种情况的移位,(1)在模式中,良好后缀之前的字符(让我们称其为c)加上良好后缀,也与良好后缀的下一个(从右数)匹配加上它之前的字符,(2)字符加上良好后缀不匹配。

例如,对于情况(1),如果您有一个模式0XXXcXXXcXXXcXXXcXXXcXXX,如果第一个 c 的测试失败后,您可以将20个字符向右移动,并将0XXX与已经测试过的文本对齐。

这就是计算数字20的方式,

              1         2
    012345678901234567890123
    0XXXcXXXcXXXcXXXcXXXcXXX
    ^                   ^
在不匹配的位置是20,移位的子字符串0XXX将从20到23占据位置。而0XXX以位置0开始,所以20-0=20。 对于情况(2),例如在这个例子中,0XXXaXXXbXXXcXXX,如果在第一次测试c失败后,您只能向右移动4个字符,并将bXXX与已测试的文本对齐。 这就是数字4的计算方式。
    0123456789012345
    0XXXaXXXbXXXcXXX

不匹配发生的位置是12,下一个字符串是bXXX,以8为起始位置,12-8=4。如果我们设定j=12i=8,那么移位就是j-i,也就是在代码中的s[j] = j - i;

边界

为了考虑所有好的后缀,我们首先需要了解什么是所谓的边界。边界是既是一个合适的前缀又是一个合适的后缀的子字符串。例如,在字符串XXXcXXX中,X是一个边界,XX是一个边界,XXX也是一个边界。但是XXXc不是。我们需要确定模式后缀的最宽边界的起始点,并将这个信息保存在数组f[i]中。

如何确定f[i]

假设我们知道f[i]=j,并且对于所有其他f[k](其中i < k < m),即从位置i开始的后缀的最宽边界始于位置j。我们想要根据f[i]找到f[i-1]

例如,对于模式aabbccaacc,在位置i=4处,后缀是ccaacc,最宽的边界是ccp[8]p[9]),所以j=f[i=4]=8。现在我们想根据我们拥有的关于f[4]f[5]等的信息来了解f[i-1]=f[3]。对于f[3],后缀现在是bccaacc。在位置处,它是a!=p[4-1],即b。因此,bcc不是一个边界。

我们知道,宽度 >= 1 的边界bccaacc必须以 b 开头,并从位置j = 8开始搜索后缀的边界,这个例子中是cc。在位置j = f[8]处有最宽的边界c,即9。因此,我们继续比较p[4-1]p[j-1]。它们再次不相等。现在后缀是p[9] = c,并且在位置10处只有零长度的边界。所以现在j = f[9],而它是10。所以我们继续比较p[4-1]p[j-1],它们不相等,那就是字符串的结尾了。然后,f[3]只有零长度边界,使其等于10

更一般地描述该过程

因此,f[i] = j的意思是这样的,

    Position:    012345     i-1  i     j - 1   j       m
    pattern:     abcdef ... @    x ... ?       x ...   $ 

如果在位置i-1处的字符@与位置j-1处的字符?相同,那么我们知道 f[i - 1] = j - 1;--i; --j; f[i] = j;。边界是从位置j-1开始的后缀 @x ... $

但是,如果在位置i-1处的字符@与位置j-1处的字符?不同, 我们必须继续向右搜索。 我们知道两个事实:(1)现在我们知道边界宽度必须小于从位置开始的宽度,即小于x...$。其次,边界必须以@...开头并以字符$结尾,或者可能为空。

基于这两个事实,我们在子字符串x ... $(从位置j到m)内继续搜索以x开头的边界。然后下一个边界应该在j处,即等于f[j],即j=f[j]。然后我们将字符@x之前的字符(位于j-1处)进行比较。如果它们相同,我们就找到了边界,否则继续这个过程直到m。 这个过程由下面的代码展示:

    while (j<=m && p[i-1]!=p[j-1])
    {
        j=f[j];
    }

    i--; j--;
    f[i]=j;

现在看一下条件p[i-1] != p[j-1],这就是我们在情况(2)中讨论过的,即p[i]p[j]匹配,但p[i-1] != p[j-1],所以我们从i移动到j,即s[j] = j - i;

现在唯一没有解释的是测试if(s[j] == 0),当一个更短的后缀具有相同的边界时,它将发生。例如,你有一个模式

    012345678
    addbddcdd
当你计算f[i - 1]并且i = 4时,你会设置s[7]。但是当你计算f[i-1]并且i = 1时,如果没有测试if (s[j] == 0),你会再次设置s[7]。这意味着如果你在位置6上有不匹配,你需要将3向右移(使bdd与占据位置的cdd对齐),而不是将其移动到6(不要将其移动直到add到占据位置的cdd)。
    void bmPreprocess1()
    {
        // initial condition, set f[m] = m+1;
        int i=m, j=m+1;
        
        f[i]=j;

        // calculate f[i], s[j] from i = m to 0.
        while (i>0)
        {
            // at this line, we know f[i], f[i+1], ... f[m].
            while (j<=m && p[i-1]!=p[j-1]) // calculate the value of f[i-1] and save it to j-1
            {
                if (s[j]==0) s[j]=j-i; // if s[j] is not occupied, set it.
                j=f[j]; // get the start position of the border of suffix p[j] ... p[m-1]
            }
            // assign j-1 to f[i-1]
            i--; j--;
            f[i]=j;
        }
    }

1
+1. 一个问题:我们知道 bccaacc 的任何宽度≥1的边界都必须以 b 开头,后缀从位置 j = 8 开始的边界是什么直觉?我理解它应该以 b 开头,但我们如何简单地跳到 f[8]? - Imposter
我的意思是我可以想到f(3)可以从f(4)推导出来,但它如何使用f(5),f(6)等等呢?特别是这段代码: "while (j<=m && p[i-1]!=p[j-1]) { j=f[j]; }" 这个直觉j=f[j]是怎么来的? - Imposter
2
既然我们已经确定bcc不是边界,那么我们必须找到边界cc的边界,因为边界既是前缀又是后缀。f(3)需要f(5)的原因也是如此。如果当前的边界不起作用,我们需要找到当前边界的边界。 - CS Pei
1
一个这样的例子是 012dabdabcabdab。现在考虑 f(3) - CS Pei
2
如果人们能够使用更好的单词变量名,那么有多少解释可以被省略掉呢? - BartoszKP
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接