题意
给定一个长度为 \(n\) 的由正整数构成的串和一个整数 \(K\),求至少出现 \(K\) 次的串的最长长度(可以重叠)。
\(1 \leq n\leq 20000\)
\(2\leq K\leq n\)
思路
变量及其含义
后缀数组(SA)是一个比较灵活的处理字符串有关问题的算法,它能对一个串的所有后缀进行排序。我们用串 abaaab 作例子,来分析这个算法。
sa(suffix array)
算法名由来的数组,可见该数组是核心。\(sa[i]\) 的定义是排第 \(i\) 的后缀开头在哪个位置。
sa | suffix |
---|---|
3 | aaab |
4 | aab |
5 | ab |
1 | abaaab |
6 | b |
2 | baaab |
比如例串的 \(sa\) 数组如上表
rk(rank)
\(rk\) 数组是 \(sa\) 数组的逆运算,\(sa\) 是排名映射到位置,而 \(rk\) 则是位置映射到排名,即 \(rk[sa[i]]=i\) 。
H(height)
\(H\) 数组也就是常说的高度数组,它是在求解后缀数组问题时一个重要的数组,满足 \(H[i]=\text{lcp}(sa[i],sa[i-1])\) ,其中 \(\text{lcp}\) 表示最长公共前缀。高度数组的含义也就是排名相邻的两个后缀的最长公共前缀长度。
这么说可能不太形象,那么我们把上面的表格再拓展一列。
sa | H | suffix |
---|---|---|
3 | / | aaab |
4 | 2 | aab |
5 | 1 | ab |
1 | 1 | abaaab |
6 | 0 | b |
2 | 1 | baaab |
具体流程
一般后缀数组使用 \(O(n\log n)\) 的倍增算法进行构造,当然有一个更加优秀的叫做 \(\text{DC3}\) 的算法(我不会)是线性的,听说较难实现。
基数排序
为了能实现较快的排序,有必要实现一个线性的排序算法,而基排就是一个相当灵活的线形排序算法。基数排序这里不在详细介绍,这里仅仅把它挖的更深一点,方便理解后缀数组中的基排。我们以它的一次排序(其实就是计数排序)来演示。
比如直接把长度为 \(n\) ,数值属于 \([1,n]\) 的 \(a\) 数组排序,放入 \(b\) 数组中:
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[a[i]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)b[c[a[i]]--]=a[i];
而直接按照下标放置(即把 \(a_i\) 当作比较关键字,对一个\(1\) 到 \(n\) 的全排列进行排序),只用在第 \(4\) 行把 \(a[]\) 去掉即可。
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[a[i]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)b[c[a[i]]--]=i;
再拓展一下,把一个长度为 \(n\) 的 \(a\) 数组,按比较关键字 \(f\) 数组进行排序,放入 \(b\) 数组中,其中 \(f_i\in[1,m]\)。
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[f[a[i]]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)b[c[f[a[i]]]--]=a[i];
利用基排属于稳定排序的顺序,可以在此基础上继续优先度更高的排序。
现在对基排的理解有没有更深?九种排序什么的要不仅仅会打板子,还要知道灵活运用才行。
倍增过程
回忆一下我们学过的倍增。如果可以依次求出 \(1,2,3,4\) 直到 \(k\) 的答案,那么有时稍作修改,通过依次求 \(2^0,2^1,2^2,2^3\) 的答案,就可以一直到 \(2^k\) 的答案。
那么构造后缀数组的倍增也是如此,我们想得到后缀的排序,把问题抽象一下,就是求从每个字符开始长度为 \(k\) 的串的顺序(\(k>=n\) ,越界则该位置无穷小),我们把问题转化成给 \(n\) 个长为 \(k\) 的串排序( \(k\) 为 \(2\) 的正整数次幂),通过长为 \(k\) 的串的顺序合并出长为 \(2k\) 的串的顺序。而长为 \(1\) 则是直接对每个字符排序。
每次合并的过程就是给每第 \(i\) 个位置一个 \((rk[i],rk[i+k])\) 的二元组,按这个排序得到一个新的顺序,就是长为 \(2k\) 的串的顺序。
更加具体的流程只能通过代码+注释讲解了。
int *x=tmp[0],*y=tmp[1],*c=tmp[2]; //c是基数排序的计数数组
x[n+1]=y[n+1]=0; //监视哨防止越界
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[x[i]=s[i]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)sa[c[x[i]]--]=i; //先解决2^0的排序
for(int k=1;k<=n;k<<=1)
{ //x在这里表示长为k的串的rk,y在这里表示二元组的后面那一维的顺序
int p=0;
FOR(i,n-k+1,n)y[++p]=i; //i+k>n,故rk[i+k]为极小值
FOR(i,1,n)if(sa[i]>k)y[++p]=sa[i]-k;
FOR(i,1,m)c[i]=0; //对y数组以x为关键字排序,放在sa中
FOR(i,1,n)c[x[y[i]]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)sa[c[x[y[i]]]--]=y[i];
std::swap(x,y); //其实这里就是把x数组赋给y,并丢掉原来的x
p=x[sa[1]]=1;
FOR(i,2,n)x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?p:++p; //x在这里表示长为2k的串的rk,通过已经得到的顺序sa算出
if(p==n)break; //如果rk已经两两不同
m=p;
}
FOR(i,1,n)rk[sa[i]]=i; //逆运算,得到rk
高度数组
- 定理:\(H[rk[i+1]]>=H[rk[i]]-1\)
用人类的语言阐述一下,就是在原字符串 \(i+1\) 位置在后缀数组对应位置中的 \(H\) 最少只可能比 \(i\) 的少 \(1\) 。
这个定理还是比较显然的,设原字符串的 \(i\) 位置 \(\text{lcp}\) 为 \(l\) ,那么 \(i+1\) 位置保底也有一个 \(\text{lcp}\) 长度为 \(l-1\) (就是与 \(i\) 位置形成 \(\text{lcp}\) 的那个后缀去掉开头一位),至于有没有更长就不知道了。
那我们只用按原串的顺序,求出后缀数组对应位置的高度即可,通过 \(\text{k- -}\) 与 \(\text{k++}\) 的次数分析,可得复杂度 \(O(n)\) 。
int k=0;
FOR(i,1,n)
{
if(k)k--;
if(rk[i]==1)continue;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
H[rk[i]]=k;
}
如何用SA解题
利用 \(sa,rk,H\) 三个关键的数组,将字符串问题转化成这三个数组上的问题,是后缀数组解题的一般套路。
后缀的前缀就是子串,是后缀结构的核心原理。
我们此题题,求至少出现 \(K\) 次的串的最长长度为例进行分析。
首先求出后缀数组,至少出现 \(K\) 次,就意味着一段后缀数组上的一段长度大于 \(K\) 的区间,它们相邻的 \(\text{lcp}\)( \(H\) 值)最大。那么很自然的想到二分这个最长长度,检查后缀数组上是否存在一个长度为 \(K\) 的区间满足 \(H\) 的最小值大于等于这个长度即可。
代码
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define FOR(i,x,y) for(int i=(x),i##END=(y);i<=i##END;++i)
#define DOR(i,x,y) for(int i=(x),i##END=(y);i>=i##END;--i)
template<typename T,typename _T>inline bool chk_min(T &x,const _T y){return y<x?x=y,1:0;}
template<typename T,typename _T>inline bool chk_max(T &x,const _T y){return x<y?x=y,1:0;}
typedef long long ll;
const int N=2e4+5;
int sa[N],rk[N],H[N],tmp[3][N];
int disc[N],D;
int s[N];
int n,K;
void get_SA(int *s,int n,int m)
{
int *x=tmp[0],*y=tmp[1],*c=tmp[2];
x[n+1]=y[n+1]=0;
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[x[i]=s[i]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
int p=0;
FOR(i,n-k+1,n)y[++p]=i;
FOR(i,1,n)if(sa[i]>k)y[++p]=sa[i]-k;
FOR(i,1,m)c[i]=0;
FOR(i,1,n)c[x[y[i]]]++;
FOR(i,2,m)c[i]+=c[i-1];
DOR(i,n,1)sa[c[x[y[i]]]--]=y[i];
std::swap(x,y);
p=x[sa[1]]=1;
FOR(i,2,n)x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?p:++p;
if(p==n)break;
m=p;
}
FOR(i,1,n)rk[sa[i]]=i;
int k=0;
FOR(i,1,n)
{
if(k)k--;
if(rk[i]==1)continue;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
H[rk[i]]=k;
}
}
bool check(int len)
{
int cnt=0;
FOR(i,1,n)
{
cnt++;
if(cnt>=K)return true;
if(H[i+1]<len)cnt=0;
}
return false;
}
int main()
{
scanf("%d%d",&n,&K);
FOR(i,1,n)scanf("%d",&s[i]);
FOR(i,1,n)disc[++D]=s[i];
std::sort(disc+1,disc+D+1);
D=std::unique(disc+1,disc+D+1)-disc-1;
FOR(i,1,n)s[i]=std::lower_bound(disc+1,disc+D+1,s[i])-disc;
get_SA(s,n,n);
int l=0,r=n;
while(l<r)
{
int mid=(l+r+1)>>1;
if(check(mid))
l=mid;
else r=mid-1;
}
printf("%d\n",l);
return 0;
}