字符串小祭(二)

AC 自动机

板子

Link
相信大家写过 KMP 例题九能够初步理解自动机的概念了。
AC 自动机也是通过存儿子的方式隐式应用了 fail 指针,从而形成了一个 Trie 图结构。
最后一步,我们的 fail 指针类似 border 树的,会形成一颗 fail 树,
于是不妨通过拓扑排序或者 dfs 来统计答案。

Code
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=200999,M=2000099;
char S[M];
int n,_;
struct ac{
	void ins(int x){
		int cur=0;
		for(int i=1;i<=n;i++){
			if(s[cur][S[i]-'a']==0)
				s[cur][S[i]-'a']=++cnt;
			cur=s[cur][S[i]-'a'];
		}
		pos[x]=cur;
	}
	void bd(){
		queue<int> q;
		for(int i=0;i<26;i++)
			if(s[0][i]!=0)
				q.push(s[0][i]),k[0].pb(s[0][i]);
		while(!q.empty()){
			int t=q.front();
			q.pop();
			for(int i=0;i<26;i++)
				if(s[t][i])
					q.push(s[t][i]),f[s[t][i]]=s[f[t]][i],k[s[f[t]][i]].pb(s[t][i]);
				else
					s[t][i]=s[f[t]][i];
		}
	}
	void dfs(int i){
		for(int j:k[i])
			dfs(j),vis[i]+=vis[j];
	}
	void qr(){
		for(int i=1,cur=0;i<=n;i++){
			cur=s[cur][S[i]-'a'];
			++vis[cur];
		}
		dfs(0);
		for(int i=1;i<=_;i++)
			printf("%d\n",vis[pos[i]]);
	}
	int s[N][26],f[N],vis[N],pos[N],cnt;
	vector<int> k[N];
}T;
int main(){
	scanf("%d",&_);
	for(int i=1;i<=_;i++){
		scanf("%s",S+1);
		n=strlen(S+1);
		T.ins(i);
	}
	T.bd();
	scanf("%s",S+1);
	n=strlen(S+1);
	T.qr();
	return 0;
}

后缀数组相关

Link
原链还能看,这里简化一下。

大致思想:
通过对数轮双关键字排序,合并字符串。
每轮合并采取先计排第二关键字,再计排第一关键字的策略。

  • 预备排序:把每个字符的排名设为字符本身,进行一轮计数排序,处理出一开始的 rksa 数组。
int lim=128;
for(int i=1;i<=n;i++) ++cnt[rk[i]=S[i]];
for(int i=1;i<=lim;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
//倒序来做,维护排序的稳定性。
  • 正式排序:从一开始枚举块长 \(w\)
    • 清空计数的 cnt 数组。
    • 考虑 \([n-w+1,n]\) 没有第二关键字,直接插入 tsa 数组。
      对于剩余的数,已经排好了一个 sa 数组,我们依次选择位置大于 \(w\) 的插入 tsa 数组。
    • 对第一关键字进行计数,把 tsa 转化为 sa
    • 制作一份 rk 数组的拷贝 trk,使用迭代器对于 sa 数组里面的东西依次赋予 rk
      对于本质相同的两个东西给予相同的 rk
  • 收尾工作:块长乘二,计数上界重新设置为最大的 rk,如果 rk 两两不同直接退出。

这个代码并不好写,几个细节:

  • trk 数组一定要开两倍。
  • 想好什么时候用 lim 什么时候用 n
  • 计数排序都是 cnt[...]--
  • 注意 rk[sa[i]]=it 而非 rk[i]=it
  • 注意 f2(i,1,n) sa[cnt[rk[tsa[i]]]]--]=tsa[i]; 这行语句的书写。
点击查看代码
#include<bits/stdc++.h>
#define f1(i,a,b) for(int i=a;i<=b;++i)
#define f2(i,a,b) for(int i=b;i>=a;--i)
using namespace std;
const int N=1009999;
char S[N];
int n,rk[N],trk[N+N],cnt[N],tsa[N],sa[N],lim=128,it;
int main(){
    scanf("%s",S+1);
    n=strlen(S+1);
    f1(i,1,n)
        ++cnt[rk[i]=S[i]];
    f1(i,1,lim)
        cnt[i]+=cnt[i-1];
    f2(i,1,n)
        sa[cnt[rk[i]]--]=i;
    for(int w=1;;w<<=1,lim=it){
        memset(cnt,0,sizeof cnt);
        it=0;
        f1(i,n-w+1,n)
            tsa[++it]=i;
        f1(i,1,n) if(sa[i]>w)
            tsa[++it]=sa[i]-w;
        f1(i,1,n)
            ++cnt[rk[i]];
        f1(i,1,lim)
            cnt[i]+=cnt[i-1];
        f2(i,1,n)
            sa[cnt[rk[tsa[i]]]--]=tsa[i];
        memcpy(trk,rk,sizeof rk);
        it=0;
        f1(i,1,n){
            it+=(trk[sa[i]]!=trk[sa[i-1]]||trk[sa[i]+w]!=trk[sa[i-1]+w]);
            rk[sa[i]]=it;
        }
        if(it==n) break;
    }
    f1(i,1,n)
    	printf("%d ",sa[i]);
    return 0;
}

后缀数组可以用来求任意两个子串的最长公共前缀 LCP。
\(h(x)=\mathrm{LCP}(sa(x-1),sa(x)),h(1)=0\)
那么有 \(h(rk(x))\ge h(rk(x-1))-1\),这样两个后缀的 LCP 就可以 ST 表来求。

LCP 板子:https://www.luogu.com.cn/problem/U103231

注意要想好,h[0][rk[i]]=max(0,h[0][rk[i-1]])j=sa[rk[i]-1] 的问题。

