【字符串】字符串相关

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

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[i-kmp[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\),直接上暴力即可。

对于 \(i\le r\),显然有 \(s[i-l+1,z[l]]=s[i,r]\),故令 \(z[i]=\min(z[i-l+1],r-i+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[i-d[i]+1,i+d[i]-1]\) 是回文串。

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

对于 \(i>r\),直接暴力。

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

4. Suffix Array 后缀数组(SA)

4.1 模板

P3809 【模板】后缀排序

$O(n\log^2n)$ 版本(直接排序)
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(n\log n)$ 版本(基数排序优化)
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;
}

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

  • \(sa_i\) 表示排名为 \(i\) 的后缀编号。

  • \(rk_i\) 表示编号为 \(i\) 的后缀排名。

于是就有 \(sa_{rk_i}=rk_{sa_i}=i\)(禁止套娃)。OI-wiki 里面有个很好的图:

image

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

假设我们当前知道了每个长为 \(2^{w-1}\) 的子串的排名,记为 \(rk_{i,{w-1}}\)

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

image

直接双关键字排序是 \(O(\log n)\times O(n\log n)=O(n\log^2n)\) 的。注意到 \(rk\) 的值域是 \(O(n)\) 的,可以用基数排序优化至 \(O(\log n)\times O(n)=O(n\log n)\)

4.2 height 数组

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

定义:

  • \(suf_i\):后缀 \(s[i,n]\)

  • \(\operatorname{lcp}(i,j)\)排名\(i\) 的后缀与排名\(j\) 的后缀的最长公共前缀,即 \(\operatorname{lcp}(suf_{sa_i},suf_{sa_j})\)

  • \(ht_i\)\(ht_i=\operatorname{lcp}(i-1,i)\)

性质:

  1. \(\operatorname{lcp}(i,j)=\operatorname{lcp}(j,i)\)

  2. \(\operatorname{lcp}(i,i)=\operatorname{len}(suf_{sa_i})=n-sa_i+1\)

  3. \(\operatorname{lcp}(i,j)=\min(\operatorname{lcp}(i,k),\operatorname{lcp}(k,j))\)\(i\le k\le j\)

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

首先根据后缀排序的定义,\(i\)\(j\) 一段相同长度的前缀 \(p_i,p_j\),总有 \(p_i\le p_j\) (由字典序比较可知),\(k\) 同理。

image

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

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

\(\operatorname{lcp}(i,j)\) 为图中 \(i,j\) 橙色的一段前缀,对于 \(k\) 的一段相同长度的前缀,总有 \(p_i\le p_k\le p_j\),又有 \(p_i=p_j=\operatorname{lcp}(i,j)\),故 \(\operatorname{lcp}(i,j)\le\min(\operatorname{lcp}(i,k),\operatorname{lcp}(k,j))\)

综上得证。

于是可知,\(\operatorname{lcp}(i,j)=\min\limits_{k=i+1}^j\operatorname{lcp}(k-1,k)=\min\limits_{k=i+1}^jht_k\)

但是 \(height\) 数组怎么求呢?有一个关键的性质:\(ht_{rk_i}\ge ht_{rk_{i-1}}-1\),再乱证一下:

image

假设 \(rk_{i-1}< rk_i\),令 \(k=sa_{rk_{i-1}-1}\)

\(suf_k\)\(suf_{i-1}\) 的第一个字母不同,则 \(ht_{rk_{i-1}}=0\),上述结论显然成立。

\(suf_k\)\(suf_{i-1}\) 的第一个字母相同,则我们可以去掉它们的第一个字母,那么 \(suf_{k}\)\(suf_{i-1}\) 就变成了 \(suf_{k+1}\)\(suf_{i}\)

显然 \(\operatorname{lcp}(k+1,i)=ht_{rk_{i-1}}-1\)。又有 \(\operatorname{lcp}(k+1,i)\le \operatorname{lcp}(i-1,i)\),即 \(ht_{rk_i}\ge ht_{rk_{i-1}}-1\)

于是我们可以写出下面构造 \(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\) 相似的子串。我们发现,对于 \(ht_i<r\),我们可以把 \(i\)\(i-1\) 划分为两个连通块,同一块内的元素都是可以组合的,因为它们的 \(\operatorname{lcp}\) 都不小于 \(r\)。再者我们发现,随着 \(r\) 增大,连通块的个数不断减小,当 \(r=n\) 时,每个元素自成一个连通块。于是我们考虑倒着求答案,每次把 \(ht_i=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]生成魔咒

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

先考虑对整个字符串的本质不同子串个数怎么求,有结论 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=2}^{n}ht_i\),证明一下:

