upd.学了后缀自动机,感觉自己已经真香了:“得想办法放下我无敌的SAM”
0、闲扯
我学后缀数组的时候问过lzy一个问题,大概是一段代码的含义
lzy:"你会后缀自动机吗?"
我:???
果然lzy太强了啊
1、一些定义
\(sa[i]:\)字典序从小到大排名为i的后缀的位置
\(rnk[i]:\)以位置i开头的后缀的排名(与sa互逆)
\(tp[i]:\)基数排序辅助数组,第二关键字(第二关键字是什么后面会讲)排名为i的后缀的位置
\(tax[i]:\)基数排序辅助数组,一个桶
\(lcp(x,y):\)以位置x开头的后缀和以位置y开头的后缀的最长公共前缀
\(heght[i]:lcp(sa[i-1],sa[i])\)
\(h[i]:height[rnk[i]]\)
关于基数排序,看这里
2、后缀排序
后缀排序的方法,常见的有倍增/DC3/SA-IS,倍增是\(O(nlogn)\)的,DC3和SA-IS都是\(O(n)\)的,但很难写 其实只是你不会吧,所以这里只介绍倍增。
说到倍增,就不得不上那张经典的思路图:
可以发现一个从\(i\)开始,长度为\(2x\)的字字符串(记为\(S\)),是由从\(i\)开始,长度为\(x\)的字符串(记为\(S1\))和一个从\(i+x\)开始,长度为\(x\)的字符串组成的(记为\(S2\))。
此时\(S1\)的排名为\(S\)的第一关键字(图中x),\(S2\)的排名为\(S\)的第二关键字(图中y),得到\(S\)对应的二元组\((x,y)\)。再对\(n\)个这样的二元组进行排序得到新一轮的\(sa\),进而求出\(rnk\)。这么做正确性显然。
如果排序用快排的话,时间复杂度为\(O(nlogn^2)\),不如写个哈希。考虑到只是对二元组进行排序,基数排序的时间复杂度为\(O(n)\),时间复杂度为\(O(nlogn)\),非常优秀。
上一下基数排序的代码
void Qsort(){
int i;
for(i=0;i<=sz;++i) tax[i]=0;
//清空桶,桶的大小为上轮的排名的个数
for(i=1;i<=l;++i) tax[rnk[i]]++;
//记录第一关键字
for(i=1;i<=sz;++i) tax[i]+=tax[i-1];
//前缀和,用于快速定位
for(i=l;i>=1;--i) sa[tax[rnk[tp[i]]]--]=tp[i];
//从大到小枚举第二关键字,再定位回第一关键字,得出排名
}
然后是整个后缀排序的代码:
void Qsort(){
int i;
for(i=0;i<=sz;++i) tax[i]=0;
for(i=1;i<=l;++i) tax[rnk[i]]++;
for(i=1;i<=sz;++i) tax[i]+=tax[i-1];
for(i=l;i>=1;--i) sa[tax[rnk[tp[i]]]--]=tp[i];
}
inline bool cmp(int x,int y,int w){ return (tp[x]==tp[y])&&(tp[x+w]==tp[y+w]); }//二元组相同,排名相同
void SufSort(){
int i,x;
sz=75;
for(i=1;i<=l;++i) rnk[i]=s[i]-'0'+1,tp[i]=i;
Qsort();
for(x=1;x<=l;x<<=1){
int num=0;
for(i=l-x+1;i<=l;++i) tp[++num]=i;
//没有第二关键字
for(i=1;i<=l;++i) if(sa[i]>x) tp[++num]=sa[i]-x;
//从i-x开始的后缀的第二关键字为从i开始的后缀的第一关键字,联系tp和sa含义理解吧qwq
Qsort();
memcpy(tp,rnk,sizeof(tp));
//tp没啥用了,存上一轮的rnk以获得新一轮的rnk
rnk[sa[1]]=1;
for(i=2;i<=l;++i) rnk[sa[i]]=rnk[sa[i-1]]+(!cmp(sa[i],sa[i-1],x));
if(rnk[sa[l]]==l) break;
sz=rnk[sa[l]];
}
}
3、height数组
忘记\(height/h\)定义的请自行往回翻 = =
关于\(h\)数组,有\(h[i]\geq h[i-1]-1\)
证明引自远航之曲大佬(大佬博客好像挂了QwQ)
能够线性计算\(height[]\)的值的关键在于\(H[]\) \((height[rank[]])\)的性质,即\(h[i] \geq h[i-1]-1\),下面具体分析一下这个不等式的由来。
我们先把要证什么放在这:对于第i个后缀,设\(j=sa[rank[i] – 1]\),也就是说j是i的按排名来的上一个字符串,按定义来i和j的最长公共前缀就是\(height[rank[i]]\),我们现在就是想知道\(height[rank[i]]\)至少是多少,而我们要证明的就是至少是\(height[rank[i-1]]-1\).
好啦,现在开始证吧。。
首先我们不妨设第i-1个字符串(这里以及后面指的“第?个字符串”不是按字典序排名来的,是按照首字符在字符串中的位置来的)按字典序排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。
这时,依据\(height[]\)的定义,第k个字符串和第i-1个字符串的公共前缀自然是\(height[rank[i-1]]\),现在先讨论一下第k+1个字符串和第i个字符串的关系。
第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为\(height[rank[i-1]]\)就是0了呀,那么无论\(height[rank[i]]\)是多少都会有\(height[rank[i]]>=height[rank[i-1]]-1\),也就是\(h[i]\geq h[i-1]-1\)。
第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面,要么就产生矛盾了。同时,第k个字符串和第i-1个字符串的最长公共前缀是\(height[rank[i-1]]\),那么自然第k+1个字符串和第i个字符串的最长公共前缀就是\(height[rank[i-1]]-1\)。
到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的字典序排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即\(sa[rank[i]-1]\)。也就是说\(sa[rank[i]]\)和\(sa[rank[i]-1]\)的最长公共前缀至少是\(height[rank[i-1]]-1\),那么就有\(height[rank[i]]\geq height[rank[i-1]]-1\),也即\(h[i]\geq h[i-1]-1\)。
然后就可以愉快地写出代码求啦
void GetHeight(){
int i,k=0;
for(i=1;i<=l;++i){
if(k) k--;
int j=sa[rnk[i]-1];
while(s[i+k]==s[j+k]) k++;
height[rnk[i]]=f[rnk[i]][0]=k;
}
}
有了height可以干很多很多事,由于现在题做得不是很多还不能讲QwQ,题单和套路什么的先咕咕咕了