点击查看代码
#include<bits/stdc++.h>
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,l,r) for(int i=r;i>=l;--i)
using namespace std;
const int N=1000099,K=20;
char S[N];
int n,cnt[N],rk[N],trk[N+N],sa[N],tsa[N],h[K+5][N],lim=128,it,lg[N];
int main(){
    int T;
    scanf("%d%s",&T,S+1),n=strlen(S+1);
    f1(i,1,n) ++cnt[rk[i]=S[i]];
    f1(i,1,lim) cnt[i]+=cnt[i-1];
    f2(i,1,n) sa[cnt[rk[i]]--]=i;
    for(int w=1;;w<<=1,lim=it){
        memset(cnt,0,sizeof cnt);
        it=0;
        f1(i,n-w+1,n)
            tsa[++it]=i;
        f1(i,1,n) if(sa[i]>w)
            tsa[++it]=sa[i]-w;
        f1(i,1,n)
            ++cnt[rk[i]];
        f1(i,1,lim)
            cnt[i]+=cnt[i-1];
        f2(i,1,n)
            sa[cnt[rk[tsa[i]]]--]=tsa[i];
        memcpy(trk,rk,sizeof rk);
        it=0;
        f1(i,1,n){
            it+=(trk[sa[i]]!=trk[sa[i-1]]||trk[sa[i]+w]!=trk[sa[i-1]+w]);
            rk[sa[i]]=it;
        }
        if(it==n) break;
    }
    for(int i=1,j,lst;i<=n;i++){
        if(rk[i]==1) continue;
        h[0][rk[i]]=max(0,h[0][rk[i-1]]-1);
        j=sa[rk[i]-1];
        while(i+h[0][rk[i]]<=n&&j+h[0][rk[i]]<=n&&S[i+h[0][rk[i]]]==S[j+h[0][rk[i]]])
            ++h[0][rk[i]];
    }
    lg[1]=0;
    f1(i,2,n)
        lg[i]=lg[i>>1]+1;
    f1(i,1,K) for(int j=1;j+(1<<i)-1<=n;j++)
        h[i][j]=min(h[i-1][j],h[i-1][j+(1<<(i-1))]);
    for(int i=1,a,b,c,d,ans;i<=T;i++){
        scanf("%d%d%d%d",&a,&b,&c,&d);
        ans=min(b-a+1,d-c+1);
        if(a==c){
            printf("%d\n",ans);
            continue;
        } 
        a=rk[a],c=rk[c];
        if(a>c)
            swap(a,c);
        ++a;
        b=lg[c-a+1];
        ans=min(ans,min(h[b][a],h[b][c-(1<<b)+1]));
        printf("%d\n",ans);
    }
    return 0;
}
一些水题

https://www.luogu.com.cn/problem/P4051
破环为链是一定的,然后按照要求的话,直接后缀排序即可。

https://www.luogu.com.cn/problem/P2870
这是非常经典的贪心了,不一样就暴力删除,一样就二分哈希找不同位置即可。
但是我们其实就是想快速比较一个前缀和后缀的大小关系啦!
那就构造正反串,例如 2024ing->2024inggni4202 然后就做完了。

https://www.luogu.com.cn/problem/P2852
无端有一个想法,对于 height 数组滑动窗口,区间最小值的最大值就是答案。
首先子串是后缀的前缀,然后这些位置 LCP 都是他,他就出现这么多次,就做完了……

https://vjudge.net/problem/POJ-3693#author=translator:1281309:zh
我们再来理解一下,考虑我们会 \(O(1)\) 求两个子串的 LCP。
那你不妨枚举一下这个连续段的左端点,枚举他的周期长度,暴力推 \(\mathrm{LCP}(x,x+L)\ge L\)
这样获得的就是 \(O(n^2 \log n)\) 做法。

然后你考虑如果你不知道左端点,你仅仅枚举周期的话,这个东西也是可做的。
考虑答案大于等于二的时候一定这个串会覆盖掉至少两个连续的 \(L\) 的倍数。
LCP 这个东西其实等价于移位哈希判定周期,
只不过他优秀过 KMP 就在于他起点是可变的。

所以这题思路相当暴力:
两个点往前往后分别做 LCP,然后问题到了一组子串的大小关系上,
怎么快速找到最小位置呢?用 ST 表啊!
这样有一个调和求和约等于 \(\log\) 的复杂度。

参考:https://www.cnblogs.com/MashiroSky/p/6279887.html

点击查看代码
#include<algorithm>
#include<cstdio>
#include<cstring>
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,l,r) for(int i=r;i>=l;--i)
using namespace std;
const int N=100090,K=18;
char S[2][N];
int n,lim,it,cnt[2][N],rk[2][N],trk[2][N+N],sa[2][N],tsa[2][N],h[2][K+4][N],pos[K+4][N],lg[N];

inline int cmp(int a,int b){
	return rk[0][a]<rk[0][b]?a:b;
}

inline int lcp(int _,int a,int b){
	if(a==b) return n-a+1;
	a=rk[_][a],b=rk[_][b];
	if(a>b) swap(a,b);
	++a;
	int bs=lg[b-a+1];
	return min(h[_][bs][a],h[_][bs][b-(1<<bs)+1]);
}

inline int bst(int a,int b){
	int bs=lg[b-a+1];
	return cmp(pos[bs][a],pos[bs][b-(1<<bs)+1]);
}

inline void upd(int &ans,int &l,int &r,int tns,int ll,int rr){
	if(tns<ans) return;
	if(tns>ans){
		ans=tns;
		l=ll;
		r=rr;
		return;
	}
	if(lcp(0,l,ll)>=min(r-l+1,rr-ll+1)){
		if(rr-ll+1<r-l+1) r=rr;
		return;
	}
	if(rk[0][l]>rk[0][ll])
		l=ll,r=rr;
}
int main(){
	int Test_Case=0;
	lg[1]=0;
	f1(i,2,100000) lg[i]=lg[i>>1]+1;
	while(scanf("%s",S[0]+1)){
		if(S[0][1]=='#')
			return 0;
		printf("Case %d: ",++Test_Case);
		n=strlen(S[0]+1);
		f1(i,1,n) S[1][i]=S[0][n-i+1];
		f1(_,0,1){
			lim=128;
			f1(i,1,n) ++cnt[_][rk[_][i]=S[_][i]];
			f1(i,1,lim) cnt[_][i]+=cnt[_][i-1];
			f2(i,1,n) sa[_][cnt[_][rk[_][i]]--]=i;
			for(int w=1;;w<<=1,lim=it){
				memset(cnt[_],0,sizeof cnt[_]),it=0;
				f1(i,n-w+1,n) tsa[_][++it]=i;
				f1(i,1,n) if(sa[_][i]>w) tsa[_][++it]=sa[_][i]-w;
				f1(i,1,n) cnt[_][rk[_][i]]++;
				f1(i,1,lim) cnt[_][i]+=cnt[_][i-1];
				f2(i,1,n) sa[_][cnt[_][rk[_][tsa[_][i]]]--]=tsa[_][i];
				memcpy(trk[_],rk[_],sizeof rk[_]),it=0;
				f1(i,1,n){
					it+=(trk[_][sa[_][i]]!=trk[_][sa[_][i-1]]||trk[_][sa[_][i]+w]!=trk[_][sa[_][i-1]+w]);
					rk[_][sa[_][i]]=it;
				}
				if(it==n) break;
			}
			f1(i,1,n){
				if(rk[_][i]==1) continue;
				int j=sa[_][rk[_][i]-1],k=max(0,h[_][0][rk[_][i-1]]-1);
				while(i+k<=n&&j+k<=n&&S[_][i+k]==S[_][j+k]) ++k;
				h[_][0][rk[_][i]]=k;
			}
			f1(i,1,K) f1(j,1,n-(1<<i)+1)
				h[_][i][j]=min(h[_][i-1][j],h[_][i-1][j+(1<<(i-1))]);
			
		}
		f1(i,1,n) pos[0][i]=i;
		f1(i,1,K) f1(j,1,n-(1<<i)+1)
			pos[i][j]=cmp(pos[i-1][j],pos[i-1][j+(1<<(i-1))]);
		
		int ans=1,ll=sa[0][1],rr=sa[0][1];
		f1(L,1,n) for(int i=1,j;i+L<=n;i+=L) if(S[0][i]==S[0][j=i+L]){
			int ff=i-lcp(1,n-i+1,n-j+1)+1,tt=j+lcp(0,i,j)-1;
			int len=tt-ff+1;
			int tns=len/L;
			int fff=tt-tns*L+1;
			int tll=bst(ff,fff);
			upd(ans,ll,rr,tns,tll,tll+tns*L-1);
			// printf("(%d %d %d %d %d)",ff,tt,ll,rr,ans);
		}
		f1(i,ll,rr) putchar(S[0][i]);
		f1(_,0,1) f1(i,1,max(128,n))
			S[_][i]='\0',cnt[_][i]=rk[_][i]=trk[_][i]=sa[_][i]=tsa[_][i]=h[_][0][i]=pos[0][i]=0;
		lim=128,it=0;
		putchar('\n');
	}
	return 0;
}
一些模型
  • 可重叠最长重复子串:取 height 数组最大值。
  • 不可重叠最长重复子串:二分答案,对于 LCP 合法的一段后缀区间分组,每组暴力找最小最大下标位置。
  • 至少 \(k\) 次的可重叠最长重复子串:水题三。
  • 本质不同子串数:\(S=\frac 12n(n+1)-\sum h_i\)
  • 重复次数最多的连续重复子串:水题四。
