【字符串】字符串相关

啥都不会,重新学。先把模板摆上来,有时间写写例题。

1. KMP 算法

1.1. 模板

P3375 【模板】KMP字符串匹配

点击查看代码
//核心代码
int main(){
    scanf("%s %s",s1+1,s2+1);
    n=strlen(s1+1),m=strlen(s2+1);
//---------------------------------
    for(int i=2,j=0;i<=m;++i){
        while(j&&s2[j+1]!=s2[i])j=kmp[j];
        if(s2[j+1]==s2[i])++j;
        kmp[i]=j;
    }
    for(int i=1,j=0;i<=n;++i){
        while(j&&s1[i]!=s2[j+1])j=kmp[j];
        if(s1[i]==s2[j+1])++j;
        if(j==m){printf("%d\n",i-m+1);j=kmp[j];}
    }
//---------------------------------
    for(int i=1;i<=m;++i)printf("%d ",kmp[i]);
    return 0;
}

其中 kmp[i] 表示模式串 t 的前缀 t[1,i]最长相等真前缀和真后缀的长度,即有 t[1,kmp[i]]=t[ikmp[i]+1,i](kmp[i]<i)。其原理就是通过自己匹配自己求出失配位置。

1.2 例题

P3435 [POI2006] OKR-Periods of Words

见“border 理论”。

2. Z 函数(exKMP)

2.1. 模板

P5410 【模板】扩展 KMP(Z 函数)

点击查看代码
//z 函数构造方法
    z[1]=m;
    for(int i=2,l=0,r=0;i<=m;++i){
        if(i<=r)z[i]=min(z[i-l+1],r-i+1);
        while(i+z[i]<=m&&s[i+z[i]]==s[1+z[i]])++z[i];
        if(i+z[i]-1>r)l=i,r=i+z[i]-1;
    }

其中 z[i] 表示字符串 s 与其后缀 s[i,n] 的 LCP 长度,即有 s[1,z[i]]=s[i,i+z[i]1]

其原理就是通过已匹配段 [l,r] 推出 z[i],其中 s[1,z[l]]=s[l,r]

对于 i>r,直接上暴力即可。

对于 ir,显然有 s[il+1,z[l]]=s[i,r],故令 z[i]=min(z[il+1],ri+1)(能保证在匹配段内正确)然后暴力匹配。

3. manacher 算法

3.1. 模板

P3805 【模板】manacher 算法

点击查看代码
// 字符串通过插入分隔字符变成长为奇数
    void get(){
	s[1]='?';s[cnt=2]='#';register char c=getchar();
	while(c<'a'||c>'z')c=getchar();
	while(c>='a'&&c<='z')s[++cnt]=c,s[++cnt]='#',c=getchar();s[cnt+1]='!';
    }
    int main(){
	get();
	for(int i=2,l=0,r=-1;i<=cnt;++i){
		d[i]=i>r?1:min(d[l+r-i],r-i+1);
		while(s[i-d[i]]==s[i+d[i]])++d[i];
		ans=max(d[i],ans);if(i+d[i]>r)r=i+d[i]-1,l=i-d[i]+1;
	}
	printf("%d",ans-1);
	return 0;
    }

其中 d[i] 表示以 i 为中心的回文半径长度,即有 s[id[i]+1,i+d[i]1] 是回文串。

其构造思路跟上面差不多,考虑已匹配段 [l,r],满足 s[l,r] 是回文串。

对于 i>r,直接暴力。

对于 ir,根据回文对称性,有以 l+ri 为中心的回文串与 i 对称,则我们令 d[i]=min(d[l+ri],ri+1)(长度超出匹配段的部分不能保证对称)然后暴力即可。

4. Suffix Array 后缀数组(SA)

4.1 模板

P3809 【模板】后缀排序

O(nlog2n) 版本(直接排序)
char s[N];
int w,len,sa[N],rk[N<<1],oldrk[N<<1];//注意开双倍空间

inline bool cmp(int a,int b){
	return rk[a]==rk[b]?rk[a+w]<rk[b+w]:rk[a]<rk[b];
}

int main(){
	scanf("%s",s+1);len=strlen(s+1);
//------------------------------------------------
	for(int i=1;i<=len;++i)sa[i]=i,rk[i]=s[i];
	for(w=1;w<len;w<<=1){
		sort(sa+1,sa+len+1,cmp);
		memcpy(oldrk,rk,sizeof rk);
		for(int i=1,p=0;i<=len;++i){
			if(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w])rk[sa[i]]=p;
			else rk[sa[i]]=++p;
		}
	}
