重复旋律:后缀数组+后缀自动机

空の音响け、高く哀しみを越えて
今ここに生きてること 笑い合えるその日まで
优しさも梦もここに留めておけない
消えてゆく光の中 明日を奏でて

やがて君の手が掴む永久の真実
かなわないと思うから
いっそ高らかな声で
その歌に君は希望と名付けて泣いた
梦见る人の心に 确かに届くよ

空之音嘹亮的回响 高贵地越过那些悲伤
你眼中所映出的一切 就是这真实的世界
连泪水都无法让你留在这里
在倾泻而下的光辉中 奏响明天

你的手即将掌握永恒的真实
若注定无法企及
索性以宏亮的声音奏出
那首你哭着命名为希望的歌
它将确实地传达到 梦中人的心里

——《光の旋律》

后缀数组(SA)

int n,s[MAXN];
int siz,sa[MAXN],rk[MAXN<<1],sc[MAXN<<1],bk[MAXN],ht[MAXN];

inline void radixsort(){
	rin(i,1,siz) bk[i]=0;
	rin(i,1,n) bk[rk[i]]++;
	rin(i,2,siz) bk[i]+=bk[i-1];
	rec(i,n,1) sa[bk[rk[sc[i]]]--]=sc[i];
}

inline void suffixsort(){
	siz=100;
	rin(i,1,n){rk[i]=s[i];sc[i]=i;}
	radixsort();
	for(int wd=1;;wd<<=1){
		int cnt=0;
		rin(i,1,wd) sc[++cnt]=n-wd+i;
		rin(i,1,n) if(sa[i]>wd) sc[++cnt]=sa[i]-wd;
		radixsort();
		std::swap(rk,sc);
		rk[sa[1]]=cnt=1;
		rin(i,2,n) rk[sa[i]]=(sc[sa[i-1]]==sc[sa[i]]&&sc[sa[i-1]+wd]==sc[sa[i]+wd])?cnt:++cnt;
		if(cnt==n) return;
		siz=cnt;
	}
}

inline void getheight(){
	int preh=0;
	rin(i,1,n){
		if(rk[i]==1){preh=0;continue;}
		int now=std::max(preh-1,0),pre=sa[rk[i]-1];
		while(s[pre+now]==s[i+now]) now++;
		ht[rk[i]]=preh=now;
	}
}

inline void buildst(){
	rin(i,1,n) st[0][i]=ht[i];
	int lim=log2(n);
	rin(i,1,lim){
		int len=(1<<i);
		rin(j,1,n-len+1){
			st[i][j]=std::min(st[i-1][j],st[i-1][j+(len>>1)]);
		}
	}
}

inline int lcp(int x,int y){
	if(x==y) return n-x+1;
	x=rk[x],y=rk[y];
	if(x>y) std::swap(x,y);
	x++;
	int lim=log2(y-x+1);
	return std::min(st[lim][x],st[lim][y-(1<<lim)+1]);
}

int main(){
	n=read();
	scanf("%s",s+1);
	suffixsort();
	getheight();
	buildst();
	...;
	return 0;
}

后缀自动机(SAM)

int n,las,tot;
struct Sam{
	int fa,to[26];
	int len;
}sam[MAXN<<1];

inline void extend(int c){
	int p=las,np=++tot;las=np;sam[np].len=sam[p].len+1;
	while(p&&!sam[p].to[c]){sam[p].to[c]=np;p=sam[p].fa;}
	if(!p){sam[np].fa=1;return;}
	int q=sam[p].to[c];
	if(sam[p].len+1==sam[q].len){sam[np].fa=q;return;}
	int nq=++tot;sam[nq]=sam[q];sam[nq].len=sam[p].len+1;sam[np].fa=sam[q].fa=nq;
	while(p&&sam[p].to[c]==q){sam[p].to[c]=nq;p=sam[p].fa;}
}

int main(){
	n=read();
	scanf("%s",s+1);
	las=tot=1;
	rin(i,1,n) extend(s[i]-'a');
	...;
	return 0
}

想学的话可以上OI Wiki。

hihoCoder#1403 : 后缀数组一·重复旋律

题意

一段旋律中出现次数至少为\(K\)次的旋律最长是多少?

分析