例题一

https://www.luogu.com.cn/problem/P1117
找一个性质:对于给定字符串和确定的长度,可以 \(O(1)\) 判断合法性,虽然是哈希。
枚举是必要的,先枚举这个 A 的长度。
然后我们容易调和的求解出连续两个 A 的位置啦。
然后你思考这种东西……他好像就能转化成答案。
不妨维护以每个点为末尾和开头的答案。
不妨钦定每次仅仅标记开头在这个东西前面的一个区间,以图不重不漏,
写一个差分就能过。

点击查看代码
#include<bits/stdc++.h>
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,l,r) for(int i=r;i>=l;--i)
using namespace std;
typedef long long ll;
const int N=30090,K=15;
char S[2][N];
int n,lim,it,cnt[2][N],rk[2][N],trk[2][N+N],sa[2][N],tsa[2][N],h[2][K+4][N],lg[N];
ll ed[N],fr[N];

inline int cmp(int a,int b){
	return rk[0][a]<rk[0][b]?a:b;
}

inline int lcp(int _,int a,int b){
	if(a==b) return n-a+1;
	a=rk[_][a],b=rk[_][b];
	if(a>b) swap(a,b);
	++a;
	int bs=lg[b-a+1];
	return min(h[_][bs][a],h[_][bs][b-(1<<bs)+1]);
}

int main(){
	int T; scanf("%d",&T);
	lg[1]=0; f1(i,2,30000) lg[i]=lg[i>>1]+1;
	while(T--){
        scanf("%s",S[0]+1);
		n=strlen(S[0]+1);
		f1(i,1,n) S[1][i]=S[0][n-i+1];
		f1(_,0,1){
			lim=128;
			f1(i,1,n) ++cnt[_][rk[_][i]=S[_][i]];
			f1(i,1,lim) cnt[_][i]+=cnt[_][i-1];
			f2(i,1,n) sa[_][cnt[_][rk[_][i]]--]=i;
			for(int w=1;;w<<=1,lim=it){
				memset(cnt[_],0,sizeof cnt[_]),it=0;
				f1(i,n-w+1,n) tsa[_][++it]=i;
				f1(i,1,n) if(sa[_][i]>w) tsa[_][++it]=sa[_][i]-w;
				f1(i,1,n) cnt[_][rk[_][i]]++;
				f1(i,1,lim) cnt[_][i]+=cnt[_][i-1];
				f2(i,1,n) sa[_][cnt[_][rk[_][tsa[_][i]]]--]=tsa[_][i];
				memcpy(trk[_],rk[_],sizeof rk[_]),it=0;
				f1(i,1,n){
					it+=(trk[_][sa[_][i]]!=trk[_][sa[_][i-1]]||trk[_][sa[_][i]+w]!=trk[_][sa[_][i-1]+w]);
					rk[_][sa[_][i]]=it;
				}
				if(it==n) break;
			}
			f1(i,1,n){
				if(rk[_][i]==1) continue;
				int j=sa[_][rk[_][i]-1],k=max(0,h[_][0][rk[_][i-1]]-1);
				while(i+k<=n&&j+k<=n&&S[_][i+k]==S[_][j+k]) ++k;
				h[_][0][rk[_][i]]=k;
			}
			f1(i,1,K) f1(j,1,n-(1<<i)+1)
				h[_][i][j]=min(h[_][i-1][j],h[_][i-1][j+(1<<(i-1))]);
			
		}

        f1(L,1,n) for(int i=1,j;(j=i+L)<=n;i+=L) if(S[0][i]==S[0][j]){
            int ff=i-lcp(1,n-i+1,n-j+1)+1,tt=j+lcp(0,i,j)-1-L-L+1;
            tt=min(tt,i),ff=max(ff,i-L+1);
            if(ff>tt) continue;
            ++fr[ff],--fr[tt+1];
            ++ed[ff+L+L-1],--ed[tt+L+L-1+1];
        }
		f1(i,1,n) ed[i]+=ed[i-1],fr[i]+=fr[i-1];
        ll ans=0;
        f1(i,1,n) ans+=ed[i]*fr[i+1];
        printf("%lld\n",ans);
		f1(_,0,1) f1(i,1,30001)
			S[_][i]='\0',cnt[_][i]=rk[_][i]=trk[_][i]=sa[_][i]=tsa[_][i]=h[_][0][i]=ed[i]=fr[i]=0;
		lim=128,it=0;
	}
	return 0;
}

这个思路本质上是发现 AA 和 BB 的可合并性,哈希是通过暴力枚举字串实现我们两次 LCP 的事情。

例题二

https://www.luogu.com.cn/problem/P2178
你考虑高级的相似包含低级的相似,同级相似具有传递性。
一个级别的相似对应若干个 h 数组连续段。
如果你去降级,你就可能你合并一些连续段。
那你不妨线段树或者 ST 表维护相邻排名的美味度乘积最大值,
具体实现就是维护区间最大值,次大值,最小值,次小值,然后拒绝分讨直接取 max。
没有虐待自己的习惯,还是用线段树吧……
你不妨对于临项 h 数组排序后,每次快速合并一些东西,这里使用并查集,同时维护连通块左右端点,
然后这个瓶颈复杂度大概是后缀排序的对数。