//------------------------------------------------
	for(int i=1;i<=len;++i)printf("%d ",sa[i]);
	return 0;
}
O(nlogn) 版本(基数排序优化)
const int N=1e6+10;

int n;
char s[N];
int buc[N],rk[N],ork[N],sa[N],id[N],pid[N];

//减少内存访问优化
bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}

int main(){
    scanf("%s",s+1);n=strlen(s+1);
//------------------------------------------------
    int m=1<<7,num=0;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;//由于要稳定排序故倒序枚举

    for(int w=1;;w<<=1,m=num,num=0){
        //对第二关键字排序优化
        for(int i=n-w+1;i<=n;++i)id[++num]=i;//i+w>n的话一定排在前面(第二关键字为空)
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++num]=sa[i]-w;//这里枚举的sa[i]实际上是sa[i+w]即第二关键字
        //对第一关键字排序
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;//用pid代替,使访问连续
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];//稳定排序,如果第一关键字相同则按照原来排好的第二关键字顺序
        swap(rk,ork);num=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],w)?num:++num;//离散化
        if(num==n)break;//排好就退出
    }
//------------------------------------------------
    for(int i=1;i<=n;++i)printf("%d ",sa[i]);
    return 0;
}

后缀排序里主要用到两个数组:

  • sai 表示排名为 i 的后缀编号。

  • rki 表示编号为 i 的后缀排名。

于是就有 sarki=rksai=i(禁止套娃)。OI-wiki 里面有个很好的图:

image

后缀排序的过程运用了倍增的思想:

假设我们当前知道了每个长为 2w1 的子串的排名,记为 rki,w1

则我们可以通过 rki,w1rki+2w1,w1 双关键字排序,求出 rki,w(若 i+2w1>n 则规定 rki,w 为负无穷,它一定排在前面)。OI-wiki 又有个很好的图:

image

直接双关键字排序是 O(logn)×O(nlogn)=O(nlog2n) 的。注意到 rk 的值域是 O(n) 的,可以用基数排序优化至 O(logn)×O(n)=O(nlogn)

4.2 height 数组

可以说 SA 算法的目的就是求出 height 数组。下面给出几个定义和性质:

定义:

  • sufi:后缀 s[i,n]

  • lcp(i,j)排名i 的后缀与排名j 的后缀的最长公共前缀,即 lcp(sufsai,sufsaj)

  • htihti=lcp(i1,i)

性质:

  1. lcp(i,j)=lcp(j,i)

  2. lcp(i,i)=len(sufsai)=nsai+1

  3. lcp(i,j)=min(lcp(i,k),lcp(k,j))ikj

这个需要证明一下(写的有点不太严谨感性理解):

首先根据后缀排序的定义,ij 一段相同长度的前缀 pi,pj,总有 pipj (由字典序比较可知),k 同理。

image

lcp(i,k) 为图中橙色一段前缀,lcp(k,j) 为图中蓝色一段前缀。

则图中 j 橙色的一段前缀也一定与 i 的前缀相等,于是我们证明了 lcp(i,j)min(lcp(i,k),lcp(k,j))

lcp(i,j) 为图中 i,j 橙色的一段前缀,对于 k 的一段相同长度的前缀,总有 pipkpj,又有 pi=pj=lcp(i,j),故 lcp(i,j)min(lcp(i,k),lcp(k,j))

综上得证。

于是可知,lcp(i,j)=mink=i+1jlcp(k1,k)=mink=i+1jhtk

但是 height 数组怎么求呢?有一个关键的性质:htrkihtrki11,再乱证一下:

image

假设 rki1<rki,令 k=sarki11

sufksufi1 的第一个字母不同,则 htrki1=0,上述结论显然成立。

sufksufi1 的第一个字母相同,则我们可以去掉它们的第一个字母,那么 sufksufi1 就变成了 sufk+1sufi

显然 lcp(k+1,i)=htrki11。又有 lcp(k+1,i)lcp(i1,i),即 htrkihtrki11

于是我们可以写出下面构造 height 数组的代码。

点击查看代码
for(int i=1,k=0;i<=n;++i){
    if(k)--k;
    int j=sa[rk[i]-1];
    while(s[i+k]==s[j+k])++k;
    ht[rk[i]]=k;
}

注意到 k 指针最多移动 2n 次,故时间复杂度 O(n)