二分答案后挖去\(height[i]<mid\)的位置,检查有没有长度超过\(k\)的连续段。

代码片段

inline bool check(int mid){
	int temp=0;
	rin(i,2,n){
		if(ht[i]>=mid) temp++;
		else{
			if(temp>=k-1) return 1;
			temp=0;
		}
	}
	return temp>=k-1;
}

int main(){
	n=read(),k=read();
	rin(i,1,n) s[i]=read();
	suffixsort();
	getheight();
	int l=0,r=n,ans;
	while(l<=r){
		int mid=((l+r)>>1);
		if(check(mid)) ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",ans); 
	return 0;
}

hihoCoder#1407 : 后缀数组二·重复旋律2

题意

一段旋律中出现次数至少为两次的不重叠旋律最长是多少?

分析

还是二分答案后挖去\(height[i]<mid\)的位置,然后检查每个连续段内最小的位置和最大的位置是否符合要求。

代码片段

inline bool check(int mid){
	int minpos=sa[1],maxpos=sa[1];
	rin(i,2,n){
		if(ht[i]>=mid){
			minpos=std::min(minpos,sa[i]);
			maxpos=std::max(maxpos,sa[i]);
		}
		else{
			if(minpos+mid-1<maxpos) return 1; 
			minpos=maxpos=sa[i];
		}
	}
	return minpos+mid-1<maxpos;
}

int main(){
	n=read();
	rin(i,1,n) s[i]=read();
	suffixsort();
	getheight();
	int l=0,r=n,ans;
	while(l<=r){
		int mid=((l+r)>>1);
		if(check(mid)) ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",ans);
	return 0;
}

hihoCoder#1415 : 后缀数组三·重复旋律3

题意

两部作品的共同旋律最长是多少?

SA做法

分析

把两个串用特殊字符粘在一起,求一个SA和\(height\)。然后扫一遍\(height\)数组,如果排名相邻的两个后缀属于不同的串,那么用它们的\(height\)更新\(ans\)

代码片段

inline void solve(){
	rin(i,2,n){
		int u=sa[i-1]-divpos,v=sa[i]-divpos;
		if(u*v>=0) continue;
		ans=std::max(ans,ht[i]);
	}
}		

int main(){
	scanf("%s",str+1);
	n=strlen(str+1);
	rin(i,1,n) s[i]=str[i]-'a'+2;
	s[++n]=1;
	divpos=n;
	getchar();
	scanf("%s",str+1);
	int nn=strlen(str+1);
	rin(i,n+1,n+nn) s[i]=str[i-n]-'a'+2;
	n+=nn;
	suffixsort();
	getheight();
	solve();
	printf("%d\n",ans);
	return 0;
}

SAM做法

分析

parent树上的\(fa\)指针就好像文本串的\(next\)数组。对第一个串建SAM,然后让第二个串在这个SAM上跑,随时更新答案,如果失配就跳\(fa\)指针。

代码片段

inline void solve(){
	int now=1,cur=0;
	rin(i,1,m){
		if(sam[now].to[str[i]-'a']){now=sam[now].to[str[i]-'a'];cur++;ans=std::max(ans,cur);}
		else{
			while(now&&!sam[now].to[str[i]-'a']){now=sam[now].fa;cur=sam[now].len;}
			if(!now){cur=0;now=1;continue;}
			now=sam[now].to[str[i]-'a'];cur++;ans=std::max(ans,cur);
		}
	}
}

int main(){
	scanf("%s",str+1);
	n=strlen(str+1);
	las=tot=1;
	rin(i,1,n) extend(str[i]-'a');
	getchar();
	scanf("%s",str+1);
	m=strlen(str+1);
	solve();
	printf("%d\n",ans);
	return 0;
}

hihoCoder#1419 : 后缀数组四·重复旋律4

题意

小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一个音乐旋律被表示为长度为\(N\)的数构成的数列。小Hi在练习过很多曲子以后发现很多作品中的旋律有重复的部分。

我们把一段旋律称为\((k,l)\)-重复的,如果它满足由一个长度为\(l\)的字符串重复了\(k\)次组成。 如旋律abaabaabaaba\((4,3)\)-重复的,因为它由aba重复\(4\)次组成。

小Hi想知道一部作品中\(k\)最大的\((k,l)\)-重复旋律。

分析

后缀数组求\(LCP\)的应用。

先枚举\(l\),然后对于每个\(l\)的倍数的位置\(pos\)求出\(LCP(pos,pos+l)\),推出\(pos-l \sim pos-1\)中能使\(LCP(x,x+l)\)最大的位置\(x\),并尝试更新\(ans\)

代码片段

inline void solve(){
	rin(i,1,n){
		for(int j=i;j+i<=n;j+=i){
			int temp=lcp(j,j+i);
			ans=std::max(ans,lcp(j-i+temp%i,j+temp%i)/i+1);
		}
	}
}

int main(){
	scanf("%s",str+1);
	n=strlen(str+1);
	rin(i,1,n) s[i]=str[i]-'a'+1;
	suffixsort();
	getheight();
	buildst();
	solve();
	printf("%d\n",ans);
	return 0;
}

hihoCoder#1445 : 后缀自动机二·重复旋律5

题意

一部作品中出现了多少不同的旋律?

分析

本质不同子串个数,直接枚举后缀自动机的每个状态,求:

\[\sum_{i=1}^{tot}len[i]-len[fa[i]] \]

Update on 2018/12/14:后缀数组做法:

\[\frac{n \times (n+1)}{2}-\sum_{i=2}^{n}height[i] \]

即可。

代码片段

int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	las=tot=1; 
	rin(i,1,n) extend(s[i]-'a');
	LL ans=0;
	rin(i,1,tot) ans+=sam[i].len-sam[sam[i].fa].len;
	printf("%lld\n",ans);
	return 0;
}

hihoCoder#1449 : 后缀自动机三·重复旋律6

题意

对于每个\(K\),一部作品中所有长度为\(K\)的旋律中出现次数最多的旋律的出现次数。

分析

本题的关键是求出SAM上每个状态\(right\)集合的大小。注意到两个不同的\(right\)要么没有交集,要么一个包含另一个。又因为parent树上的每个状态的\(right\)集合都包含其子状态的\(right\)集合,然后还发现一个状态能自己凭空产生一个新的结束位置当且仅当这个状态能表示原串的一个前缀。所以,对于parent树上的每个状态\(x\),有:

\[|right(x)|=\sum_{y是x的子状态}|right(y)|+[x能表示原串的一个前缀] \]

在建SAM时有且只有每个\(np\)满足后面那个条件,然后dfs一遍统计答案。每个状态\(x\)\(|right|\)可以直接统计在\(ans[len[x]]\)上,最后因为答案单调不增所以倒着扫一遍用每个\(ans[i]\)尝试更新\(ans[i-1]\)就好了。

代码片段

void dfs(int x){
	trav(i,x){
		int ver=e[i].to;
		dfs(ver);
		siz[x]+=siz[ver];
	}
}

int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	las=tot=1,siz[1]=1;
	rin(i,1,n) extend(s[i]-'a');
	rin(i,2,tot) add_edge(sam[i].fa,i);
	dfs(1);
	rin(i,1,tot) ans[sam[i].len]=std::max(ans[sam[i].len],siz[i]);
	rec(i,n-1,1) ans[i]=std::max(ans[i],ans[i+1]);
	rin(i,1,n) printf("%d\n",ans[i]);
	return 0;
}