点击查看代码
#include<bits/stdc++.h>
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,l,r) for(int i=r;i>=l;--i)
#define ls (i<<1)
#define rs (i<<1|1)
#define ins int mid=(l+r)>>1
using namespace std;
typedef long long ll;
const int N=3000099,inf=1e9+99;
char S[N];
int n,cnt[N],rk[N],trk[N+N],sa[N],tsa[N],h[N],lim=128,it,tsy[N],stid[N];

struct sgmt{
	void bd(int i,int l,int r){
		if(l==r){
			mx[i]=mn[i]=tsy[sa[l]];
			smx[i]=-inf,smn[i]=inf;
			return;
		}
		ins;
		bd(ls,l,mid),bd(rs,mid+1,r);
		mx[i]=max(mx[ls],mx[rs]);
		mn[i]=min(mn[ls],mn[rs]);
		smx[i]=max(min(mx[ls],mx[rs]),max(smx[ls],smx[rs]));
		smn[i]=min(max(mn[ls],mn[rs]),min(smn[ls],smn[rs]));
	}
	void qr(int i,int l,int r,int L,int R,int &_mx,int &_mn,int &_smx,int &_smn){
		if(l==L&&r==R){
			_mx=mx[i];
			_mn=mn[i];
			_smx=smx[i];
			_smn=smn[i];
			return;
		}
		ins;
		if(R<=mid) qr(ls,l,mid,L,R,_mx,_mn,_smx,_smn);
		else if(L>mid) qr(rs,mid+1,r,L,R,_mx,_mn,_smx,_smn);
		else{
			int mnls,mnrs,mxls,mxrs,smxls,smxrs,smnls,smnrs;
			qr(ls,l,mid,L,mid,mxls,mnls,smxls,smnls);
			qr(rs,mid+1,r,mid+1,R,mxrs,mnrs,smxrs,smnrs);
			_mx=max(mxls,mxrs);
			_mn=min(mnls,mnrs);
			_smx=max(min(mxls,mxrs),max(smxls,smxrs));
			_smn=min(max(mnls,mnrs),min(smnls,smnrs));
		}
	}
	int mx[N<<2],smx[N<<2],mn[N<<2],smn[N<<2];
}Sgt;

int fa[N],lll[N],rrr[N],sz[N];
ll ans1,ans2=LLONG_MIN,ansl[N],ansr[N];
int fd(int x){return (fa[x]==x)?x:fa[x]=fd(fa[x]);}
void mg(int x,int y){
	// printf("mg %d %d\n",x,y);
	x=fd(x),y=fd(y);
	if(x==y) return;
	ans1-=1ll*sz[x]*(sz[x]-1)/2;
	ans1-=1ll*sz[y]*(sz[y]-1)/2;
	fa[x]=y,lll[y]=min(lll[y],lll[x]),rrr[y]=max(rrr[y],rrr[x]),sz[y]+=sz[x];
	ans1+=1ll*sz[y]*(sz[y]-1)/2;
	int a,b,c,d;
	Sgt.qr(1,1,n,lll[y],rrr[y],a,b,c,d);
	ans2=max(ans2,max(1ll*a*c,1ll*b*d));
}

bool cmp(int a,int b){
	return h[a]>h[b];
}

int main(){
    int T;
    scanf("%d%s",&T,S+1),n=strlen(S+1);
    f1(i,1,n) ++cnt[rk[i]=S[i]];
    f1(i,1,lim) cnt[i]+=cnt[i-1];
    f2(i,1,n) sa[cnt[rk[i]]--]=i;
    for(int w=1;;w<<=1,lim=it){
        memset(cnt,0,sizeof cnt);
        it=0;
        f1(i,n-w+1,n)
            tsa[++it]=i;
        f1(i,1,n) if(sa[i]>w)
            tsa[++it]=sa[i]-w;
        f1(i,1,n)
            ++cnt[rk[i]];
        f1(i,1,lim)
            cnt[i]+=cnt[i-1];
        f2(i,1,n)
            sa[cnt[rk[tsa[i]]]--]=tsa[i];
        memcpy(trk,rk,sizeof rk);
        it=0;
        f1(i,1,n){
            it+=(trk[sa[i]]!=trk[sa[i-1]]||trk[sa[i]+w]!=trk[sa[i-1]+w]);
            rk[sa[i]]=it;
        }
        if(it==n) break;
    }
    f1(i,1,n){
    	if(rk[i]==1) continue;
    	int j=sa[rk[i]-1],k=max(0,h[rk[i-1]]-1);
    	while(i+k<=n&&j+k<=n&&S[i+k]==S[j+k]) ++k;
    	h[rk[i]]=k;
    	// printf("h %d(%d %d) = %d\n",rk[i],i,j,k);
    }
   	f1(i,1,n) scanf("%d",tsy+i);
   	Sgt.bd(1,1,n);
   	f1(i,1,n) fa[i]=lll[i]=rrr[i]=stid[i]=i,sz[i]=1;
   	sort(stid+2,stid+n+1,cmp);
   	int pos=2;
   	f2(i,0,n-1){
   		// printf("%d %d %d\n",i,h[stid[pos]],stid[pos]);
   		while(pos<=n&&h[stid[pos]]>=i)
   			mg(stid[pos],stid[pos]-1),++pos;
   		ansl[i]=ans1;
   		ansr[i]=(ans1)?ans2:0;
   		// printf("%lld %lld\n",ans1,ans2);
   	}
   	f1(i,0,n-1) printf("%lld %lld\n",ansl[i],ansr[i]);
    return 0;
}

SAM

板子

颓题颓不动了,学发 SAM。

一些理论 SAM 同样是自动机,几个性质:
  • 任意被标记的节点对应一个后缀。

wiki 上“结束位置”集合相同的子串,
必然呈现出一些是另一些的后缀。
显然,后缀更短,他的结束位置集合是单调不降的。
而如果两个子串没有相交关系,他们的结束位置集合无交。

  • 每个节点可能会对应若干个子串,必然保证这些子串的结束位置集合相同。
  • 每个节点的后缀链接,指向自己的一个后缀,使得后缀短到结束位置集合比自己大。
    特殊的,类似 fail 不行就直接指向 trie 树空结点。
  • 每个节点对应 endpos 集合的一种合法取值,而显然 link 也可以形成一棵树。(喵!)