4.3 例题:

P2178 [NOI2015] 品酒大会

“子串相等”可以转化为“后缀的前缀相等”,于是可以用 height 数组来做。

考虑排序后的后缀,当前要求 r 相似的子串。我们发现,对于 hti<r,我们可以把 ii1 划分为两个连通块,同一块内的元素都是可以组合的,因为它们的 lcp 都不小于 r。再者我们发现,随着 r 增大,连通块的个数不断减小,当 r=n 时,每个元素自成一个连通块。于是我们考虑倒着求答案,每次把 hti=r 的两个联通块合并,这个可以用并查集实现。

注意权值有正有负,要维护块内最大最小次大次小。

点击查看代码
#define pb push_back
typedef long long ll;
typedef pair<ll,ll> pr;

const int N=3e5+10;
const ll inf=1e18;

int n;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];

inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
    int m=1<<7,p=0;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
    for(int w=1;;w<<=1,m=p,p=0){
        for(int i=n-w+1;i<=n;++i)id[++p]=i;
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
        swap(ork,rk);p=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n)break;
    }
    for(int i=1,k=0;i<=n;++i){
        if(k)--k;
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])++k;
        ht[rk[i]]=k;
    }
}

vector<int> h[N];
ll a[N],fa[N],siz[N],mx1[N],mx2[N],mn1[N],mn2[N];
ll res=0,maxv=-inf;
pr ans[N];

int fnd(int x){return x!=fa[x]?fa[x]=fnd(fa[x]):x;}
inline ll get(int x){return 1ll*x*(x-1)/2;}

pr calc(int v){
    for(int x:h[v]){
        int l=fnd(x-1),r=fnd(x);
        if(siz[l]>siz[r])swap(l,r);
        res-=get(siz[l])+get(siz[r]);
        siz[r]+=siz[l],fa[l]=r;
        res+=get(siz[r]);
        if(mx1[l]>=mx1[r]){
            mx2[r]=max(mx1[r],mx2[l]);
            mx1[r]=mx1[l];
        }else if(mx1[l]>mx2[r])mx2[r]=mx1[l];
        if(mn1[l]<=mn1[r]){
            mn2[r]=min(mn1[r],mn2[l]);
            mn1[r]=mn1[l];
        }else if(mn1[l]<mn2[r])mn2[r]=mn1[l];
        maxv=max(maxv,max(mx1[r]*mx2[r],mn1[r]*mn2[r]));
    }
    return maxv==-inf?make_pair(0ll,0ll):make_pair(res,maxv);
}

int main(){
    scanf("%d%s",&n,s+1);
    for(int i=1;i<=n;++i)scanf("%lld",&a[i]);
    build();
    for(int i=1;i<=n;++i){
        fa[i]=i,siz[i]=1;
        mx1[i]=mn1[i]=a[sa[i]];
        mx2[i]=-inf,mn2[i]=inf;
    }
    for(int i=2;i<=n;++i)h[ht[i]].pb(i);
    for(int i=n-1;~i;--i)ans[i]=calc(i);
    for(int i=0;i<n;++i)printf("%lld %lld\n",ans[i].first,ans[i].second);
    return 0;
}

P4070 [SDOI2016]生成魔咒

对每个前缀求本质不同子串个数

先考虑对整个字符串的本质不同子串个数怎么求,有结论 n(n+1)2i=2nhti,证明一下:

image

i=1 排在第一位,则显然它的每个前缀在前面都没出现过,贡献为 lensai

对于 j=2,它在前面出现过的前缀个数即为 lcp(i,j)=htj,即为图中橙色部分,贡献为 lensajhtj

对于 k=3,它在前面出现过的前缀个数为 htk,即为图中蓝线部分,之后的前缀在前面一定没出现过。考虑反证法:若之后的前缀在前面有出现过(设为图中绿色部分),则有 lcp(i,k) 绿色部分长度。但是根据性质 3 又有 lcp(i,k)lcp(j,k),矛盾,故原命题正确。则贡献为 lensakhtk

以此类推,总贡献为 n(n+1)2i=2nhti

再来考虑这道题怎么做,每在后面增加一个字符,则会影响所有后缀。我们不妨把整个串翻转过来,变为在前面加入字符,于是每次操作只会增加一个后缀而不影响其他的后缀,且贡献不变。加字符的操作变成减字符的操作更好做,每次合并 htrki1htrki,根据性质 3 可求得新的贡献。那么维护一个双链表就可以了。

