后缀数组代码细节个人理解
后缀数组代码细节的理解(笔记)
在阅读蓝书后缀数组代码时,几个细节思考了好几天才理解。看懂后将自己的个人理解写出来,大佬勿喷.....
模板代码
// O(nlogn) 预处理
string r;
int SA[maxn], WS[maxn], x[maxn], y[maxn];
void build_SA(int m) //m为 ASCII 最大值代表循环边界
{
int n = r.size();
for(int i=0; i<m; ++i) WS[i] =0;
for(int i=0; i<n; ++i) WS[ x[i] = r[i] ]++;
//n = r.size() x[] 是対r 的ascii码呈现
//for(int i=0;i<n;++i) printf("%d", x[i]); printf("\n");
//for(int i='a';i<'z';i++) printf("i = %d, WS[i] = %d\n", i, WS[i]);
for(int i=1; i<m; i++) WS[i] += WS[i-1];
for(int i=n-1; i>=0; i--) SA[-- WS[x[i]] ] = i; //x[i] 和之前的字符共出现 WS[x[i]] 次
for(int k=1; k<=n; k<<=1)
{
int p = 0; //Y存放长度为2j的字符串第二关键字排序结果,现在将第二关键字为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; //基数排序第二关键字
for(int i=0; i<m; i++) WS[i] =0;
for(int i=0; i<n; i++) WS[x[y[i]]] ++; //y[i]为第二关键字排序后排名的位置,x[y[i]]根据第二关键字排名位置找到第一关键字,WS[x[y[i]]] 为第一关键字统计。
for(int i=0; i<m; i++) WS[i] += WS[i-1];
for(int i=n-1; i>=0; --i) SA[-- WS[x[y[i]]]] = y[i];
swap(x, y);//根据 SA 和 Y 计算新的x数组
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++; //y为上阶段x数组。当k == 1 时 ,首尾两个字符分别比较确保相同。 //当k > 1时 y为上阶段 k 个字符的x 排序,比较y[i]相当于比较k个字符
if(p>=n) break;
m = p;
}
}
基数排序
for(int i=0; i<n; ++i) WS[ x[i] = r[i] ]++;
for(int i=1; i<m; i++) WS[i] += WS[i-1];
for(int i=n-1; i>=0; i--) SA[-- WS[x[i]] ] = i;
其中WS[]为第一关键字的统计量,第二个循环将统计第i个位置的字母的个数,和之前的比它小的字母个数总量。
当在第三个循环时 SA[-- WS[x[i]] ] = i,当循环到位置i时,该位置的字母以及之前比它小的字母个数即为它的排名。
-- WS[x[i]] 操作不仅是因为字符串从0开始,同时也是因为i位置字母以及被计算过因此统计量要减一。
下面表格为对应的排序名次。
WS统计
| a | b |
|---|---|
| 5 | 7 |
排序后结果。第三行为对应-- WS[x[i]]操作后结果,同时每次--为下一个相同字母的排序。
| a | a | b | a | a | a | a | b |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 6 | 2 | 3 | 4 | 5 | 7 |
| -1 | 1 | 5 | 1 | 2 | 3 | 4 | 6 |
循环中排序第二关键字
int p = 0; //Y存放长度为2j的字符串第二关键字排序结果,现在将第二关键字为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; //基数排序第二关键字
y[]表示第二关键字的第几名在第一关键字的位子。如y[4] = 0表示字符串首位a对应的第二关键字排序为4.
这是根据先前的排序SA[]得到对应的位置 向前寻找k位即为第一关键字对应的位置的第二关键字的排序。
第一个循环从n-k位开始,此时向后K位第二关键字为0(没有第二关键字),每个位置从0开始增加。为了便于表示假设k = 4 结果为
| a | a | b | a | a | a | a | b |
|---|---|---|---|---|---|---|---|
| 4 | 5 | 6 | 7 | ||||
| 0 | 1 | 2 | 3 |
第二个循环当SA[i] < k时向前寻找k位无法找到第一关键字,因此跳过。当i = 2找到SA[2] = 6,第一关键字排序为第二位的在6, 因此向前搜索4位SA[i] - k = 2即第二关键字的p++(4)名对应的第一关键字位置为2.
| a | a | b | a | a | a | a | b |
|---|---|---|---|---|---|---|---|
| 2 | 4 | 5 | 6 | 7 | |||
| 4 | 0 | 1 | 2 | 3 |
循环结束对应结果为
| a | a | b | a | a | a | a | b |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 5 | 6 | 4 | 7 | 0 | 1 | 2 | 3 |
| 此时根据上一次排序的结果排序第二关键字已经完成了 |
根据y[]対第一关键字进行基数排序
for(int i=0; i<n; i++) WS[x[y[i]]] ++; //y[i]为第二关键字排序后排名的位置,x[y[i]]根据第二关键字排名位置找到第一关键字,WS[x[y[i]]] 为第一关键字统计。
for(int i=0; i<m; i++) WS[i] += WS[i-1];
for(int i=n-1; i>=0; --i) SA[-- WS[x[y[i]]]] = y[i]; //根据 SA 和 Y 计算新的x数组
前面由y[]相当与已经将第二关键字排序完毕得到结果为:
R[]代表对应位置的排序。 R[SA[i]] = i;
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| r | a | a | b | a | a | a | a | b |
| SA | 0 | 1 | 6 | 2 | 3 | 4 | 5 | 7 |
| R | 0 | 1 | 3 | 4 | 5 | 6 | 2 | 7 |
| Y | 5 | 6 | 4 | 7 | 0 | 1 | 2 | 3 |
| 排序 | 50 | 60 | 20 | 70 | 32 | 05 | 16 | 37 |
y[i]得知对应的第一关键字的位置,x[y[i]]得到对应位置的第一关键字的内容,WS[x[y[i]]] ++则是対第一关键字数量的统计。 |
||||||||
SA[-- WS[x[y[i]]]] = y[i] 举个例子当 i = 7 ,就是将第二关键字最后一位37取出排序,y[7] = 3, x[3] = 'a', SA[--WS['a']] = y[7] = 3.排名为5的位置为3. |
从新计算X数组
swap(x, y);
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++;
此时计算得出的x数组表示的是长度为2*k的字符串在相应位置对应的排名,同时将相同的字符串排名也相同。
当k = 1时 比较 y[SA[i]] y[SA[i-1]], y[SA[i]+1] y[SA[i-1]+1]判断字符串是否相同。
当k > 1时 y[]为上一轮的长度为k字符串的对应X数组,每一个y[i]对应的字符串长度为k,因此也只需比较两个位置即可。
浙公网安备 33010602011771号