字符串小祭(二)
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
原链还能看,这里简化一下。
大致思想:
通过对数轮双关键字排序,合并字符串。
每轮合并采取先计排第二关键字,再计排第一关键字的策略。
- 预备排序:把每个字符的排名设为字符本身,进行一轮计数排序,处理出一开始的
rk和sa数组。
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 树上最深的节点,可以通过上跳取后缀,所以包含所有子串。
- 他在 trie 图上看,等价于所有后缀的 AC 自动机,
自己看一下 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;
}

浙公网安备 33010602011771号