理论知识
详见 OI Wiki 。
模板
后缀排序
一切有关后缀数组问题的必备板子。
求后缀数组模板题,OI Wiki 有详解 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e6+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N];
char s[N];
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
cin>>s+1;
n=strlen(s+1);
init();
for(int i=1;i<=n;i++)
cout<<sa[i]<<' ';
}
可重叠最长重复子串
求 \(height\) 数组模板题,OI Wiki 有详解如何求 \(height\) 数组。
即求 \(\max\limits_{i=1}^n\{height_i\}\) 。
注意 \(height_1=0\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],ans;
char s[N];
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n);
cin>>s+1;
init();
for(int i=1;i<=n;i++)
ans=max(ans,height[i]);
cout<<ans;
}
应用
\(height\) 数组基本应用
不同子串个数
所有子串的个数为 \(\dfrac{n(n+1)}{2}\) ,其中重复的有 \(\sum\limits_{i=1}^nheight_i\) ,故答案为 \(\dfrac{n(n+1)}{2}-\sum\limits_{i=1}^nheight_i\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,q,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],ans;
char s[N];
int sta[N],top,a[N],b[N];
struct aa
{
int l,r,id;
}e[N];
bool cmp(aa a,aa b) {return a.l==b.l?a.r>b.r:a.l<b.l;}
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n);
cin>>s+1;
init();
for(int i=1;i<=n;i++)
ans+=height[i];
cout<<n*(n+1)/2-ans;
}
Milk Patterns G
求出现最少 \(k\) 次的子串的最大长度。
出现至少 \(k\) 次说明后缀排序后至少有连续 \(k\) 个后缀以这个子串为公共串。
即求出没连续 \(k-1\) 个 \(height_i\) 的最小值,这些最小值中的最大值即为所求。
可以用单调队列 \(O(n)\) 解决,当然及时不用也足以 \(AC\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e6+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,K,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],ans;
int s[N];
deque<int>q;
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=1000000,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n),read(K);
K--;
for(int i=1;i<=n;i++) read(s[i]);
init();
for(int i=1;i<=n;i++)
{
while(!q.empty()&&height[q.front()]>=height[i]) q.pop_front();
q.push_front(i);
while(!q.empty()&&q.back()<=i-K) q.pop_back();
if(i>=K) ans=max(ans,height[q.back()]);
}
write(ans);
}
结合 \(ST\) 表
我们知道 \(LCP(sa_l,sa_r)=\min\limits_{i=l+1}^r\{height_i\}\) ,所以通过 \(ST\) 表维护 \(RMQ\) 是很常见的。
很多时候 \(ST\) 表起的只是一个辅助作用,所以好多结合 \(ST\) 表的题还会结合别的。
Yet Another LCP Problem
对于每一组给定的 \(a,b\) 数组,不妨将其拼成 \(c\) 数组。
我们定义 \(ans_x=\sum\limits_{i=1}^{len_x}\sum\limits_{j=1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})\) ,那么有:
\[\sum\limits_{i = 1}^{i = k} \sum\limits_{j = 1}^{j = l}{LCP(s_{a_i\sim n}, s_{b_j \sim n})}=\dfrac{ans_c-ans_a-ans_b}{2} \]
至于为什么 \(\dfrac{1}{2}\) 如下:
我们知道 \(\sum\limits_{i=1}^{len_x}\sum\limits_{j=i+1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})=\dfrac{\sum\limits_{i=1}^{len_x}\sum\limits_{j=1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})}{2}\) ,不妨直接另 \(ans_x=\sum\limits_{i=1}^{len_x}\sum\limits_{j=i+1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})\) ,那么求 \(ans_c-ans_a-ans_b\) 即可。
结合单调栈求 \(ans_x\) :
先将 \(x\) 数组按照 \(rk[x_1]<rk[x_2]\) 排序,使其符合单调性。
定义 \(val_i=LCP(x_{i-1},x_i)\) ,类似 \(luogu~P4248\) 差异 这道题,去计算 \(val_i\) 的贡献,实际上 \(val\) 的定义和 \(height\) 是类似的,因为 \(x_i\) 并不连续,所以需要这样操作,显然有 \(val_1=0\) 。
接下来单调栈维护的过程与 \(luogu~P4248\) 差异 是类似的,处理出最靠右的 \(0\leq l<i,val_l<val_i\) 的位置,以及最靠左的 \(i<r\leq n,val_r<val_i\) 的位置,那么 \(l\sim r\) 这一段的 \(LCP\) 均为 \(val_i\) ,依据乘法原理, \(val_i\) 的贡献就是 \(val_i\times (i-l)\times (r-i)\) ,不妨定义 \(l_i=i-l,r_i=r-i\) 。
那么 \(ans_x=\sum\limits_{i=1}^{len_x}val_i\times l_i\times r_i\) 。
至于 \(LCP\) ,可以用 \(ST\) 表维护。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=4e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,m,ans,sum,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],l[N],r[N],a[N],b[N],c[N],mi[N][30],val[N];
char s[N],t[N];
int sta[N],top;
void clean()
{
memset(rk,0,sizeof(rk));
memset(sa,0,sizeof(sa));
memset(id,0,sizeof(id));
memset(oldrk,0,sizeof(rk));
memset(cnt,0,sizeof(cnt));
memset(key,0,sizeof(key));
memset(height,0,sizeof(height));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
}
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init(char s[],int n)
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
void RMQ()
{
memset(mi,0x3f,sizeof(mi));
for(int i=1;i<=n;i++)
mi[i][0]=height[i];
for(int j=1;j<=log2(n);j++)
for(int i=1;i<=n-(1<<j)+1;i++)
mi[i][j]=min(mi[i][j-1],mi[i+(1<<(j-1))][j-1]);
}
int ask(int l,int r)
{
int t=log2(r-l+1);
return min(mi[l][t],mi[r-(1<<t)+1][t]);
}
bool cmp(int a,int b) {return rk[a]<rk[b];}
int calc(int x[],int len)
{
sort(x+1,x+1+len,cmp);
for(int i=2;i<=len;i++)
if(x[i]==x[i-1])
val[i]=n-x[i]+1;
else
val[i]=ask(rk[x[i-1]]+1,rk[x[i]]);
sum=ans=top=0;
for(int i=1;i<=len;i++)
{
while(val[sta[top]]>val[i]) top--;
l[i]=i-sta[top];
sta[++top]=i;
}
val[len+1]=-1,sta[++top]=len+1;
for(int i=len;i>=1;i--)
{
while(val[sta[top]]>=val[i]) top--;
r[i]=sta[top]-i;
sta[++top]=i;
}
for(int i=1;i<=len;i++)
ans+=l[i]*r[i]*val[i];
return ans;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n),read(m);
cin>>s+1;
init(s,n);
RMQ();
for(int i=1,len1,len2;i<=m;i++)
{
read(len1),read(len2);
for(int j=1;j<=len1;j++)
read(a[j]),
c[j]=a[j];
for(int j=1;j<=len2;j++)
read(b[j]),
c[len1+j]=b[j];
cout<<calc(c,len1+len2)-calc(a,len1)-calc(b,len2)<<endl;
}
}
相似子串
我们发现所有子串的左端点都在后缀数组中出现过,问题是考虑右端点。
发现例如后缀 abcd
,可以将其分为 a
、ab
、abc
、abcd
四个子串,若其与上一个后缀的 \(LCP\) 为 ab
,则新的子串就剩下 abc
、abcd
。
由此可得每个后缀对子串个数的贡献为 \(len_i-height_i\) ,如此得来所有的子串已经是排好序的。
不妨使用前缀和维护,利用二分查找出第 \(i\) 个子串处于哪一个后缀中,定义为 \(pos_i\) 。
对于每次查询 \(x,y\) ,处理出 \(pos_x,pos_y\) ,那么 \(len_x=height_{pos_x}+x-sum_{pos_x-1}\) ,\(leny\) 同理,那么很容易求出子串 \(x,y\) 的公共前缀 \(=\min(\min(len_x,len_y),LCP(sa_{pos_x},sa_{pos_y}))\) ,当然若 \(pos_x=pos_y\) ,其只能 \(=\min(len_x,len_y)\) ,可以用 \(ST\) 表维护 \(LCP\) 。
问题来到如何处理公共后缀,不难想到建一个反串类似的维护,而问题是子串 \(x,y\) 在反串中的 \(pos\) 是多少。
定义 \(pos'i\) 表示子串 \(i\) 在反串中处于哪一个后缀中,有 \(pos'x=rk{n-(sa{pos_x}+len_x-1)+1}\) ,\(pos'_y\) 同理,即该子串右端点的 \(rk\) ,再类似上面维护即可。
虽然这样求 \(pos'\) 不一定绝对准确,但是他能够求出这样的 \(pos'\) 说明他在 \(pos'\) 中一定是合法的,所以这样去求也是没有任何问题的,样例 ``ababa 就是个很好的例子,
aba``` 的 \(pos'\) 应该为 \(2\) ,但求出来的 \(pos'=3\) ,然而对答案没有影响,可以手动模拟理解一下。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,m,ans,sa[N][2],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N][2],sum[N][2],ls[N],mi[2][N][30];
char s[N],t[N];
void clean()
{
memset(rk,0,sizeof(rk));
// memset(sa,0,sizeof(sa));
memset(id,0,sizeof(id));
memset(oldrk,0,sizeof(rk));
memset(cnt,0,sizeof(cnt));
memset(key,0,sizeof(key));
// memset(height,0,sizeof(height));
memset(ls,0,sizeof(ls));
}
void count_sort(int n,int m,bool d)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]][d]=id[i],
cnt[key[i]]--;
}
void init(char s[],int n,bool d)
{
clean();
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m,d);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i][d]>w)
num++,
id[num]=sa[i][d]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m,d);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i][d]]!=oldrk[sa[i-1][d]]||oldrk[sa[i][d]+w]!=oldrk[sa[i-1][d]+w]),
rk[sa[i][d]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1][d]+k]) k++;
height[rk[i]][d]=k;
}
for(int i=1;i<=n;i++)
ls[i]=n-sa[i][d]+1-height[i][d],
sum[i][d]=sum[i-1][d]+ls[i];
}
void RMQ(bool d)
{
memset(mi[d],0x3f,sizeof(mi[d]));
for(int i=1;i<=n;i++)
mi[d][i][0]=height[i][d];
for(int j=1;j<=log2(n);j++)
for(int i=1;i<=n-(1<<j)+1;i++)
mi[d][i][j]=min(mi[d][i][j-1],mi[d][i+(1<<(j-1))][j-1]);
}
int LCP(int l,int r,bool d)
{
l++;
int t=log2(r-l+1);
return min(mi[d][l][t],mi[d][r-(1<<t)+1][t]);
}
int ask(int x,int y,bool d)
{
int l=1,r=n,mid,pos_x,pos_y;
while(l<=r)
{
mid=(l+r)>>1;
if(sum[mid][0]<x) l=mid+1;
if(sum[mid][0]>x) r=mid-1,pos_x=mid;
if(sum[mid][0]==x)
{
pos_x=mid;
break;
}
}
l=1,r=n;
while(l<=r)
{
mid=(l+r)>>1;
if(sum[mid][0]<y) l=mid+1;
if(sum[mid][0]>y) r=mid-1,pos_y=mid;
if(sum[mid][0]==y)
{
pos_y=mid;
break;
}
}
int len_x=height[pos_x][0]+x-sum[pos_x-1][0],len_y=height[pos_y][0]+y-sum[pos_y-1][0];
if(d==1)
pos_x=rk[n-(sa[pos_x][0]+len_x-1)+1],
pos_y=rk[n-(sa[pos_y][0]+len_y-1)+1];
if(pos_x>pos_y) swap(pos_x,pos_y);
return pos_x==pos_y?min(len_x,len_y):min(min(len_x,len_y),LCP(pos_x,pos_y,d));
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n),read(m);
cin>>s+1;
for(int i=1;i<=n;i++)
t[n-i+1]=s[i];
init(s,n,0),init(t,n,1);
RMQ(0),RMQ(1);
for(int i=1,x,y;i<=m;i++)
{
read(x),read(y);
if(x>sum[n][0]||y>sum[n][0]) puts("-1");
else cout<<ask(x,y,0)*ask(x,y,0)+ask(x,y,1)*ask(x,y,1)<<endl;
}
}
结合单调栈
因为 \(LCP(sa_l,sa_r)=\min\limits_{i=l+1}^r\{height_i\}\) ,所以很多时候为了优化复杂度去计算 \(height_i\) 的贡献时需要结合单调栈。
通常思路有两种,本人主要采取类似于 广告印刷 这道题,处理出每个 \(height_i\) 能管辖到的 \(l,r\) ,即区间 \([l\sim r]\) 中的最小值为 \(height_i\) ,那么通常此时 \(height_i\) 能做的贡献就是 \((i-l+1)\times (r-i+1)\times height_i\) ,依据乘法原理。
差异
结合单调栈板子题。
对于 \(\sum\limits_{i\leq i<j\leq n}len(T_i)+len(T_j)-2\times LCP(T_i,T_j)\) 这个式子,将其分为两部分计算。
不难计算出 \(\sum\limits_{1\leq i<j\leq n}len(T_i)+len(T_j)=\dfrac{n\times(n-1)\times (n+1)}{2}\) 。首先有 \(n\) 个字符串,那么会产生 \(\dfrac{n\times (n-1)}{2}\) 对组合,我们知道每个后缀的长度平均值 \(=\dfrac{\sum\limits_{i=1}^nlen_{T_i}}{n}=\dfrac{n+1}{2}\) ,那么两个后缀长度加一起就是 \(n+1\) ,故答案为 \(\dfrac{n\times(n-1)\times (n+1)}{2}\) 。
问题主要是如何求 \(\sum\limits_{1\leq i<j\leq n}LCP(T_i,T_j)\) 。
\(\begin{aligned} \sum\limits_{i=1}^{n}\sum\limits_{j=i+1}^{n}LCP(s_{i \sim |s|,j \sim |s|}) &=\sum\limits_{i=1}^{n}\sum\limits_{j=i+1}^{n}LCP(s_{sa_{i} \sim |s|,sa_{j} \sim |s|}) \\&=\sum\limits_{i=1}^{n}\sum\limits_{j=i+1}^{n}\min\limits_{k=i+1}^{j} \{ height_{k} \} \\ &=\sum\limits_{i=2}^{n}\sum\limits_{j=i}^{n}\min\limits_{k=i}^{j} \{ height_{k} \} \\ &=\sum\limits_{i=1}^{n}\sum\limits_{j=i}^{n}\min\limits_{k=i}^{j} \{ height_{k} \}-\sum\limits_{i=1}^{n}\min\limits_{j=1}^{i} \{ height_{j} \} \\ &=\sum\limits_{i=1}^{n}\sum\limits_{j=i}^{n}\min\limits_{k=i}^{j} \{ height_{k} \}-\sum\limits_{i=1}^{n}0 \\ &=\sum\limits_{i=1}^{n}\sum\limits_{j=i}^{n}\min\limits_{k=i}^{j} \{ height_{k} \} \end{aligned}\)
根据 \(LCP(sa_l,sa_r)=\min\limits_{i=l+1}^r\{height_i\}\) ,所以很多时候为了优化复杂度去计算 \(height_i\) ,考虑计算每个 \(height_i\) 的贡献,于是用到单调栈。
通过单调栈,处理出 \(\max\limits_{0\leq l<i\&height_l<height_i}\{l\}\) 以及 \(\min\limits_{i<r\leq n+1\&height_r<height_i}\{r\}\) ,那么 \(height_i\) 的贡献就是 \((i-l+1)\times(r-i+1)\times height_i\) ,不妨设 \(l_i=i-l+1,r_i=r-i+1\) ,那么其贡献就是 \(l_i\times r_i\times height_i\) 。
那么最后答案就是 \(\dfrac{n\times(n-1)\times (n+1)}{2}-2\times \sum\limits_{i=1}^nl_i\times r_i\times height_i\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=5e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],ans,l[N],r[N];
char s[N];
int sta[N],top;
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
cin>>s+1;
n=strlen(s+1);
init();
for(int i=1;i<=n;i++)
{
while(height[sta[top]]>height[i]) top--;
l[i]=i-sta[top];
sta[++top]=i;
}
sta[++top]=n+1,height[n+1]=-1;
for(int i=n;i>=1;i--)
{
while(height[sta[top]]>=height[i]) top--;
r[i]=sta[top]-i;
ans-=2ll*l[i]*r[i]*height[i];
sta[++top]=i;
}
ans+=1ll*n*(n-1)*(n+1)/2;
cout<<ans;
}
找相同字符
设两个串为 \(t_1,t_2\) ,那么将两个串拼起来成为 \(s\) ,中间用分隔符 #
隔开,定义 \(ans_x\) 表示与差异所求一样的东西,那么答案就是 \(ans_s-ans_{t_1}-ans_{t_2}\) ,实际上应该是 \(\dfrac{ans_s-ans_{t_1}-ans_{t_2}}{2}\) ,具体怎么回事见 Yet Another LCP Problem 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=4e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,m2,m1,ans,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],l[N],r[N];
char s[N],t2[N],t1[N];
int sta[N],top;
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init(char s[],int n)
{
memset(rk,0,sizeof(rk));
memset(sa,0,sizeof(sa));
memset(id,0,sizeof(id));
memset(oldrk,0,sizeof(rk));
memset(cnt,0,sizeof(cnt));
memset(key,0,sizeof(key));
memset(height,0,sizeof(height));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
int calc(char s[],int len)
{
init(s,len);
ans=top=0;
for(int i=1;i<=len;i++)
{
while(height[sta[top]]>height[i]) top--;
l[i]=i-sta[top];
sta[++top]=i;
}
int x=height[len+1];
sta[++top]=len+1,height[len+1]=-1;
for(int i=len;i>=1;i--)
{
while(height[sta[top]]>=height[i]) top--;
r[i]=sta[top]-i;
ans+=l[i]*r[i]*height[i];
sta[++top]=i;
}
height[len+1]=x;
return ans;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
cin>>t1+1>>t2+1;
m1=strlen(t1+1),m2=strlen(t2+1);
for(int i=1;i<=m1;i++)
s[i]=t1[i];
s[m1+1]='#';
for(int i=1;i<=m2;i++)
s[m1+1+i]=t2[i];
n=m1+m2+1;
cout<<calc(s,n)-calc(t2,m2)-calc(t1,m1);
}
Yet Another LCP Problem
上面已有。
多个串拼合
大多数找不同字符串的公共串的问题都需要将多个字符串拼起来处理。
字符加密
将 \(s\) 复制一遍变成 \(ss\) ,就转化为后缀排序问题。
若 \(sa_i\leq \dfrac{n}{2}\) ,就输出 \(s_{sa_i+\frac{n}{2}-1}\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N];
char s[N];
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init()
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
cin>>s+1;
n=strlen(s+1);
for(int i=1;i<=n;i++)
s[i+n]=s[i];
n*=2;
init();
for(int i=1;i<=n;i++)
if(sa[i]<=n/2)
cout<<s[sa[i]+n/2-1];
}
公共串
将 \(t_1\sim t_n\) 拼起来得到 \(s=t_1t'_1t_2t'_2...t_nt'_n\) ,其中 \(t'_i\) 表示分隔符,每个分隔符不相同。
那么对于每个区间 \([l,r]\) 中对于任意一个 \(t_i\) 均有一个后缀的排名在该区间中,则答案为 \(\max\limits_{1\leq l<r\leq |s|}\{\min\limits_{i=l+1}^r\{height_i\}\}\) 。
使用双指针加单调队列维护即可。
接下来详细解释怎么统计是否每个 \(t_i\) 均有后缀的排名存在于区间 \(|l,r|\) 里。
定义两个函数 \(add,del\) ,分别处理加入一个元素与去除一个元素的情况。
先处理出每个 \(t_i\) 对应的 \(l_i,r_i\) ,即左右断点,在此之内的每个元素使 \(c_{rk_j}=i,l_i\leq j\leq r_i\) 。
定义 \(vis_i\) 记录 \(i\) 出现的次数,当插入一个指针 \(i\) 时,先判断是否为分隔符,若不是则 \(vis_{c_i}++\) ,\(sum+=[vis_{c_i}=1]\) ,同理 \(del\) 函数对应 \(vis_{c_i}--\) ,\(sum-=[vis_{c_i}=0]\) 。
那么当 \(sum=n\) 时,即 \(l,r\) 满足条件。
同时因为 \(LCP(sa_l,sa_r)=\min\limits_{i=l+1}^r\{height_i\}\) ,所以我们希望这个区间尽可能小,那么枚举 \(r\) ,同时当 \(l,r\) 满足条件的前提下另 \(l\) 尽可能大,从而使处理答案的复杂度控制在 \(O(n)\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,m,ans,sum,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],l[N],r[N],c[N],v[N],L[N],R[N];
char s[N],t[N];
deque<int>q;
void clean()
{
memset(rk,0,sizeof(rk));
memset(sa,0,sizeof(sa));
memset(id,0,sizeof(id));
memset(oldrk,0,sizeof(rk));
memset(cnt,0,sizeof(cnt));
memset(key,0,sizeof(key));
memset(height,0,sizeof(height));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
}
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init(char s[],int n)
{
int m=127,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
void add(int x,int &sum)
{
if(c[x]==0) return ;
v[c[x]]++;
sum+=(v[c[x]]==1);
}
void del(int x,int &sum)
{
if(c[x]==0) return ;
v[c[x]]--;
sum-=(v[c[x]]==0);
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(m);
for(int i=1;i<=m;i++)
{
cin>>t+1;
int len=strlen(t+1);
L[i]=n+1;
for(int j=1;j<=len;j++)
s[++n]=t[j];
R[i]=n;
s[++n]=i+'#';
}
init(s,n);
for(int i=1;i<=m;i++)
for(int j=L[i];j<=R[i];j++)
c[rk[j]]=i;
add(1,sum);
for(int l=1,r=2;r<=n;r++)
{
while(!q.empty()&&height[q.front()]>=height[r])
q.pop_front();
q.push_front(r);
add(r,sum);
if(sum>=m)
{
while(sum>=m&&l<=r-1)
del(l,sum),
l++;
l--;
add(l,sum);
}
while(!q.empty()&&q.back()<=l)
q.pop_back();
if(sum==m)
ans=max(ans,height[q.back()]);
}
cout<<ans;
}
Sandy 的卡片
查分后变为 公共串 ,因为差分后长度会 \(-1\) ,所以最后答案要 \(+1\) 。
点击查看代码
cpp
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
int n,m,ans,sum,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],l[N],r[N],c[N],v[N],L[N],R[N];
int s[N],t[N];
deque<int>q;
void clean()
{
memset(rk,0,sizeof(rk));
memset(sa,0,sizeof(sa));
memset(id,0,sizeof(id));
memset(oldrk,0,sizeof(rk));
memset(cnt,0,sizeof(cnt));
memset(key,0,sizeof(key));
memset(height,0,sizeof(height));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
}
void count_sort(int n,int m)
{
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++)
cnt[key[i]]++;
for(int i=1;i<=m;i++)
cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)
sa[cnt[key[i]]]=id[i],
cnt[key[i]]--;
}
void init(int s[],int n)
{
int m=5000,tot=0,num=0;
for(int i=1;i<=n;i++)
rk[i]=(int)s[i],
id[i]=i,
key[i]=rk[id[i]];
count_sort(n,m);
for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
{
num=0;
for(int i=n;i>=n-w+1;i--)
num++,
id[num]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w)
num++,
id[num]=sa[i]-w;
for(int i=1;i<=n;i++)
key[i]=rk[id[i]];
count_sort(n,m);
for(int i=1;i<=n;i++)
oldrk[i]=rk[i];
tot=0;
for(int i=1;i<=n;i++)
tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
rk[sa[i]]=tot;
}
for(int i=1,k=0;i<=n;i++)
{
if(rk[i]==0) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
void add(int x,int &sum)
{
if(c[x]==0) return ;
v[c[x]]++;
sum+=(v[c[x]]==1);
}
void del(int x,int &sum)
{
if(c[x]==0) return ;
v[c[x]]--;
sum-=(v[c[x]]==0);
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(m);
for(int i=1,len;i<=m;i++)
{
cin>>len;
L[i]=n+1;
for(int j=1;j<=len;j++)
{
read(t[j]);
if(j>=2)
s[++n]=2000+t[j]-t[j-1];
}
R[i]=n;
s[++n]=i+2000;
}
init(s,n);
for(int i=1;i<=m;i++)
for(int j=L[i];j<=R[i];j++)
c[rk[j]]=i;
add(1,sum);
for(int l=1,r=2;r<=n;r++)
{
while(!q.empty()&&height[q.front()]>=height[r])
q.pop_front();
q.push_front(r);
add(r,sum);
if(sum>=m)
{
while(sum>=m&&l<=r-1)
del(l,sum),
l++;
l--;
add(l,sum);
}
while(!q.empty()&&q.back()<=l)
q.pop_back();
if(sum==m)
ans=max(ans,height[q.back()]);
}
cout<<ans+1;
}
找相同字符
上面已有,由于涉及将两个串拼起来的思路,所以也放在了这里。
Yet Another LCP Problem
上面已有,同样的涉及将两个串拼起来。
结合分块与莫队
优秀的拆分
枚举连续串的长度 \(|s|\) ,按照 \(|s|\) 对整个串进行分块,对相邻两个块进行 \(LCP,LCS\) 查询。
由于没有学莫队,暂时还没有写。
具体可见 [2009] 后缀数组------处理字符串的有力工具。
结合单调队列
因为 \(LCP(sa_l,sa_r)=\min\limits_{i=l+1}^r\{height_i\}\) ,所以类似于求带限制的最长公共子串问题时常结合单调队列,常见的形式为求 \(\max\limits_{1\leq l<r\leq |s|}\{\min\limits_{i=l+1}^r\{height_i\}\}\) ,且同时常存在队内长度的限制。
下面的例题前面都已经涉及,就只沾题目了。
Milk Patterns G
公共串
Sandy 的卡片
结合并查集
品酒大会
暂时还没有打,待会儿补上。