hihoCoder#1457 : 后缀自动机四·重复旋律7

题意

\(N\)部作品中所有不同的旋律的“和”(也就是把串看成数字,在十进制下的求和,允许有前导\(0\))。答案有可能很大,我们需要对\(10^9 + 7\)取摸。

分析

看到有多个串直接建广义SAM,剩下的就是一个简单的DAG上的DP了,拓排即可,状态转移方程如下:

\[sum[x] = \sum_{存在pre->x的转移,i为对应该转移的数字} sum[pre] \times 10 + i \times ( len[x] - len[fa[x]] ) \]

记得让\(len[0]=-1\)

代码片段

inline void toposort(){
	while(!q.empty()){
		int x=q.front();q.pop();
		rin(i,0,9){
			int ver=sam[x].to[i];
			if(!ver) continue;
			sum[ver]=(sum[ver]+sum[x]*10+1ll*(sam[x].len-sam[sam[x].fa].len)*i)%MOD;
			deg[ver]--;
			if(!deg[ver]) q.push(ver);
		}
	}
}

int main(){
	n=read();
	sam[0].len=-1;
	tot=1;
	rin(i,1,n){
		if(i!=1) getchar();
		scanf("%s",s+1);
		int len=strlen(s+1);
		las=1;
		rin(j,1,len) extend(s[j]-'0');
	}
	rin(i,1,tot) rin(j,0,9)
		if(sam[i].to[j]) deg[sam[i].to[j]]++;
	while(!q.empty()) q.pop();
	rin(i,1,tot) if(!deg[i]) q.push(i);
	toposort();
	LL ans=0;
	rin(i,1,tot){
		ans+=sum[i];
		if(ans>=MOD) ans-=MOD;
	}
	printf("%lld\n",ans);
	return 0;
}

