前排提醒:坑未填完。【如果你看到这句话,请提醒博主填坑】
国家集训队2009论文——罗穗骞
Hihocoder关于后缀数组的讲解
比较详细的一篇博客
【以及刘汝佳的小蓝书 和 我们学校内部的课件】
【这里我们总假设数组/字符串的起始下标为 0,终止下标为 len-1】
定义 suffix(i) 为从 i 开始到串尾的后缀,称为“后缀i”。
将字符串的所有后缀按字典序排序,可以得到这个字符串的后缀数组 sa(suffix array)。其中sa[i] 表示第 i 小的后缀为 suffix(sa[i])。
名次数组 rank 是 sa 的逆运算,即rank[sa[i]] = i,表示 suffix(i) 在所有后缀中的排名。因为长度不同的字符串不可能有相同的字典序,所以任意后缀的名次不并列。
【通俗地讲:sa[i] 表示排第 i 位的是谁,rank[i] 表示 i 排第几】【请一定要记住定义!!!】
假定某个字符串 S = “aabaaaab”。在它的末尾加入一个字符串中没有出现的字符比如”#”(等会儿我们解释为什么要加入尾字符)(原本应该是“$”来着……但是markdown会识别出来奇怪的东西)下面是它的sa和rank。
(UPD:原来还有转义字符这玩意儿……是博主low了)。
(假设 # 的优先级比字母小……哦不好像字典序的确 # 的优先级比字母小所以用不着假设233)
看到这里,我们大概可以瞎BB一个算法出来了:直接用sort进行排序。一共 n 个后缀,因此总复杂度 O(nlog2n) O ( n l o g 2 n ) 。因此,本算法被在合理时间复杂度内完美解决……?
注意到字符串之间的比较与字符串长成正比,所以算法复杂度将会是 O(n2log2n) O ( n 2 l o g 2 n ) 。平凡的 n 个字符串进行排序的复杂度是 O(n2log2n) O ( n 2 l o g 2 n ) 的(UPD:其实用字符串哈希可以得到一个O(nlog^2n)字符串排序算法来着,但是毕竟哈希的正确性并不稳定……),但是因为后缀之间是有联系的,所以我们可以设计出一个更优秀的算法:
该算法基于这样一个事实:两个字符串的比较,如果前半部分相同则比较结果决定于后半部分的比较结果,否则比较结果决定于前半部分的比较结果。
对于字符串 S S ,定义k-前缀为:
Sk=S[0...k−1](k<=lenS) S k = S [ 0... k − 1 ] ( k <= l e n S )
Sk=S(k>lenS) S k = S ( k > l e n S )
然后,我们令 sak[i] s a k [ i ] 为k-前缀意义下的后缀数组, rankk[i] r a n k k [ i ] 为k-前缀意义下的名次数组。
一、容易求出 rank1 r a n k 1 与 sa1 s a 1 的值,只需要sort排序一次即可。
二、如果我们求出了 sak s a k ,我们可以很快的求出 rankk r a n k k :
rankk[i]=rankk[i−1](suffix(sa[i])k=suffix(sa[i−1])k) r a n k k [ i ] = r a n k k [ i − 1 ] ( s u f f i x ( s a [ i ] ) k = s u f f i x ( s a [ i − 1 ] ) k )
rankk[i]=rankk[i−1]+1(suffix(sa[i])k≠suffix(sa[i−1])k) r a n k k [ i ] = r a n k k [ i − 1 ] + 1 ( s u f f i x ( s a [ i ] ) k ≠ s u f f i x ( s a [ i − 1 ] ) k )
三、然后,假如说我们求出了 rankk r a n k k ,则定义二元组 (rankk[i],rankk[i+k]) ( r a n k k [ i ] , r a n k k [ i + k ] ) 。以“ rankk[i]相等,则比较rankk[i+k],否则比较rankk[i] r a n k k [ i ] 相 等 , 则 比 较 r a n k k [ i + k ] , 否 则 比 较 r a n k k [ i ] ”的排序规则进行sort排序,即可求出 sa2k s a 2 k 。
也就是说算法流程是这样的:
什么时候停止呢?当不存在并列的rank的时候,所有后缀的大小关系就会被定义出来,这时候就可以停止算法了。
(图片来源百度百科)
实际上第三次排序完就可以停止算法了。
每一次排序需要 nlog2n n l o g 2 n 次比较,每一次比较需要 O(1) O ( 1 ) 的时间复杂度,因此总时间复杂度为 O(nlog22n) O ( n l o g 2 2 n )
【快排我就不给参考代码了大家直接理解桶排序的代码好嘛】【怠惰的博主】
O(nlog22n) O ( n l o g 2 2 n ) 的时间复杂度尽管很好,但还是不够优秀。
咱们先从后缀数组前走开,探究这样一个问题:
假设有 n 个数进行排序,则排序时间复杂度下限为 O(nlog2n) O ( n l o g 2 n ) 。
但假设保证这n个数都是两位数,我们可以做到 O(n) O ( n ) 的算法。怎么做?看一下下面的例子。
虽然实际运用中数据不可能总是两位数,但是假如我们把两位数看作一个二元组,十位看作第一关键字,个位看作第二关键字,并保证二元组的元素在能够存储的范围内,我们就可以实现对二元组的排序了。这种排序被形象地称为“桶排序”(基数排序)
我们先对字符串的元素进行离散化,元素的值域就变成了[0…n-1]。且离散化后我们发现 rank1[i]=S[i] r a n k 1 [ i ] = S [ i ] ,一举双得。然后每一次迭代中,不难发现 rank r a n k 和 sa s a 都不会超过 n−1 n − 1 (想想它们所代表的含义)。
于是得出结论:我们可以用基数排序代替每一轮循环中的sort。
一共进行 log2n l o g 2 n 次排序,每次排序遍历n个元素,n个桶,总时间复杂度 O(nlog2n) O ( n l o g 2 n )
【后缀数组还存在 O(n) O ( n ) 的构造方法 dc3 d c 3 ,但代码相对复杂。因为对于大多数题来说 O(nlog2n) O ( n l o g 2 n ) 的复杂度已经足够了,所以此处不再讨论。有兴趣的读者可以去游览一下集训队大佬的论文orz(其实是我自己没学懂233)】
参考实现如下(作者根据自己的理解所写的代码,与网上大部分代码不大相同,但是作者表示他觉得效率差不多)(因为网上的代码作者也看不懂233):
int arr[MAXN + 5], rnk[MAXN + 5], nrnk[MAXN + 5];
int bin[MAXN + 5], nxt[MAXN + 5], sa[MAXN + 5];
void InsertBIN(int x, int k) {
nxt[x] = bin[k];
bin[k] = x;
}
void GetSAFromBIN(int n) {
int cnt = 0;
for(int i=0;iwhile( bin[i] != -1 ) {
sa[cnt++] = bin[i];
bin[i] = nxt[bin[i]];
}
}
}
void BuildSA(int n) {
for(int i=0;ifor(int i=0;i1;
for(int i=0;ifor(int k=1;rnk[sa[n-1]]!=n-1;k<<=1) {
for(int i=n-1;i>=0;i--)
if( sa[i] >= k )
InsertBIN(sa[i]-k, rnk[sa[i]-k]);
for(int i=n-k;ifor(int i=0;i0]] = 0;
for(int i=1;iint del = (nrnk[sa[i]] != nrnk[sa[i-1]]) || (nrnk[sa[i]+k] != nrnk[sa[i-1]+k]);//注1
rnk[sa[i]] = rnk[sa[i-1]] + del;
}
}
}
【注1:这里sa[i]+k会访问到非法内存吗?不会。假如说 nrnk[sa[i]]==nrnk[sa[i−1]] n r n k [ s a [ i ] ] == n r n k [ s a [ i − 1 ] ] ,则两个后缀的前半段一定不包含尾字符“#”,所以不会访问出界。否则因为C++短路运算符的特性,后面的语句不会执行】
【注2:实际上尾字符不一定要是“#”,让字符串长度++,字符串末尾就会多出现‘\0’这个字符,充当了尾字符的作用】
光有后缀数组并没有用的,我们还要借助它解决这样一个问题:LCP(Longest Common Prefix,最长公共前缀)。LCP的应用范围就非常广泛了,详情可以见下面的例题。
怎么解决呢?我们令 LCP(i,j)=lcp(suffix(i),suffix(j)) L C P ( i , j ) = l c p ( s u f f i x ( i ) , s u f f i x ( j ) ) ,并令 height[i]=LCP(sa[i],sa[i−1]) h e i g h t [ i ] = L C P ( s a [ i ] , s a [ i − 1 ] ) 。对于满足 rank[i]<rank[j] r a n k [ i ] < r a n k [ j ] ,有这样一个结论:
LCP(i,j) L C P ( i , j )
=min(height[rank[i]+1],height[rank[i]+2],...height[rank[j]]) = m i n ( h e i g h t [ r a n k [ i ] + 1 ] , h e i g h t [ r a n k [ i ] + 2 ] , . . . h e i g h t [ r a n k [ j ] ] )
=RMQ(height,rank[i]+1,rank[j]) = R M Q ( h e i g h t , r a n k [ i ] + 1 , r a n k [ j ] )
这个结论的证明参考了上面提到的那篇博客,大家可以去看看那一位dalao的详细证明思路。证明如下:
后缀数组要发挥威力,还必须依靠一个很重要的结论:
证明大家可以看看论文或者博客。我这里让大家【感性认知】一下。
下面是一个例子,大家可以对照着验证上面两个结论。
下面是博主求height数组的参考实现代码。
void CalHeight(char *arr, int n) {
int k = 0;
for(int i=0;iif( rnk[i] == 0 ) height[rnk[i]] = 0;
else {
if( k ) k--;
int j = sa[rnk[i]-1];
while( arr[i+k] == arr[j+k] )
k++;
height[rnk[i]] = k;
}
}
}
暂缺,待填坑。
UPD in 2018/8/12:对讲解部分进行了完善修改。
UPD in 2018/8/12:我想了想,例题大家还是看看dalao的论文吧,里面的例题非常丰富来着。如果之后我有遇到什么后缀数组的题的话,就加在这个地方