点击查看代码
const int N=1e5+10;
typedef long long ll;

int n,tot,s[N],tmp[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int pre[N],nxt[N];
ll ans[N],sum=0;

inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
    int m=tot,p=0;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
    for(int w=1;;w<<=1,m=p,p=0){
        for(int i=n-w+1;i<=n;++i)id[++p]=i;
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
        swap(ork,rk);p=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n)break;
    }
    for(int i=1,k=0;i<=n;++i){
        if(k)--k;
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])++k;
        ht[rk[i]]=k;
    }
}


int main(){
    read(n);
    for(int i=n;i;--i)read(s[i]),tmp[i]=s[i];
    sort(tmp+1,tmp+n+1);tot=unique(tmp+1,tmp+n+1)-tmp-1;
    for(int i=1;i<=n;++i)s[i]=lower_bound(tmp+1,tmp+tot+1,s[i])-tmp;
    build();
    for(int i=1;i<=n;++i){
        sum+=n-sa[i]+1-ht[i];
        pre[i]=i-1,nxt[i]=i+1;
    }nxt[0]=1,pre[n+1]=n;ans[n]=sum;
    for(int i=1;i<n;++i){
        int k=rk[i],j=nxt[k];
        sum-=n-i+1-ht[k];sum+=ht[j];
        ht[j]=min(ht[k],ht[j]);
        pre[j]=pre[k],nxt[pre[k]]=j;
        sum-=ht[j];
        ans[n-i]=sum;
    }
    for(int i=1;i<=n;++i)printf("%lld\n",ans[i]);
    return 0;
}

CF822E Liar

显然可以贪心,尽量让极长的一段来匹配。注意到 x 较小,可以 dp。

fi,j 表示从 s[1,j] 中选取不超过 i 个子串,最多能匹配到 t 的第几位。

若当前不选,则转移到 fi,j+1;若当前选择,则要选择极长的一段,即 st 那一段后缀的 lcp,这个可以通过把 s,t 合并成一个字符串,然后用 st 表维护区间 min 即可。设 lcpL,则转移到 fi,j+L

点击查看代码
const int N=2e5+10;

int n,lens,lent,tot=0,x;
char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int lg[N],st[N][18],f[31][N];

inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
    int m=1<<7,p=0;n=tot;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
    for(int w=1;;w<<=1,m=p,p=0){
        for(int i=n-w+1;i<=n;++i)id[++p]=i;
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
        swap(ork,rk),p=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n)break;
    }
    for(int i=1,k=0;i<=n;++i){
        if(k)--k;
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])++k;
        ht[rk[i]]=k;
    }
    for(int i=1;i<=n;++i)st[i][0]=ht[i];
    for(int i=2;i<=n;++i)lg[i]=lg[i>>1]+1;
    for(int j=1;j<=lg[n];++j){
        for(int i=1;i+(1<<j)-1<=n;++i){
            st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
        }
    }
}
int lcp(int i,int j){
    if(i>lens||j>lent)return 0;
    i=rk[i],j=rk[j+lens+1];
    if(i>j)swap(i,j);++i;
    int p=lg[j-i+1];
    return min(st[i][p],st[j-(1<<p)+1][p]);
}

int main(){
    scanf("%d",&lens);
    for(int i=1;i<=lens;++i)scanf(" %c",&s[++tot]);s[++tot]='#';
    scanf("%d",&lent);
    for(int i=1;i<=lent;++i)scanf(" %c",&s[++tot]);
    build();scanf("%d",&x);
    for(int i=1;i<=x;++i){
        for(int j=0;j<lens;++j){
            int L=lcp(j+1,f[i-1][j]+1);
            f[i][j+1]=max(f[i][j+1],f[i][j]);
            f[i][j+L]=max(f[i][j+L],f[i-1][j]+L);
        }
    }
    if(f[x][lens]==lent)printf("YES\n");
    else printf("NO\n");
    return 0;
}

P4248 [AHOI2013]差异

原式 = n(n1)(n+1)22×lcp(Ti,Tj)。左半部分可以先不管他,考虑怎么求右半部分,他显然可以化为:

lcp(Ti,Tj)=mink=i+1jhtk

这事实上是个很典的做法:考虑枚举 hti 作为最小值,求出它向左和向右最多能扩展到哪里,计算一下经过它的贡献即可。这个过程可以用单调栈维护。

点击查看代码
const int N=5e5+10;
#define int long long