hihoCoder#1465 : 后缀自动机五·重复旋律8

题意

小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一段音乐旋律可以被表示为一段数构成的数列。

小Hi发现旋律可以循环,每次把一段旋律里面最前面一个音换到最后面就成为了原旋律的“循环相似旋律”,还可以对“循环相似旋律”进行相同的变换能继续得到原串的“循环相似旋律”。

小Hi对此产生了浓厚的兴趣,他有若干段旋律,和一部音乐作品。对于每一段旋律,他想知道有多少在音乐作品中的子串(重复便多次计)和该旋律是“循环相似旋律”。

分析

我觉得官方题解写的挺好的。(翻译:我不会)

戳我看题解

代码片段

void dfs(int x){
	trav(i,x){
		int ver=e[i].to;
		dfs(ver);
		siz[x]+=siz[ver];
	}
}

inline void solve(){
	ans=0;
	rin(i,1,m) s[i]=str[i]-'a';
	rin(i,m+1,(m<<1)-1) s[i]=s[i-m];
	m=(m<<1)-1;
	int cur=0,now=1;
	rin(i,1,m){
		if(sam[now].to[s[i]]){now=sam[now].to[s[i]];cur++;}
		else{
			while(now&&!sam[now].to[s[i]]){now=sam[now].fa;cur=sam[now].len;}
			if(!now){cur=0;now=1;}
			else{now=sam[now].to[s[i]];cur++;}
		}
		if(cur>((m+1)>>1)) while(sam[sam[now].fa].len>=((m+1)>>1)){now=sam[now].fa;cur=sam[now].len;}
		if(cur>=((m+1)>>1)&&vis[now]!=tag){vis[now]=tag;ans+=siz[now];}
	}
}

int main(){
	scanf("%s",str+1);
	las=tot=1;
	n=strlen(str+1);
	rin(i,1,n) extend(str[i]-'a');
	rin(i,2,tot) add_edge(sam[i].fa,i);
	dfs(1);
	int T=read();
	for(tag=1;tag<=T;tag++){
		scanf("%s",str+1);
		m=strlen(str+1);
		solve();
		printf("%d\n",ans);
	}
	return 0;
}

hihoCoder#1466 : 后缀自动机六·重复旋律9

题意

小Hi平时的一大兴趣爱好就是演奏钢琴。我们知道一段音乐旋律可以被表示为一段字符构成的字符串。

现在小Hi已经不满足于单单演奏了!他通过向一位造诣很高的前辈请教,通过几周时间学习了创作钢琴曲的基本理论,并开始对曲目做改编或者原创。两个月后,小Hi决定和前辈进行一场创作曲目的较量!

规则是这样的,有两部已知经典的作品,我们称之为\(A\)\(B\)。经典之所以成为经典,必有其经典之处。

刚开始,纸上会有一段\(A\)的旋律和一段\(B\)的旋律。两人较量的方式是轮流操作,每次操作可以选择在纸上其中一段旋律的末尾添加一个音符,并且要求添加完后的旋律依然是所在作品的旋律(也就是\(A\)\(B\)的一个子串)。谁词穷了(无法进行操作)就输了。

小Hi和前辈都足够聪明,但是小Hi还是太年轻,前辈打算教训他一顿。前辈表示会和小Hi进行\(K\)次较量,只要小Hi赢了哪怕一次就算小Hi获得最终胜利。但是前提是开始纸上的两段旋律需要他定。小Hi欣然同意,并且表示每次较量都让前辈先操作。

