研究了好几天的后缀数组,今天终于是把代码实现给看明白了了。
大多数博客都讲解了了后缀数组以及倍增法,但是对于代码的讲解不是很明白。
这篇就把LRJ蓝书上的代码拆开来一句一句的进行解释。
关于倍增算法我建议去看lrj的数,写的非常清晰明了;基数排序可以去看看百度百科,其实和桶排序是一家子。
代码:
char s[maxn];
int sa[maxn],t[maxn],t2[maxn],c[maxn],n;
void print(int *arr){
for(int i=0;i<n;i++) cout<<arr[i]<<" ";
cout<<endl;
}
void getSa(int m){
int *x=t,*y=t2;
for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++) c[x[i]=s[i]]++;
for(int i=1;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--)
sa[--c[x[i]]]=i;
cout<<"sa:";print(sa);
cout<<"x:";print(x);
for(int k=1;k<=n;k<<=1){
int p=0;
for(int i=n-k;i<n;i++) y[p++]=i;
for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
cout<<"y:";print(y);
for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++)
c[x[y[i]]]++;
for(int i=0;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
cout<<"sa:";print(sa);
swap(x,y);
cout<<"y:";print(y);
p=1;x[sa[0]]=0;
for(int i=1;i<n;i++)x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
cout<<"x:";print(x);
if(p>=n) break;
m=p;
}
}
代码中为了便于调试增加了数组输出的函数;
首先我们必须搞懂每一个数组的意义和用途,这是非常重要的而且是书上所没有提及的。
不搞懂容易让人一头雾水。
sa | y | c | s | x |
后缀数组(第一关键字的名次) | 第二关键字的名次 | 基数排序用到的“桶” | 字符串 | 后缀数组的一个离散 |
上表用于在完全理解了几个数组的含义后对照使用,下面在深度的说一下这几个数组
sa:就是最后要得到的后缀数组,也就是第一关键字的名次,第一关键字的定义的蓝书上非常明了,可以参看书上。
sa[i]:排名为i的后缀的下标为sa[i]
x:很多人把x数组当做第一关键字的排序,我觉得这么理解的话容易让人产生误解。
我觉得把x理解为后缀的一个离散化数组更好。至于怎么个离散化,一会看代码;
比如第一遍倍增法的得到的后缀是这样的(书上例子):aa,ab,ba,aa,aa,aa,ab,b$;
那么对应的x数组为:0 1 3 0 0 0 1 2
x[i]:后缀i的离散化;
y:以第二关键字进行排序,排名第i名的后缀为y[i]
下面看具体代码:
先看这四行:
for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++) c[x[i]=s[i]]++;
for(int i=1;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;
第一行:桶清零,没啥好说的
for(int i=0;i<n;i++) c[x[i]=s[i]]++;
将s[i]赋值到x[i]中,并放入桶中。
此时x数组为:97 97 98 97 97 97 97 97 98;c数组为:c[97]=6;c[98]=2;其余为0
(97 97分别为a,b对应的ASC码值)
for(int i=1;i<m;i++) c[i]+=c[i-1];
加上前面桶的值,也就得到了这个值的排名;c[97]=6;c[98]=8;
for(int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;
这一行稍微难理解一点,再重申一下sa的定义:sa[i]:排名为i的后缀;
在上一行代码中,对应的桶中已经记录了对应“字符”的排名(字符离散到x数组中)
所以说每个排名对应的下面就全部放入到sa之中了;
eg:sa[--c[x[7]]=7->sa[--c[98]]=7->sa[7]=7 所以排第七的是后缀7
--c[x[i]]的意思是这个元素已经拿走了,下一次再取到和这个相同的元素的话排名会提前一个,所以要--
然后就是这个循环的顺序问题,他是从大到小的。想一下为什么,可以反过来吗?
提示一下这个c[i]的值是有关系的, 因为c[i]在取完之后是减一,这就保证了对于相同后缀,后面的排名更高。
然后看下面这堆代码
for(int k=1;k<=n;k<<=1){ //k是倍增的‘步数’
int p=0; //p在下面两行起一个下标的作用
for(int i=n-k;i<n;i++) y[p++]=i;
for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
cout<<"y:";print(y);
for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++) c[x[y[i]]]++;
for(int i=0;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
cout<<"sa:";print(sa);
swap(x,y);
cout<<"y:";print(y);
p=1;x[sa[0]]=0;
for(int i=1;i<n;i++)x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
cout<<"x:";print(x);
if(p>=n) break;
m=p;
}
开始进入了倍增法
看这两行
for(int i=n-k;i<n;i++) y[p++]=i;
for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
这两行是求y数组,第二关键字的排序,向上翻看y的定义并牢牢记住,非常重要,不要容易把自己绕进去;
第一行:后面k个后缀是没有第二关键字的,所以他们的排名在前面,参照课本很容易理解;
第二行:根据sa求y,也就是说根据第一关键字求第二关键字;
这很容易理解,也很容易想,当前位置的第一关键字,不就是前一位置的第二关键字嘛,对吧
根据代码实际动手构造一下会很容易理解;sa[i]>=k是因为前k个字符根本不能成为任何后缀的第二关键字;
sa[i]-k是因为当前下标是由后面下标提过来的,需要-k;(建议手动理解)
看下面四行代码
for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++) c[x[y[i]]]++;
for(int i=0;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
看上去和刚开始的四行一样,但是有有点不一样;
就是第二行和第四行的i换成了y[i],也可以这样理解,刚开始没有第二关键字,所以y就是从0开始递增的,也就是y[i]=i;
对于相同的不作过多的赘述
看这一行:
for(int i=0;i<n;i++) c[x[y[i]]]++;
那么x[y[i]]表示啥意思呢?就是一第二关键字为顺序组成的一个离散化的数组
这个数组第二关键字是有序的。
建议把这个数组写一遍,就能发现其含义;
再看第四行
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
从上面知道了,x[y[i]]的第二关键字是有序的,那么再把第一关键字排一下,那么两个不就都有序了吗
同样,顺序是从大到小的,原因同上;
到这里,比较难懂的地方就完成了,细节比较多,不容易一次性理解。
剩下的地方看着书很容易理解,代码也容易书写,就不做过多解释了。
来源:CSDN
作者:(羽翼)
链接:https://blog.csdn.net/weixin_40532377/article/details/104327562