后缀数组学习笔记,待续

看了好久,,,简单挤一挤


例题:读入一个长度为n的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为1到n。

先看一下哈希加二分。。注意细节。。只有70分。。因为复杂度(n*log²n),1e6要T。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int maxn=1e6+10;
const ull base=131;
char a[maxn];
ull Hash[maxn],power[maxn];
int n,sa[maxn];
inline void Hash_work(){
	Hash[n+1]=0,power[0]=1;
	for(int i=n;i>=1;--i){
		sa[i]=i,power[n-i+1]=power[n-i]*base;
		Hash[i]=Hash[i+1]*base+a[i];
	}
}
inline ull Get_Hash(int l,int r){return Hash[l]-power[r-l+1]*Hash[r+1];}
inline bool cmp(int L,int R){
	int l=0,r=min(n-L+1,n-R+1);
	//二分lcp的长度
	while(l<r){
		int mid=(l+r+1)>>1;
        //看前mid个是否相同
		if(Get_Hash(L,L+mid-1)==Get_Hash(R,R+mid-1)) l=mid;
		else r=mid-1;
	}
	return a[L+l]<a[R+l];
}
inline void print(int x){
	if(x>9) print(x/10);
	putchar(x%10+'0');
}
int main(){
	scanf("%s",a+1),n=strlen(a+1),Hash_work();
	sort(sa+1,sa+n+1,cmp);
	for(int i=1;i<=n;++i)
		print(sa[i]),putchar(' ');
}

后缀数组

后面要出现的数组含义如下:

suf[i]:假设原串为S[1~n],那么suf[i]就代表S[i~n],也就是从i开始的后缀。

sa[i]:排名为i的后缀的位置。

rank[i]:位置为i的后缀的排名。

height[i]:suf[sa[i]]和suf[sa[i-1]]的最长公共前缀的长度。(注意,height数组的下标代表排名!)

关于如何求sa[i],rank[i],有个方法叫倍增,这里倍增字符串的长度。

假设原串长度为n。我们从长度为1开始倍增。最开始,我们有n个长度为1的字符串。

然后比较出它们的大小(sort),然后我们得到了它们的大小关系。

然后考虑长度为2。对于最后一个,在它后面补一个0就行了。类似的,后面长度不够的都补0,不会影响大小的比较。

现在我们有n个长度为2的字符串。

那么这个时候比较两个子串的大小就可以这样:把两个字符串平都分成两份。先看这两个字符串的前一半,这两个一半的长度为1,那么它们的大小关系是已知的。如果现在比较出了这两个一半的大小,那么也就比出了这两个长度为2的子串的大小。如果它们的前一半相同,那么就看它们的后一半。

以此类推。

结合图解释一下:a和b是原串的两个子串。它们的长度相同。a1,a2是a的前一半和后一半,b1,b2是b的前一半和后一半。

若a1>b1,则a>b。

若a1=b1:若a2<b2,则a<b。若a2=b2,则a=b。若a2>b2,则a>b。

若a1<b1,则a<b。

这个时候一次比较就是O(1)的。

比较出长度为2^k(k∈[0,logn])的所有子串的大小需要O(n logn),一共要比较log n次,那么总的复杂度就是O(n log²n)。

可以用基数排序再优化优化。

每次比较需要两个关键字。可以看做是个两位数。仿照基数排序的思想。

先把所有数丢到个位桶里。举个例子:

比如有几个数:61,35,11,21,15,23。

(这里的遍历都假设为:先进去的先遍历,后进去的后遍历)

现在有10个桶,分别为0~9,代表个位为0~9。我们把这些数丢进去之后再把每个桶(按个位从小到大)遍历一下:

个位为1:{61,11,21}    个位为3:{23}    个位为5:{35,15}    (空的桶懒得写了)

遍历一遍:61,11,21,23,35,15

我们发现,这些数遍历完后,它们的个位一定是从小到大排的。

然后我们把它们丢到十位桶里。

丢进十位桶里的顺序按照刚才遍历出来的顺序:61,11,21,23,35,15

十位为1:{11,15}    十位为2:{21,23}   十位为3:{35}    十位为6:{61}    然后就发现它们有序了。为什么呢?

对于一个单独的十位的桶,它们的十位都是一样的。它们的大小比较只看个位。然而我们之前把这些数丢进去的时候是保证了整个序列的个位是从小到大排的。那么先丢进去的数的个位就小,后丢进去的数的个位就大。那么对于任意一个桶,我们去遍历它的时候,就可以保证先遍历到的数的个位就小,后遍历到的数的个位就大。那么对于这个桶,遍历完之后,得到的序列就是从小到大排列的。

然后十位从1~9遍历,由于每个桶遍历完都是从小到大的,那么得到的总序列就肯定是从小到大排的了。

然而这里的两位数代表的是一个二元组,它的两个位置不一定是0~9。但是总而言之,思想是一样的。

参考博客

贴个板子。抄过来的。参考博客里解释很详细。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
char s[maxn];
int n,m=200,num;
int cnt[maxn],x[maxn],y[maxn],sa[maxn];
inline void print(int x){
	if(x>9) print(x/10);
	putchar(x%10+'0');
}
inline void Get_Sa(){
	for(int i=1;i<=n;++i) ++cnt[x[i]=s[i]];
	for(int i=2;i<=m;++i) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--) sa[cnt[x[i]]--]=i;
	
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n-k+1;i<=n;++i) y[++num]=i;
		for(int i=1;i<=n;++i) if(sa[i]>k) y[++num]=sa[i]-k;
		memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;++i) ++cnt[x[i]];
		for(int i=2;i<=m;++i) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;--i) sa[cnt[x[y[i]]]--]=y[i],y[i]=0;
		
		swap(x,y),x[sa[1]]=1,num=1;
		for(int i=2;i<=n;++i) x[sa[i]]=((y[sa[i]]==y[sa[i-1]])&&(y[sa[i]+k]==y[sa[i-1]+k]))?num:(++num);
		if(num==n) break;
		m=num;
	}
	
	for(int i=1;i<=n;++i) print(sa[i]),putchar(' ');
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	Get_Sa();
}

猜你喜欢

转载自blog.csdn.net/g21wcr/article/details/84667115
今日推荐