前辈老谋深算,显然是有备而来。他已经洞悉了所有先手必胜的初始(两段)旋律。第\(i\)天前辈会挑选字典序第\(i\)小的初始(两段)旋律来和小Hi较量。那么问题来了,作为吃瓜群众的你想知道,最后一天即第\(K\)天,前辈会定哪两个旋律呢?

初始时两段旋律的字典序比较方式是先比较前一个旋律字典序,一样大则比较后一旋律的字典序。

分析

我觉得官方题解写的挺好的,字面意思不需要翻译。

戳我看题解

趁着写这道题的功夫学了一下SG(傻狗)函数。

代码片段

inline void toposort(){
	while(!q.empty()){
		int x=q.front();q.pop();
		tpo[++tot]=x;
		rin(i,0,25){
			int ver=sam[x].to[i];
			if(!ver) continue;
			deg[ver]--;
			if(!deg[ver]) q.push(ver);
		}
	}
}

inline void get_sg_and_cnt(){
	rec(i,tot,1){
		memset(vis,0,sizeof vis);
		rin(j,0,25) if(sam[tpo[i]].to[j]) vis[sg[sam[tpo[i]].to[j]]]=1;
		rin(j,0,26) if(!vis[j]){sg[tpo[i]]=j;break;}
		cnt[tpo[i]][sg[tpo[i]]]=1;
		rin(j,0,25) if(sam[tpo[i]].to[j])
			rin(l,0,26) cnt[tpo[i]][l]+=cnt[sam[tpo[i]].to[j]][l];
	}
	rin(i,1,tot){
		LL temp=0;
		rin(j,0,26) temp+=cnt[i][j];
		rin(j,0,26) ncnt[i][j]=temp-cnt[i][j];
	}
}

void dfss(int x){
	if(ncnt[T][sg[x]]>=k){
		sfin=x;
		return;
	}
	k-=ncnt[T][sg[x]];
	rin(i,0,25){
		int ver=sam[x].to[i];
		if(!ver) continue;
		LL temp=0;
		rin(j,0,26) temp+=cnt[ver][j]*ncnt[T][j];
		if(temp>=k){
			s_ans[++slen]='a'+i;
			dfss(ver);
			return;
		}
		k-=temp;
	}
	flag=1;
}

void dfst(int x){
	if(sg[x]!=sg[sfin]){
		if(k==1) return;
		else k--;
	}
	rin(i,0,25){
		int ver=sam[x].to[i];
		if(!ver) continue;
		if(ncnt[ver][sg[sfin]]>=k){
			t_ans[++tlen]=i+'a';
			dfst(ver);
			return;
		}
		k-=ncnt[ver][sg[sfin]];
	}
}

inline void solve(){
	dfss(S);
	if(flag){
		printf("NO\n");
		exit(0);
	}
	dfst(T);
}

int main(){
	k=read();
	scanf("%s",s+1);
	getchar();
	scanf("%s",t+1);
	n=strlen(s+1);
	m=strlen(t+1);
	S=las=tot=1;
	rin(i,1,n) extend(s[i]-'a',S);
	T=las=++tot;
	rin(i,1,m) extend(t[i]-'a',T);
	rin(i,1,tot) rin(j,0,25) if(sam[i].to[j]) deg[sam[i].to[j]]++;
	while(!q.empty()) q.pop();
	rin(i,1,tot) if(!deg[i]) q.push(i);
	tot=0;
	toposort();
	get_sg_and_cnt();
	solve();
	printf("%s\n%s\n",s_ans+1,t_ans+1);
	return 0;
}

一些小总结

1、后缀数组的核心是\(height\)数组,常用的算法是二分答案后对\(height\)数组进行分组。

2、\(len[x]-len[fa[x]]\)的意义;

A. x这个状态能表示的字符串个数。
B. 从S出发到达x的路径数(当然首先你要令len[0]=-1)。

3、\(|right|\)的求法,标记所有能表示原串前缀的状态(即\(np\)),然后在parent树上dfs一遍统计即可。

4、parent树是反串的后缀树。

可能之后还会再补充一些。

posted on 2018-12-14 00:36  ErkkiErkko  阅读(296)  评论(0编辑  收藏  举报