又是你需要知道的事情,一些道理的道理。

  • 你考虑这东西有一个 trie 图,还有一个 link 树。
    这里头有所有后缀,必然也有所有后缀的前缀,也就是有所有子串。
    如果你选了对应原字符串的这条最长路径,那你选到的就是前缀了。

  • 这 link 树非常神奇,你往上跳取得是当前串后缀,
    一蹦之后 endpos 集合变大了,跳过了可能不止一个字符串。
    那如果你从原字符串往上跳,你得到的就是所有后缀了。
    反过来,你往下跳的话,相当于你往前面加字符,所以加不一样的字符状态就可以分裂。

  • link 树很 nb 的一点是,你不往下跳就形成了新的子串吗,
    那显然,新的子串之间 endpos 集合是不交的,而他们并起来就是当前点的 endpos 集合……吗?
    这不一定,比如当前点 bcde,如果他就出现在开头,他就拥有一个独属于自己新的 endpos 了。

  • 回到 trie 图上玩,俩点可能通过加一个相同的字符干到一个点上。

  • 然后就李姐万岁,给你整俩李姐:

    • 他在 trie 图上看,等价于所有后缀的 AC 自动机,
      所以他也包含所有后缀的前缀,也就是有所有子串。
    • 你考虑对应前缀和原字符串的那条最长 trie 链,
      他们恰好是 fail 树上最深的节点,可以通过上跳取后缀,所以包含所有子串。

自己看一下 wiki 的小结部分,看一下关于节点对应最长/最短串的定义。
其实就是讲,阿巴阿巴,一个字符对应后缀相同的一组字符串,长度属于区间 \([mnl,mxl]\)

其实不妨直接从 wiki 实现部分开始看,再回去看看理论。

https://www.luogu.com.cn/problem/P3804
解释一下题意,应当这样断句:

所有/出现次数不为 1 的子串/的出现次数/乘上该子串长度/的最大值。

如果你用后缀数组做,
就是对于 h 数组求区间 \(\min\) 乘区间长度加一最大值。
这个东西并非不可做,我们不妨维护 \(h\) 数组中每一个数左右侧第一个比它小的,
显然单调栈板板题,然后线性统计答案即可。

SA 做法
#include<bits/stdc++.h>
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,l,r) for(int i=r;i>=l;--i)
using namespace std;
const int N=1000999;
char s[N];
int n,rk[N],sa[N],trk[N+N],tsa[N],cnt[N],h[N],lim=128,it,l[N],r[N];
stack<int> st;
int main(){
    scanf("%s",s+1),n=strlen(s+1);
    f1(i,1,n) ++cnt[rk[i]=s[i]];
    f1(i,1,lim) cnt[i]+=cnt[i-1];
    f2(i,1,n) sa[cnt[rk[i]]--]=i;
    for(int w=1;;w<<=1,lim=it){
        memset(cnt,0,sizeof cnt),it=0;
        f1(i,n-w+1,n) tsa[++it]=i;
        f1(i,1,n) if(sa[i]>w) tsa[++it]=sa[i]-w;
        f1(i,1,n) ++cnt[rk[i]];
        f1(i,1,lim) cnt[i]+=cnt[i-1];
        f2(i,1,n) sa[cnt[rk[tsa[i]]]--]=tsa[i];
        memcpy(trk,rk,sizeof rk),it=0;
        f1(i,1,n){
            it+=(trk[sa[i]]!=trk[sa[i-1]]||trk[sa[i]+w]!=trk[sa[i-1]+w]);
            rk[sa[i]]=it;
        }
        if(it==n) break;
    }
    f1(i,1,n){
        if(rk[i]==1) continue;
        int j=sa[rk[i]-1],k=max(0,h[rk[i-1]]-1);
        while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
        h[rk[i]]=k;
    }
    f1(i,1,n){
        while(!st.empty()&&h[st.top()]>=h[i]) st.pop();
        l[i]=(st.empty())?1:st.top()+1;
        st.push(i);
    }
    while(!st.empty()) st.pop();
    f2(i,1,n){
        while(!st.empty()&&h[st.top()]>=h[i]) st.pop();
        r[i]=(st.empty())?n:st.top()-1;
        st.push(i);
    }
    long long ans=0;
    f1(i,1,n)
        ans=max(ans,1ll*(r[i]-l[i]+2)*h[i]);
    printf("%lld\n",ans);
    return 0;
}

然后考虑 SAM,你考虑一个点 endpos 相同,出现次数也相同,
那你肯定会去选这里最优秀的 len。
然后你考虑你把 link 树建出来,
前缀链特殊标记一下,在树上 dfs 即可。

点击查看代码
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=2000999,K=26;
char a[N];
int n;
long long ans;
struct sam{
    void dfs(int u){
        for(int v:k[u])
            dfs(v),num[u]+=num[v];
        if(num[u]!=1)
            ans=max(ans,1ll*num[u]*len[u]);
    }
    
    void bd(){
        f[0]=-1;
        for(int i=1,c;i<=n;i++){
            c=a[i]-'a';
            cur=++cnt;
            len[cur]=len[lst]+1;
            while(~lst&&s[lst][c]==0)
                s[lst][c]=cur,lst=f[lst];
            if(lst==-1)
                f[cur]=0;
            else if(len[s[lst][c]]==len[lst]+1)
                f[cur]=s[lst][c];
            else{
                int son=s[lst][c],nson=++cnt;
                f[nson]=f[son];
                memcpy(s[nson],s[son],sizeof s[son]);
                len[nson]=len[lst]+1;
                f[cur]=f[son]=nson;

                while(~lst&&s[lst][c]==son)
                    s[lst][c]=nson,lst=f[lst];
            }
            lst=cur;
        }
        
        for(int i=1;i<=cnt;i++) k[f[i]].pb(i);
        for(int i=1,c,it=0;i<=n;i++){
            c=a[i]-'a',it=s[it][c];
            ++num[it];
        }
        dfs(0);
    }
    int s[N][K],len[N],f[N],lst,cur,cnt;
    vector<int> k[N];

    int num[N];
}Sam;

int main(){
    scanf("%s",a+1),n=strlen(a+1);
    Sam.bd();
    printf("%lld\n",ans);
    return 0;
}

一种猎奇的优化是按照 len 从长到短对于每一个节点排序,
然后迭代这样来做,避免爆栈,可以显式建树,也可以直接刷表法。

一些模型
  • 检查字符串是否出现
    解:走就完了。
  • 计算不同子串的个数
    解:每个点对应串数是可做的,加一下即可。
  • 计算所有不同子串的总长度
    解:等差数列求和公式。
  • 字典序第 k 大子串/sam-dp
    解:参见 0x05。