image

\(i=1\) 排在第一位,则显然它的每个前缀在前面都没出现过,贡献为 \(len_{sa_{i}}\)

对于 \(j=2\),它在前面出现过的前缀个数即为 \(\operatorname{lcp}(i,j)=ht_{j}\),即为图中橙色部分,贡献为 \(len_{sa_j}-ht_j\)

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

以此类推,总贡献为 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=2}^{n}ht_i\)

再来考虑这道题怎么做,每在后面增加一个字符,则会影响所有后缀。我们不妨把整个串翻转过来,变为在前面加入字符,于是每次操作只会增加一个后缀而不影响其他的后缀,且贡献不变。加字符的操作变成减字符的操作更好做,每次合并 \(ht_{rk_i-1}\)\(ht_{rk_i}\),根据性质 \(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。

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

若当前不选,则转移到 \(f_{i,j+1}\);若当前选择,则要选择极长的一段,即 \(s\)\(t\) 那一段后缀的 \(\operatorname{lcp}\),这个可以通过把 \(s,t\) 合并成一个字符串,然后用 st 表维护区间 \(\min\) 即可。设 \(\operatorname{lcp}\)\(L\),则转移到 \(f_{i,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]差异

原式 = \(\dfrac{n(n-1)(n+1)}{2}-2\times\sum \operatorname{lcp}(T_i,T_j)\)。左半部分可以先不管他,考虑怎么求右半部分,他显然可以化为:

\[\sum \operatorname{lcp}(T_i,T_j)=\sum\min\limits_{k=i+1}^{j}ht_k \]

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

点击查看代码
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\) 的串与前面的本质不同子串个数就是 \(len_i-ht_i\),枚举一下就行了。

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

因为不用考虑相同子串,故排名在 \(i\) 前的后缀构成的子串数量就是 \(\sum\limits_{rk_j<rk_i}len_j\)。但是还要考虑排名在它后面的后缀,它们构成的子串字典序比 \(i\) 小的个数就是它和 \(i\)\(\operatorname{lcp}\)。故 \(i\) 的排名就是:

\[(\sum\limits_{rk_j<rk_i}len_j+\sum\limits_{rk_j>rk_i}\min\limits_{k=i+1}^{j}ht_k)+1 \]

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

总共复杂度是 \(O(n\log n)\) 的,比 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 传送门

\(\rm Bd = border\)

\(\bullet\color{blue}{\text{ 定理(1):}}\) \(\rm Bd(S)=mxBd(S)+Bd(mxBd(S))\)

image

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

\(\bullet\color{blue}{\text{ 定理(2):}}\)\(p,q\)\(S\) 的周期,且 \(p+q\leq n\) ,则 \(\gcd(p,q)\) 也是 \(S\) 的周期。

\(\bullet\color{blue}{\text{ 定理(3):}}\)\(S\)\(T\) 的前缀,且 \(T\) 有周期 \(a\)\(S\) 有整周期 \(b\),满足 \(b|a\)\(\ |S|\geq a\),则 \(T\) 也有周期 \(b\)

image

\(\bullet\color{blue}{\text{ 定理(4):}}\)\(S\) 如下图匹配,则表示 $S_1∪S_2 $有长度为 \(d\) 的周期,也即有长度为 \(|S|-d\)\(\rm Bd\)

image

\(\bullet\color{blue}{\text{ 定理(5):}}\)\(2|S|\geq|T|\),则 \(S\)\(T\) 中的匹配位置必为等差序列。

image

\(\bullet\color{blue}{\text{ 定理(6):}}\) \(S\) 的长度大于 \(\dfrac{n}{2}\)\(\rm Bd\) 长度构成一个等差序列。

\(\bullet\color{blue}{\text{ 定理(7):}}\) 一个串 \(S\) 的所有 \(\rm Bd\) 按长度排序后,可以被划分成 \(O(\log n)\) 个等差序列。

5.2 例题

P3435 [POI2006] OKR-Periods of Words

对于满足条件的 \(Q\)\(Q\)\(a\) 相差的部分就是 \(a\)\(\rm Bd\),我们要求最大周期长度就是求最小非空 \(\rm Bd\)

考虑对于每个前缀暴力跳 \(fail\) 数组(kmp 那个),跳到不能再跳的 \(\rm Bd\) 即为所求。考虑优化一下,对于前缀 \(i\),若它的最小非空 \(\rm 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] 动物园

显然有个暴力的方法:对每个前缀不断跳 \(\rm 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 @ 2022-11-09 11:42  RuntimeErr  阅读(47)  评论(0编辑  收藏  举报