int n;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];

inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
    int m=1<<7,p=0;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
    for(int w=1;;w<<=1,m=p,p=0){
        for(int i=n-w+1;i<=n;++i)id[++p]=i;
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
        swap(ork,rk);p=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n)break;
    }
    for(int i=1,k=0;i<=n;++i){
        if(k)--k;
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])++k;
        ht[rk[i]]=k;
    }
}

int stk[N],top=0;
int l[N],r[N];

signed main(){
    scanf("%s",s+1);n=strlen(s+1);
    build();
    int ans=1ll*n*(n-1)*(n+1)/2;
    for(int i=1;i<=n;++i){
        while(top&&ht[stk[top]]>ht[i])r[stk[top--]]=i;
        l[i]=stk[top],stk[++top]=i;
    }
    while(top)r[stk[top--]]=n+1;
    for(int i=2;i<=n;++i)ans-=2ll*(i-l[i])*(r[i]-i)*ht[i];
    printf("%lld\n",ans);
    return 0;
}

P3975 [TJOI2015]弦论

几道例题里唯一一道自己口胡出来对的题(

对于 t=0 的询问很 naive,排名为 i 的串与前面的本质不同子串个数就是 lenihti,枚举一下就行了。

对于 t=1 的询问有点复杂,考虑一个串的排名是啥:

因为不用考虑相同子串,故排名在 i 前的后缀构成的子串数量就是 rkj<rkilenj。但是还要考虑排名在它后面的后缀,它们构成的子串字典序比 i 小的个数就是它和 ilcp。故 i 的排名就是:

(rkj<rkilenj+rkj>rkimink=i+1jhtk)+1

这是显然可以二分的,我们要找的目标后缀就是排名最大且小于 k 的那个后缀。现在的问题变成要输出的长度为多少,这个简单。设当前枚举的长度为 l,考虑排名在它后面的与它存在长度为 l 的公共前缀的后缀个数(即 lcpl,记得还要加上自己本身),这也是可二分的,如果加上这个个数后排名大于 k 则退出即可。

总共复杂度是 O(nlogn) 的,比 SAM 做法稍劣。

点击查看代码
const int N=5e5+10;
#define int long long

int n,t,k;char s[N];
int sa[N],rk[N],ork[N],buc[N],id[N],pid[N],ht[N];
int st[20][N],lg[N];

inline bool cmp(int a,int b,int w){return ork[a]==ork[b]&&ork[a+w]==ork[b+w];}
void build(){
    int m=1<<7,p=0;
    for(int i=1;i<=n;++i)buc[rk[i]=s[i]]++;
    for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
    for(int i=n;i;--i)sa[buc[rk[i]]--]=i;
    for(int w=1;;w<<=1,m=p,p=0){
        for(int i=n-w+1;i<=n;++i)id[++p]=i;
        for(int i=1;i<=n;++i)if(sa[i]>w)id[++p]=sa[i]-w;
        for(int i=1;i<=m;++i)buc[i]=0;
        for(int i=1;i<=n;++i)buc[pid[i]=rk[id[i]]]++;
        for(int i=1;i<=m;++i)buc[i]+=buc[i-1];
        for(int i=n;i;--i)sa[buc[pid[i]]--]=id[i];
        swap(ork,rk);p=0;
        for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
        if(p==n)break;
    }
    for(int i=1,k=0;i<=n;++i){
        if(k)--k;
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])++k;
        ht[rk[i]]=k;
    }
    lg[0]=-1;
    for(int i=1;i<=n;++i)st[0][i]=ht[i],lg[i]=lg[i>>1]+1;
    for(int i=1;i<=lg[n];++i){
        for(int j=1;j+(1<<i)-1<=n;++j){
            st[i][j]=min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
        }
    }
}

int query(int l,int r){
    int p=lg[r-l+1];
    return min(st[p][l],st[p][r-(1<<p)+1]);
}

bool check(int mid){
    int sum=0;
    for(int i=1;i<mid;++i)sum+=n-sa[i]+1;
    for(int i=mid+1;i<=n;++i)sum+=query(mid+1,i);
    return sum+1>=k;
}
int fnd(int p,int l,int r,int w){
    int mid,res=p;
    while(l<=r){
        mid=(l+r)>>1;
        if(query(p+1,mid)>=w)res=mid,l=mid+1;
        else r=mid-1;
    }return res-p;
}

