前一段时间看了后缀数组,也是对着大佬们的博客研究了半天,有几个点理解了好久才明白(这就是弱者的世界吗_(:з」∠)_,再加上后来敲题时也遇到了一些坑,正好今天有时间所以写个博客来记录一下吧,方便日后写bug时查阅
本文算是自己的一篇简略的小笔记吧,如果想正经学习的话建议去看大佬们的博客哦,附上一个写的很好的链接:https://blog.csdn.net/yxuanwkeith/article/details/50636898
后缀数组求出来两个数组sa[i]
和rank[i]
,sa[i]
表示按字典序排序排名第i的后缀的起始位置下标,rank[i]
表示以第i位为起始位置的后缀的排名,两者互逆,还有一个神奇的数组height[i]
它表示的是排名第i的后缀和排名第i-1的后缀的LCP,有了这三个数组就可以解决大部分问题了
求法的话就是利用倍增(还有一个更优秀的DC3算法,但是好像很复杂的样子就没有看_(:з」∠)_
首先要注意的一点就是该算法涉及的数组最好都以下标为1作为数组的第一位也就是起始位,(鬼知道为什么我敲第一道题的时候以0为起始位置怎么改怎么wa,第二天以1为起始位置重新敲了一遍就莫名其妙的a了...
实现原理概述:用str
来表示整个字符串,我们先处理每个长度为1的子串str[i]
对应的sa
、rank
的信息,然后利用str[i]
和str[i+1]
的信息就可以更新str[i~i+1]
的信息,再然后利用str[i~i+1]
和str[i+2~i+3]
更新出str[i~i+3]
的信息...如此反复,最后每个str[i~n]
对应的sa
和rank
就得到了,排序的话因为是利用双关键字所以采用的较快的基数排序height[i]
的求法则是利用一个神奇的性质来加速:height[rank[i]]>=height[rank[i-1]]-1
,具体证明没有看,以后看了再补上吧(当然很大概率没有“以后”了2333
偷度娘一张经典图XD
直接上代码然后解释一些不太好懂的点吧,这也是本篇博客的目的所在
const int siz_n=100010; char str[siz_n]; //str[i]是存字符串的数组; int rnk[siz_n],bkt[siz_n]; //rnk[i]就是上述的rank[i],fir[i]是sa[i],lcp[i]是height[i],因为个人习惯给他们改了个名 int fir[siz_n],sec[siz_n]; //fir[i](即sa[i])为第一关键字,sec[i]为第二关键字 int lcp[siz_n]; //用桶排来实现基数排序,bkt[i]就是桶 void resort(int n,int m) //n为字符串长度,m为桶的容量 { for(int i=1;i<=m;i++) bkt[i]=0; //清空桶 for(int i=1;i<=n;i++) bkt[rnk[i]]++; //将每个子串的rank信息装桶 for(int i=1;i<=m;i++) bkt[i]+=bkt[i-1]; //求前缀和 for(int i=n;i>=1;i--) fir[bkt[rnk[sec[i]]]--]=sec[i]; //这里是最绕的,解释一下:因为从n到1遍历,sec[i]就是第二关键字的排名最靠后的下标 } //那么rnk[sec[i]]就是这个排名最靠后的下标的排名,放到这个语境中就可以理解为“桶的编号” //bkt之前是求过前缀和的,所以bkt[rnk[sec[i]]]就是这个子串的正确排名排名 //那么fir[bkt[rnk[sec[i]]]--]=sec[i]的含义就显而易见了 void solve(int n) { for(int i=1;i<=n;i++) rnk[i]=str[i],fir[i]=i; //这一步是初始化的,理解了算法含义之后就能看懂了 for(int l=0,m=127,p;l<n;l=l?l*2:1) //倍增l,l就是两个相邻小段子串的长度,更新完的子串长度为2*l { int top=0; for(int i=n-l+1;i<=n;i++) sec[++top]=i; //或许是写法问题吧(明明是单纯的菜,基数排序这一部分是看的最久的 for(int i=1;i<=n;i++) if(fir[i]>l) sec[++top]=fir[i]-l; //上一行和这一行就算是把第二关键字的先后顺序存在sec[i]里,fir[i]存的信息不会受影响 resort(n,m); //第一关键字通过resort()函数进行排序来更新fir[i] swap(sec,rnk); //sec[i]参与完基数排序就没用了(工具人,所以这里让它来暂时存一下rank方便一会根据sa更新rank数组的值 rnk[fir[1]]=p=1; //按照名次依次更新rank值,p是当前排到第几名了 for(int i=2;i<=n;i++) //不同子串的rank允许并列,因为并列了基数排序时它们才会进到同一个桶里 if(sec[fir[i]]==sec[fir[i-1]]&&sec[fir[i]+l]==sec[fir[i-1]+l]) rnk[fir[i]]=p; else rnk[fir[i]]=++p; //比较当前子串的两个关键字和它前一名子串的俩关键字一不一样,一样就是并列,名次也是p,反之++p m=p; //更新m,桶的容量 } for(int i=1,k=0;i<=n;i++) //按照神奇性质求height[i],不多解释 { if(k) k--; while(str[i+k]==str[fir[rnk[i]-1]+k]) k++; lcp[rnk[i]]=k; } } int main() { scanf("%s",str+1); int len=strlen(str+1); solve(len); return 0; }
有了这几个数组再结合st表之类的东西就可以愉快的a题啦
来源:https://www.cnblogs.com/JDZJBD/p/12571912.html