sam-dp 有一个关键点:
不要去想每个点 len endpos link 这些唐氏玩应,
就考虑,我们一个字串就是一条路径,
现在就给你一个线性的 DAG,让你跑 dp,这样不会怀疑正确性。

  • 解决最小表示法问题
    这做法太多了,复制直接上 SA 也行。
    SAM 同样需要复制,然后贪心的走若干步即可。

旷野颓水题

0x01

https://www.luogu.com.cn/problem/P3121
显然 AC 机的题,考虑用一个栈维护,到终止节点就弹栈,
好像 KMP 例题里有类似的单模式串问题。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=100999;
char S[N],T[N];
int n,lns,lnt;
struct ac{
    void ins(){
        int cur=0;
        for(int i=1;i<=lnt;i++){
            int p=T[i]-'a';
            if(!s[cur][p])
                s[cur][p]=++cnt;
            cur=s[cur][p];
        }
        ed[cur]=lnt;
    }
    void bd(){
        queue<int> q;
        for(int i=0;i<26;i++)
            if(s[0][i])
                q.push(s[0][i]);
        while(!q.empty()){
            int t=q.front();
            q.pop();
            for(int i=0;i<26;i++)
                if(s[t][i])
                    f[s[t][i]]=s[f[t]][i],q.push(s[t][i]);
                else
                    s[t][i]=s[f[t]][i];
        }
    }
    int s[N][26],f[N],cnt,ed[N];
}AC;
vector<int> A;
vector<char> B;
int main(){
    scanf("%s%d",S+1,&n),lns=strlen(S+1);
    for(int i=1;i<=n;i++)
        scanf("%s",T+1),lnt=strlen(T+1),AC.ins();
    AC.bd();
    A.push_back(0);
    for(int i=1;i<=lns;i++){
        int p=AC.s[A.back()][S[i]-'a'];
        A.push_back(p);
        B.push_back(S[i]);
        for(int j=1;j<=AC.ed[p];j++)
            A.pop_back(),B.pop_back();
    }
    for(char c:B) putchar(c);
    return 0;
}
0x02

https://www.luogu.com.cn/problem/P2444
建个 AC 机,在 Trie 图上 dfs 找不经过终止点的环,
这破题你得先 DFS 判定每个节点是否等价于终止点。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=39999;
char T[N];
int n,lnt;
struct ac{
    void ins(){
        int cur=0;
        for(int i=1;i<=lnt;i++){
            int p=T[i]-'0';
            if(!s[cur][p])
                s[cur][p]=++cnt;
            cur=s[cur][p];
        }
        ed[cur]=1;
    }
    void dfs(int u){
        for(int v:k[u])
            ed[v]|=ed[u],dfs(v);
    }
    void bd(){
        queue<int> q;
        for(int i=0;i<2;i++)
            if(s[0][i])
                q.push(s[0][i]),k[0].push_back(s[0][i]);
        while(!q.empty()){
            int t=q.front();
            q.pop();
            for(int i=0;i<2;i++)
                if(s[t][i])
                    f[s[t][i]]=s[f[t]][i],q.push(s[t][i]),k[s[f[t]][i]].push_back(s[t][i]);
                else
                    s[t][i]=s[f[t]][i];
        }
        dfs(0);
    }
    void dfs2(int u){
        if(ed[u]) return;
        if(vis[u]==2) return;
        if(vis[u]==1) puts("TAK"),exit(0);
        vis[u]=1;
        dfs2(s[u][0]),dfs2(s[u][1]);
        vis[u]=2;
    }
    int s[N][2],f[N],cnt;
    bool ed[N];
    int vis[N];
    vector<int> k[N];
}AC;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%s",T+1),lnt=strlen(T+1),AC.ins();
    AC.bd();
    AC.dfs2(0);
    puts("NIE");
    return 0;
}
0x03

https://www.luogu.com.cn/problem/P2414
猜测需要用 AC 机。
那你考虑这个 BP 操作其实非常适合在 Trie 树上进行操作。
只是这个询问是猎奇的,我们猜测他和 fail 树有关,
于是咱大大方方的把 fail 树抽出来,然后这个问题?

节点 \(a\) 有多少个儿子属于节点 \(b\) 对应的集合呢?

如果你处理一下树上 LCA,暴力做,
对于每一个询问,
枚举 b 里面所有点,
判断一下,
你就可以获得本题 40 分的解法。

如果你离线下来,
对于 fail 树跑树链剖分,
对于串 b 的所有点先加再减,
你就可以获得 70 分的好成绩。

想一想,这东西其实做法很多:

  • 对于串 b 的所有点,到根这条链先加再减,统计时单点查询。
  • 对于点 a 子树区间加减,做若干个单点查询。
  • 对于串 b 的所有点加一,统计 a 的子树和。
  • 对于 a 单点加一,统计所有 b 的链的区间和。

其实我是卡在了这一步,你考虑第三个好做也好优化,
搞一个回溯 DFS 然后就完成了。

点击查看代码
#include<bits/stdc++.h>
#define pb push_back
#define lb ((x)&(-x))
using namespace std;
const int N=100999;

char S[N];
int s[N][26],cnt,f[N],n,m,len,fa[N],l[N],r[N],ans[N],pos[N];
bool rl[N][26];
vector<int> k[N],qrtp[N],qrid[N],ed[N];
queue<int> q;

int dfn[N],dfnt,sz[N];
void dfs_dfn(int i){
	dfn[i]=++dfnt,sz[i]=1;
	for(int j:k[i])
		dfs_dfn(j),sz[i]+=sz[j];
	// printf("%d: dfn=%d sz=%d\n",i,dfn[i],sz[i]);
}

int t[N];
inline void mf(int x){
	while(x<=dfnt)
		++t[x],x+=lb;
}
inline void imf(int x){
	while(x<=dfnt)
		--t[x],x+=lb;
}
inline int qr(int x){
	int ans=0;
	while(x)
		ans+=t[x],x-=lb;
	return ans;
}

void dfs(int x){
	mf(dfn[x])/*,printf("add %d\n",dfn[x])*/;
	int qrnt=qrtp[x].size();
	for(int i=0;i<qrnt;i++){
		int qd=qrid[x][i],qt=pos[qrtp[x][i]];
		ans[qd]=qr(dfn[qt]+sz[qt]-1)-qr(dfn[qt]-1);
		// printf("[%d,%d]=%d\n",dfn[qt],dfn[qt]+sz[qt]-1,ans[qd]);
	}
	for(int i=0;i<26;i++) if(rl[x][i])
		dfs(s[x][i]);
	imf(dfn[x])/*,printf("del %d\n",dfn[x])*/;
}