signed main(){
    scanf("%s%lld%lld",s+1,&t,&k),n=strlen(s+1);
    build();
    int tot=0;
    if(!t){
        for(int i=1;i<=n;++i){
            int len=n-sa[i]+1-ht[i];
            if(tot+len>=k){
                int pos=sa[i];k-=tot-ht[i];
                while(k--)printf("%c",s[pos++]);
                return 0;
            }tot+=len;
        }printf("-1\n");
    }else {
        if(k>n*(n+1)/2)return printf("-1\n"),0;
        int l=1,r=n,mid;
        while(l<r){
            int mid=(l+r)>>1;
            if(check(mid))r=mid;
            else l=mid+1;
        }--r;
        int len=n-sa[r]+1,tot=0;
        for(int i=1;i<r;++i)tot+=n-sa[i]+1;
        for(int i=1;i<=len;++i){
            int cnt=fnd(r,r+1,n,i);
            printf("%c",s[sa[r]+i-1]);tot+=cnt+1;
            if(tot>=k)break;
        }
    }
    return 0;
}

5. border 理论

5.1 相关定理

剽窃自/证明见:cmd 传送门

Bd=border

 定理(1): Bd(S)=mxBd(S)+Bd(mxBd(S))

image

即,S 的所有 Bd 等于最大 Bd 加上最大 Bd 的所有 Bd。kmp 里的 fail 数组求的就是最大 Bd

 定理(2):p,qS 的周期,且 p+qn ,则 gcd(p,q) 也是 S 的周期。

 定理(3):ST 的前缀,且 T 有周期 aS 有整周期 b,满足 b|a |S|a,则 T 也有周期 b

image

 定理(4):S 如下图匹配,则表示 S1S2有长度为 d 的周期,也即有长度为 |S|dBd

image

 定理(5):2|S||T|,则 ST 中的匹配位置必为等差序列。

image

 定理(6): S 的长度大于 n2Bd 长度构成一个等差序列。

 定理(7): 一个串 S 的所有 Bd 按长度排序后,可以被划分成 O(logn) 个等差序列。

5.2 例题

P3435 [POI2006] OKR-Periods of Words

对于满足条件的 QQa 相差的部分就是 aBd,我们要求最大周期长度就是求最小非空 Bd

考虑对于每个前缀暴力跳 fail 数组(kmp 那个),跳到不能再跳的 Bd 即为所求。考虑优化一下,对于前缀 i,若它的最小非空 Bd 存在,我们可以直接将 fail[i] 设成它,可以缩短路径。

点击查看代码
const int N=1e6+10;

int n;char s[N];
int fail[N];

int main(){
    scanf("%d%s",&n,s+1);
    for(int i=2,j=0;i<=n;++i){
        while(j&&s[i]!=s[j+1])j=fail[j];
        fail[i]=s[i]==s[j+1]?++j:j;
    }long long ans=0;
    for(int i=1;i<=n;++i){
        int j=i;while(fail[j])j=fail[j];
        ans+=i-j;
        if(fail[i])fail[i]=j;
    }printf("%lld\n",ans);
    return 0;
}

P2375 [NOI2014] 动物园

显然有个暴力的方法:对每个前缀不断跳 Bd,知道其长度小于等于前缀长度的一半,那么 num[i] 就等于它还能继续跳的次数,这个可以在建 fail 时递推出。考虑怎么优化:

类似构建 fail 数组的过程,我们可以在 fail 上匹配自身,然后跑上面的暴力。由于我们减少了重复递归,可以证明是均摊 O(n) 的。

点击查看代码
const int N=1e6+10;
typedef long long ll;
const ll mod=1e9+7;

int n;char s[N];
int fail[N];
ll cnt[N],ans=1;

void solve(){
    scanf("%s",s+1);n=strlen(s+1);
    memset(fail,0,sizeof fail);cnt[0]=0,cnt[1]=1;
    for(int i=2,j=0;i<=n;++i){
        while(j&&s[j+1]!=s[i])j=fail[j];
        j+=(s[j+1]==s[i]);
        fail[i]=j;cnt[i]=cnt[j]+1;
    }ans=1;
    for(int i=2,j=0;i<=n;++i){
        while(j&&s[j+1]!=s[i])j=fail[j];
        j+=(s[j+1]==s[i]);
        while((j<<1)>i)j=fail[j];
        ans=ans*(cnt[j]+1)%mod;
    }printf("%d\n",ans);
}
posted @   RuntimeErr  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示