conclude bm-gs algorithm.
@@ -68,3 +68,151 @@ void makeBC(char* const pattern, int* bc){
|
||||

|
||||
|
||||
可以看到,在这种情况下,每次都需要进行`m - 1`次比对,才能在最左侧一次比对中失败,而该次失败只能让模式串右移一个单位。这种情况正与蛮力策略的最好情况相一致。一般地,单次匹配成功的概率越大,即字符集越小,就越接近于这种最坏的情况;单次匹配成功的概率越小,即字符集越大,就越接近于最好的情况。
|
||||
|
||||
### bm-gs策略
|
||||
|
||||
对上述`bc`策略低效的原因进行分析,可以发现这是因为`bc`策略中只利用到了匹配失败的`坏字符`,而在坏字符之前的那多次成功比对却直接被`bc`策略忽略了。在上面的这种情形中,如果注意到最左侧的`1`与其右侧四个字符均不相等,一次比对失败后可以直接跳过这四个无意义的对齐位置,从而规避了这种低效的情况。
|
||||
|
||||
基于上面的考虑,我们这里提出好后缀(good suffix, gs)策略。顾名思义,好后缀策略就是要将某次比对失败前的成功比对信息加以利用,因此它的思想和`kmp`算法是一致的。具体说来,就是要利用这些成功比对的信息,将模式串直接移动到下一个值得对齐的位置,那么这里的值得对齐的位置和`kmp`算法是否存在异同呢?
|
||||
|
||||
设某次比对失败于模式串的位置`j`,因此`P[j+1, m)`与文本串中的对应字符依次相等。一般地,如果这`m - j -2`个字符在模式串中左侧的另一位置再次出现,则显然是一个值得的对齐位置,如下图所示:
|
||||
|
||||

|
||||
|
||||
但是如果这`m - j - 2`字符没有在模式串中重复出现,是否就不存在值得的对齐位置了呢?答案是否定的,因为此时的情形就类似`kmp`的情形了啊,一般地,如果模式串存在一个前缀,与子串`P[j+1, m)`的后缀相互匹配,那么这也是一个值得的对齐位置,如下图所示:
|
||||
|
||||

|
||||
|
||||
和`bc`算法和`kmp`算法一样,如果这样的对齐位置有多个,应该取出其中移动距离最短的一个,从而不会错过其他的对齐位置。并且仿照`bc`策略和`kmp`算法的思想,可以预先构造一个`gs`表,其中`gs[i]`表示在第`i`个位置比对失败后,按照`好后缀策略`应该采取的位移量。需要指出的是,`gs`表是只依赖于模式串`P`本身的,这是因为和`kmp`类似,文本串的相关字符已经全部和模式串匹配了。以下就主要讨论如何高效地构造`gs`表,而这个问题非常复杂,我只能尽量......
|
||||
|
||||
### gs表的构造
|
||||
|
||||
还是首先考虑蛮力策略吧,为了找到`gs`表中的任意一项,如`gs[i]`,根据`gs`表的语义,应该从位置`i`往前遍历整个模式串,直到出现上面讨论过的两种情况位置,其最坏情况下的时间复杂度为`O(m^2)`,因此蛮力算法构造`gs`表的时间复杂度为`O(m^3)`。而这里要介绍的一种`O(m)`构造`gs`表的策略,你就知道它有多难了。
|
||||
|
||||
> ss表
|
||||
|
||||
为了构造`gs`表,首先引入`ss`表的概念——`ss[i]`是表示在`P[0, i]`的所有后缀中,与`P`的某一后缀匹配的最长长度,即最长匹配后缀(maximum matched suffix)的长度。如下图所示:
|
||||
|
||||

|
||||
|
||||
如果可以成功地构造出`ss`表的话,就可以用`ss`表快捷地构造出`gs`表,因为`ss`表中包含了`gs`表中的全部信息。
|
||||
|
||||
> ss -> gs
|
||||
|
||||
这里先讨论如何通过`ss`表构造出`gs`表。
|
||||
|
||||
实际上,对应于上面提到的好后缀的两种情形,由`ss`表构造`gs`表也无非两种情况。第一种情况是`ss[j]`对应的最长匹配后缀延伸到了`P`的最左侧,此时有
|
||||
|
||||
```c
|
||||
ss[j] == j + 1;
|
||||
```
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
此时,对于模式串中所有的秩为`i`的字符,如果有`i < m - j - 1`,则`MS[j]`都是在该处匹配失败的一个候选对齐位置,对应了上面好后缀的第二种情形,此时它们的移动距离距离都是`m - j - 1`,即`m - j -1`必然是`gs[i]`的一个候选。需要指出的是,这种情形并不适用于`i >= m - j - 1`的情形,因为首位两个子串完全匹配,在该位置对齐后的下一次匹配必然会失败。
|
||||
|
||||
第二种情形是`ss[j]`是`P[0, j]`的一个真后缀,此时有
|
||||
|
||||
```c
|
||||
ss[j] < j + 1;
|
||||
```
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
在这种情况下,`MS[j]`只能作为在位置`m - ss[j] - 1`处比对失败的候选对齐位置。这是因为,假如`i > m - ss[j] - 1`,这里的情形与上面讨论的一致,两个子串完全匹配,在该位置对齐后的下一次匹配必然会失败;而假如`i < m - ss[j] - 1`,由于`ss[j]`的最值性,`MS[j]`的前一个字符必然与`P[m - ss[j] - 2]`不相等,因此这并不是一个有意义的对齐位置。综上,此时`m - j - 1`是`gs[m - ss[j] - 1]`的一个候选。
|
||||
|
||||
将上述两种情况进行综合,可以得到下面的通过`ss`表构造`gs`表的算法:
|
||||
|
||||
```c
|
||||
int* buildGS(char* P){
|
||||
int* ss = buildSS(P);
|
||||
int m = strlen(P);
|
||||
int* gs = new int[m];
|
||||
|
||||
//initialize
|
||||
for(int j = 0; j < m; ++j) gs[j] = m;
|
||||
|
||||
for(int i = 0, j = m - 1; j >= 0; --j)
|
||||
if(ss[j] == j + 1)
|
||||
while(i < m - j - 1) //double loop?
|
||||
gs[i++] = m - j - 1;
|
||||
|
||||
for(int j = 0; j < m - 1; ++j) //painter's algorithm
|
||||
gs[m - ss[j] - 1] = m - j - 1;
|
||||
|
||||
delete []ss;
|
||||
return gs;
|
||||
}
|
||||
```
|
||||
|
||||
需要对上面的算法做一些说明。可以看到,在构造`gs`表时,是使用画家算法,优先对`ss`的第一种情形进行判断,再使用第二种情形的结果来覆盖第一种情形。实际上,`ss`的第二种情形的确是优先于第一种情形的,可以证明,对于同一位置`i`,`ss`的第二种情形对应的位移量一定小于第一种情形,可以画个图自己看看(留作习题答案略,读者自证不难
|
||||
|
||||
然后对两种情形的两次循环,其方向是不一致的。第二个`for`循环(第一种情形的循环),对于每个位置`i`,是直接写入它的最短移动距离,因此是从右到左的循环。而第三个`for`循环(第二种情形的循环)由于是使用画家算法,需要不断覆盖之前的结果,所以是从左到右的循环,这样后写入的结果才是移动距离更短的。容易看出,由`ss`表构造`gs`表的算法,其时间复杂度只有`O(m)`,可以注意到其中是有一个二重循环的,但是由于`gs`表中的每个位置至多写入一次,因此该循环还是至多只会被执行`O(m)`次。
|
||||
|
||||
那么接下里的问题,就是如何构造`ss`表了!
|
||||
|
||||
> ss表的构造
|
||||
|
||||
首先还是先考虑一下如何通过蛮力来构造`ss`表,对于`ss`表中的每一项`ss[i]`,需要从该位置向前遍历,来找到一个最长匹配后缀,最坏情况下的时间复杂度为`O(m)`,因此蛮力算法的总体时间复杂度为`O(m^2)`。
|
||||
|
||||
下面介绍一种在`O(m)`时间内构造`ss`表的策略,这个策略连我邓公没有讲清楚,我就瞎写点东西......
|
||||
|
||||
这种策略的基本思路是,对于`ss[j]`,应该利用此前的构造`ss`的匹配信息,从而快速的更新当前的`ss[j]`。简单说来有两种情形:
|
||||
|
||||
第一种情形如下图所示:
|
||||
|
||||

|
||||
|
||||
在构造`ss`表的过程中,动态地保存和更新之前的极大匹配后缀,分别用`lo`和`hi`来表示它的范围,即`P(lo, hi]`。此时`j`位于`lo`和`hi`之间,因此就可以找到这样一个位置`m - hi + j - 1`,以这两个位置为后缀的子串,至少拥有`j - lo`个完全匹配的字符。第一种情形是
|
||||
|
||||
```c
|
||||
ss[m - hi + j - 1] <= j - lo
|
||||
```
|
||||
|
||||
此时,得益于`ss`的最值性,`P`的长度为`ss[m - hi + j - 1]`的后缀,必然是与`P[0, j]`匹配的最长匹配后缀,因此`ss[m - hi + j - 1`必然是`ss[j]`的最大取值,因此可以直接更新
|
||||
|
||||
```
|
||||
ss[j] = s[m - hi + j - 1];
|
||||
```
|
||||
|
||||
倘若不满足第一种情形的条件,即
|
||||
|
||||
```c
|
||||
ss[m - hi + j - 1] > j - lo
|
||||
```
|
||||
|
||||
则对应了这里的第二种情形,如下图所示:
|
||||
|
||||

|
||||
|
||||
在这种情况下,根据已有的信息,只能知道`P(lo, j]`与`P(m - hi + lo - 1, m - hi + j - 1]`是相互匹配的,因此`P(lo, j]`与`P(m + lo - j - 1, m - 1]`是相互匹配的,故`ss[j]`至少为`j - lo`。此时,`MS[j]`有可能继续向左侧扩展,因此需要依次对接下来的字符进行比对。此时将更新`hi = j`,并在一次比对成功后更新`lo`的值,即`--lo`,直到这样的比对失败,即可确定当前的`ss[j]`。
|
||||
|
||||
从这里也可以看出,`lo`和`hi`的更新是为了保证对接下来要进行确定字符,进行一个尽可能大的覆盖,而并非只是单纯地维护匹配后缀的最大值,以保证后面要遍历到位置,尽可能地处于`lo`和`hi`的包围中,从而可以应用这里的两种情形。
|
||||
|
||||
因此,可以形成下面构造`ss`表的代码:
|
||||
|
||||
```c
|
||||
int* buildSS(char* P){
|
||||
int m = strlen(P);
|
||||
int* ss = new int[m];
|
||||
ss[m - 1] = m;
|
||||
for(int lo = m -1, hi = m - 1, j = lo - 1; j >= 0; --j){
|
||||
if((lo < j) && (ss[m - hi + j - 1] <= j - lo)){//case one
|
||||
ss[j] = ss[m - hi + j - 1];
|
||||
}
|
||||
else{
|
||||
hi = j; lo = __min(lo, hi);
|
||||
while(( 0 <= lo) && (P[lo] == P[m - hi + lo - 1]))
|
||||
--lo;
|
||||
ss[j] = hi - lo;
|
||||
}
|
||||
}
|
||||
return ss;
|
||||
}
|
||||
```
|
||||
|
||||
可以注意到,上面的代码中也是含有两重循环,但是由于`lo`和`j`都至多减少到零,而每一次循环都会执行`--j`或者`--lo`,因此循环至多执行`O(m)`次,其时间复杂度仍然是`O(m)`。
|
||||
BIN
thu_dsa/chp11/buildss_case1.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
thu_dsa/chp11/buildss_case2.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
thu_dsa/chp11/gs_case1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
thu_dsa/chp11/gs_case2.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
thu_dsa/chp11/ss2gs_case1.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
thu_dsa/chp11/ss2gs_case2.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
thu_dsa/chp11/ss_definition.png
Normal file
|
After Width: | Height: | Size: 12 KiB |