int main(){
	scanf("%s",S+1);
	len=strlen(S+1);
	
	for(int i=1,cur=0;i<=len;i++)
		if(S[i]=='B')
			cur=fa[cur]/*,printf("back %d\n",cur)*/;
		else if(S[i]=='P')
			ed[cur].pb(++n),pos[n]=cur/*,printf("note %d\n",cur)*/;
		else{
			if(s[cur][S[i]-'a']==0)
				s[cur][S[i]-'a']=++cnt;
			fa[s[cur][S[i]-'a']]=cur;
			cur=s[cur][S[i]-'a'];
		}
	
	for(int i=0;i<26;i++)
		if(s[0][i]){
			rl[0][i]=1;
			q.push(s[0][i]);
			k[0].pb(s[0][i]);
			// printf("0->%d\n",s[0][i]);
		}
	while(!q.empty()){
		int t=q.front();
		q.pop();
		for(int i=0;i<26;i++)
			if(s[t][i]){
				rl[t][i]=1;
				q.push(s[t][i]);
				f[s[t][i]]=s[f[t]][i];
				k[s[f[t]][i]].pb(s[t][i]);
				// printf("%d->%d\n",s[f[t]][i],s[t][i]);
			}else
				s[t][i]=s[f[t]][i];
	}	
	
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",l+i,r+i);
		qrtp[pos[r[i]]].pb(l[i]);
		qrid[pos[r[i]]].pb(i);
	}
	
	dfs_dfn(0),dfs(0);
	
	for(int i=1;i<=m;i++)
		printf("%d\n",ans[i]);
	return 0;
}
0x04

https://www.luogu.com.cn/problem/P2336
喵;喵喵?喵,喵。喵!
考虑字符集如果小一些就是 AC 机模板题……吗?
这有几个问题:

  • 你没法每次结束之后都 DFS 求解了。
  • 这字符集太大了,只能勉强建一个 Trie 树的阉割 AC 机。

不妨考虑一下 fail 树上处理这个问题。
你有一颗 Trie 树和一颗 fail 树,
你很开心的就是这个东西是静态的,
你肯定是要上 Trie 树上跑一些匹配的,然后标记掉一些节点。

你考虑这东西:

  • 求第一问答案,用姓名匹配模板串的时候,可以考虑你会标记掉一些节点,
    每个节点的父亲都会受影响。
    那你不妨开一个双变量线段树去做树剖,每轮标记变量都可以赋值成零或一,
    然后你去做区间赋值,再加一个神奇的临时转永久标记,应用 lztg 的性质可以处理第一问。
  • 对于每个姓名求答案,一个姓名是一系列点,一个模板串是一个点……

上面是当时想的,题解放 Link 了。
贴个代码。

点击查看代码
#include<bits/stdc++.h>
#define pb push_back
#define lb (x&(-x))
using namespace std;
const int N=200099,K=20;
int n,m,S[N],len;

int t[N][2],dfn[N];

void mf(int x,int i,int lim,int k){
	// printf("md %d %d \n",x,k);
    while(x<=lim)
        t[x][i]+=k,x+=lb;
}

int qr(int x,int i){
    int ans=0;
    while(x)
        ans+=t[x][i],x-=lb;
    return ans;
}

bool cmp(int a,int b){
    return dfn[a]<dfn[b];
}

struct ac{
    queue<int> q;
    vector<int> k[N],vis[N];
    map<int,int> s[N];
    int cnt,dfnt,f[K+5][N],pos[N],dep[N],sz[N];
    
    void ins(int x,bool tp){
        int cur=0;
        for(int i=1;i<=len;i++){
            if(!s[cur].count(S[i]))
                s[cur][S[i]]=++cnt;
            cur=s[cur][S[i]];
            if(!tp) vis[x].pb(cur);
        }
        if(tp) pos[x]=cur;
    }

    void dfs(int x){
        dfn[x]=++dfnt,dep[x]=dep[f[0][x]]+1,sz[x]=1;
        // printf("%d dfn=%d dep=%d\n",x,dfn[x],dep[x]);
        for(int y:k[x])
            dfs(y),sz[x]+=sz[y];
    }

    int lca(int x,int y){
        if(dep[x]<dep[y])
            swap(x,y);
        // printf("q lca %d(%d) %d(%d)\n",x,y,dep[x],dep[y]);
        for(int i=K;i>=0;i--) if(dep[f[i][x]]>=dep[y])
            x=f[i][x];
        if(x==y) return x;
        for(int i=K;i>=0;i--) if(f[i][x]!=f[i][y])
            x=f[i][x],y=f[i][y];
        return f[0][x];
    }
    
    void bd(){
        for(auto i:s[0])
        	k[0].pb(i.second),q.push(i.second);
        while(!q.empty()){
            int t=q.front();
            q.pop();
            for(auto i:s[t]){
                int num=i.first,pos=i.second,it=f[0][t];
                q.push(pos);
                while(it!=0&&s[it].count(num)==0)
                    it=f[0][it];
                f[0][pos]=(s[it].count(num))?s[it][num]:0;
                k[f[0][pos]].pb(pos);
            }
        }
        
        dfs(0);
        for(int i=1;i<=K;i++) for(int j=1;j<=dfnt;j++)
            f[i][j]=f[i-1][f[i-1][j]];
        
        for(int i=1,lst;i<=n;i++){
            sort(vis[i].begin(),vis[i].end(),cmp);
            lst=-1;
            for(int j:vis[i]){
                mf(dfn[j],0,dfnt,1);
                if(~lst)
                    mf(dfn[lca(j,lst)],0,dfnt,-1);
                lst=j;
            }
        }

        for(int i=1;i<=m;i++){
            int j=dfn[pos[i]];
            // for(int _=1;_<=20;_++) printf("%d ",qr(_,0)-qr(_-1,0));
            // printf("\nqsz:[%d %d]\n",j+sz[pos[i]]-1,j);
            printf("%d\n",qr(j+sz[pos[i]]-1,0)-qr(j-1,0));
            mf(j,1,dfnt,1),mf(j+sz[pos[i]],1,dfnt,-1);
        }
        
        for(int i=1,lst,ans;i<=n;i++){
            lst=-1,ans=0;
            for(int j:vis[i]){
                ans+=qr(dfn[j],1);
                if(~lst)
                    ans-=qr(dfn[lca(j,lst)],1);
                lst=j;
            }
            printf("%d ",ans);
        }
    }
}AC;

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x),len=0;
        while(x--)
            scanf("%d",&S[++len]);
        S[++len]=10001;
        scanf("%d",&x);
        while(x--)
            scanf("%d",&S[++len]);
        AC.ins(i,0);
    }
    for(int i=1;i<=m;i++){
        scanf("%d",&len);
        for(int j=1;j<=len;j++)
            scanf("%d",S+j);
        AC.ins(i,1);
    }
    AC.bd();
    return 0;
}
0x05

https://www.luogu.com.cn/problem/P3975

去重

对于 SAM,每个节点对应树上一条从根开始的路径,
然后可以使用 len 进行拓扑排序,在 dag 中 dp 来求每个节点开头的路径数。
显然,这种东西做出来是去重的。

知道了这个后,显然由字典序按位比较的性质,是好做的。

不去重

考虑相同的路径因为所到达后缀不同而不同。
向上转移,你干了什么事呢?
不过在原来已有非空路径上加一条边而已。

只是你会引入一些长度为一的串,
考虑这个串对于不同后缀位置本质不同,所以维护后缀位置数即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1009999,K=26;
typedef long long ll;
char a[N];
int qtp,ask,n;

int s[N][K],f[N],len[N],cnt;
int flg[N];
void bd(){
	f[0]=-1;
	int cur;
	for(int i=1,lst=0,c,son,nson;i<=n;i++,lst=cur){
		len[cur=++cnt]=len[lst]+1;
		c=a[i]-'a';
		while(~lst&&s[lst][c]==0)
			s[lst][c]=cur,lst=f[lst];
		if(lst==-1)
			f[cur]=0;
		else if(len[lst]+1==len[son=s[lst][c]])
			f[cur]=son;
		else{
			f[nson=++cnt]=f[son];
			memcpy(s[nson],s[son],sizeof s[son]);
			len[nson]=len[lst]+1;
			f[cur]=f[son]=nson;
			while(~lst&&s[lst][c]==son)
				s[lst][c]=nson,lst=f[lst];
		}
	}
	for(int i=cur;i!=0;i=f[i])
		flg[i]=1;
}


int pos[N],bus[N],num[N];
ll dp[N];
vector<char> ans;
void work(){	
	for(int i=0;i<=cnt;i++)
		++bus[len[i]];
	for(int i=1;i<=cnt;i++)
		bus[i]+=bus[i-1]; 
	for(int i=cnt;i>=0;i--)
		pos[bus[len[i]]--]=i;
	reverse(pos+1,pos+cnt+2);
	if(qtp==0){
		for(int i=1,x;i<=cnt+1;i++){
			x=pos[i];
			dp[x]=1;
			for(int j=0;j<26;j++) if(s[x][j])
				dp[x]+=dp[s[x][j]];
		}
		// printf("%lld\n",dp[0]-1);
		if(ask>dp[0]-1) puts("-1"),exit(0);
		for(int i=0;;){
			if(ask==1){
				for(int j=0;j<26;j++)
					if(s[i][j]){
						ans.push_back(j+'a');
						break;
					}
				break;					
			}
			for(int j=0;j<26;j++) if(s[i][j]){
				if(dp[s[i][j]]<ask)
					ask-=dp[s[i][j]];
				else{
					--ask;
					ans.push_back(j+'a'),i=s[i][j];
					break;
				}
			}
		}
	}else{
		for(int i=1,x;i<=cnt+1;i++){
			x=pos[i];
			dp[x]=1,num[x]=flg[x];
			for(int j=0;j<26;j++) if(s[x][j]){
				num[x]+=num[s[x][j]];
				dp[x]+=dp[s[x][j]]+num[s[x][j]]-1;
			}
		}
		if(ask>dp[0]-1) puts("-1"),exit(0);
		for(int i=0;;){
			if(ask<=num[i]-flg[i]){
				for(int j=0;j<26;j++)
					if(s[i][j]){
						ans.push_back(j+'a');
						break;
					}
				break;					
			}
			for(int j=0;j<26;j++) if(s[i][j]){
				if(dp[s[i][j]]-1+num[s[i][j]]<ask)
					ask-=dp[s[i][j]]-1+num[s[i][j]];
				else{
					ask-=num[s[i][j]];
					ans.push_back(j+'a'),i=s[i][j];
					break;
				}
			}
		}
	}
	for(char c:ans) putchar(c);
}

int main(){
	scanf("%s%d%d",a+1,&qtp,&ask),n=strlen(a+1);
	bd();
	work();
	return 0;
}

其实没必要这么麻烦。
你维护每个点 endpos 集合大小,把他作为 dp 转移时的修改量即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1009999,K=26;
typedef long long ll;
char a[N];
int qtp,ask,n;

int s[N][K],f[N],len[N],cnt;
int flg[N];
void bd(){
	f[0]=-1;
	int cur;
	for(int i=1,lst=0,c,son,nson;i<=n;i++,lst=cur){
		len[cur=++cnt]=len[lst]+1;
		c=a[i]-'a';
		while(~lst&&s[lst][c]==0)
			s[lst][c]=cur,lst=f[lst];
		if(lst==-1)
			f[cur]=0;
		else if(len[lst]+1==len[son=s[lst][c]])
			f[cur]=son;
		else{
			f[nson=++cnt]=f[son];
			memcpy(s[nson],s[son],sizeof s[son]);
			len[nson]=len[lst]+1;
			f[cur]=f[son]=nson;
			while(~lst&&s[lst][c]==son)
				s[lst][c]=nson,lst=f[lst];
		}
	}
	for(int i=cur;i!=0;i=f[i])
		flg[i]=1;
}


int pos[N],bus[N],num[N];
ll dp[N];
vector<char> ans;
void work(){	
	for(int i=0;i<=cnt;i++)
		++bus[len[i]];
	for(int i=1;i<=cnt;i++)
		bus[i]+=bus[i-1]; 
	for(int i=cnt;i>=0;i--)
		pos[bus[len[i]]--]=i;
	reverse(pos+1,pos+cnt+2);
	
	for(int i=1,x;i<=cnt+1;i++){
		x=pos[i];
		if(qtp==0)
			num[x]=1;
		else{
			num[x]=flg[x];
			for(int j=0;j<26;j++) if(s[x][j])
				num[x]+=num[s[x][j]];
		}
		for(int j=0;j<26;j++) if(s[x][j])
			dp[x]+=dp[s[x][j]]+num[s[x][j]];
	}
	
	if(ask>dp[0]) puts("-1"),exit(0);
	
	for(int i=0;;){
		if(ask<=num[i]){
			for(int j=0;j<26;j++)
				if(s[i][j]){
					ans.push_back(j+'a');
					break;
				}
			break;					
		}
		for(int j=0;j<26;j++) if(s[i][j]){
			if(dp[s[i][j]]+num[s[i][j]]<ask)
				ask-=dp[s[i][j]]+num[s[i][j]];
			else{
				ask-=num[s[i][j]];
				ans.push_back(j+'a'),i=s[i][j];
				break;
			}
		}
	}
	
	for(char c:ans) putchar(c);
}

int main(){
	scanf("%s%d%d",a+1,&qtp,&ask),n=strlen(a+1);
	bd();
	work();
	return 0;
}
posted @ 2026-08-11 19:48  2025ing  阅读(2)  评论(0)    收藏  举报