预处理
首先,使用倍增算法求出对应sa,height值,时间复杂度
O(nlogn)
。
(具体定义参考国家集训队2009罗穗骞论文,论文中还给出一种线性DC3做法)
#include<bits/stdc++.h>
using namespace std;
const int Maxn=2e5+50;
int n,m,rk[Maxn],sa[Maxn],sa2[Maxn],c[Maxn],H[Maxn];
char ch[Maxn];
inline void Rsort()
{
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[rk[sa2[i]]]++;
for(int i=1;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[rk[sa2[i]]]--]=sa2[i];
}
inline bool cmp(int x,int y,int w)
{
return sa2[x]!=sa2[y]||sa2[x+w]!=sa2[y+w];
}
inline void getsa()
{
for(int i=1;i<=n;i++)rk[i]=ch[i],sa2[i]=i;
Rsort();
for(int w=1,p=1,i;p<n;m=p,w<<=1)
{
p=0;
for(int i=n-w+1;i<=n;i++)sa2[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)sa2[++p]=sa[i]-w;
Rsort();swap(rk,sa2);rk[sa[1]]=p=1;
for(int i=2;i<=n;i++)rk[sa[i]]=(cmp(sa[i-1],sa[i],w)?(++p):p);
}
for(int i=1,k=0,j;i<=n;H[rk[i++]]=k)
for(k?k--:k,j=sa[rk[i]-1];ch[i+k]==ch[j+k];k++);
}
使用
(题目引自国家集训队2009罗穗骞论文)
1.单个字符串问题
1.1重复子串
(定义重复子串:字符串R在字符串L中至少出现两次,则称R是L的重复子串。)
1.1.1可交叉最长重复子串
给一个字符串,求其最长重复子串,该字串出现的位置可交叉。
一个重复出现的子串必定出现在该字符串的两个位置,且该字串为这两个位置起始的后缀的最长相同前缀,排名中处于邻位。据此,直接查询height最大值即可。
1.1.2不可交叉最长重复子串(poj1743)
给一个字符串,求其最长重复子串,该字串出现的位置不可交叉。
首先假设如果存在这个重复子串,根据height定义,这个子串的长度一定是height的某一段的最小值,且这一段存在没有交叉的位置。那么二分这个子串长度,判定可行性即可。
1.1.3可交叉的k次最长重复子串
给定一个字符串,求至少出现k次的最长重复子串,这k个子串可以重叠。
同样,这个重复子串只会出现在排序后的一段连续height中,二分即可。
1.2子串个数问题
1.2.1不相同子串个数(spoj694,spoj705)
给一个字符串,求不同子串的个数
每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。
对于每一个后缀,排序后对答案的贡献为(n-sa[i]+1)-height[i]。即该后缀的长度减去与前面的后缀重复的前缀个数。
1.3循环子串问题
1.3.1求最小循环节
给定一个字符串L,已知这个字符串是由某个字符串S重复R次而得到的, 求R的最大值。
同KMP思想,枚举循环节长度k(n%k==0),若suffix(1)和suffix(k+1)的最长公共前缀为n-k(即suffix(k+1))的长度,必然构成循环。
1.3.2重复次数最多的连续重复子串(spoj687,poj3693)
给定一个字符串L,已知这个字符串的某个子串可以由某个字符串S重复R次而得到, 求R值最大子串(若有多个则输出字典序最小值)。
这道题有点难,后缀数组的好题。以下题解来自:http://blog.csdn.net/acm_cxlove/article/details/7941205
比较容易理解的部分就是枚举长度为L,然后看长度为L的字符串最多连续出现几次。
既然长度为L的串重复出现,那么str[0],str[l],str[2*l]……中肯定有两个连续的出现在字符串中。
那么就枚举连续的两个,然后从这两个字符前后匹配,看最多能匹配多远。
即以str[i*l],str[i*l+l]前后匹配,这里是通过查询suffix(i*l),suffix(i*l+l)的最长公共前缀
通过rank值能找到i*l,与i*l+l的排名,我们要查询的是这段区间的height的最小值,通过RMQ预处理
达到查询为0(1)的复杂度,
设LCP长度为M, 则答案显然为M / L + 1, 但这不一定是最好的, 因为答案的首尾不一定再我们枚举的位置上. 我的解决方法是, 我们考虑M % L的值的意义, 我们可以认为是后面多了M % L个字符, 但是我们更可以想成前面少了(L - M % L)个字符! 所以我们求后缀j * L - (L - M % L)与后缀(j + 1) * L - (L - M % L)的最长公共前缀。
即把之前的区间前缀L-M%L即可。
然后把可能取到最大值的长度L保存,由于 题目要求字典序最小,通过sa数组进行枚举,取到的第一组,肯定是字典序最小的。
2.两个字符串串问题
2.1公共子串问题
2.1.1最长公共子串(poj2774,ural1517)
给定两个字符串A和B,求最长公共子串。
这个没什么好说的,两个字符串中间随便加一个其他的字符然后拼在一起搞一搞就好了。
2.2子串个数问题
2.2.1特定长度的公共子串
给定两个字符串A和B,求长度不小于k的公共子串的个数(可以相同)。
神题。首先要分组,前面有提到。对于同一组中的每一个属于a,b的后缀,要统计这个后缀之前的属于b,a的后缀的贡献,发现这个是 O(n2) 的,进一步分析,发现可以用单调队列来维护。给出代码:
for(int i=1;i<=n;i++)
{
if(sa[i]==len1+1)continue;//加入的字符位置
if(h[i]<k)cnt[0]=cnt[1]=sum[0]=sum[1]=0;//这里体现分组
for(int j=0;j<=1;j++)
{
cnttmp[j]=0;//记录一共更新多少个height
while(cnt[j]&&st[j][cnt[j]].first>=h[i])//保证序列单调递增(height数组的性质得最长公共前缀为区间最小值)
{
cnttmp[j]+=st[j][cnt[j]].second;
sum[j]-=1ll*(st[j][cnt[j]].first-k+1)*st[j][cnt[j]].second;//统计大于k的前缀长度贡献
cnt[j]--;
}
if(cnttmp[j])st[j][++cnt[j]]=make_pair(h[i],cnttmp[j]);//更新后放到同一位置
sum[j]+=1ll*(h[i]-k+1)*cnttmp[j];//更新贡献
}
int bz=0;
if(sa[i]>len1+1)bz=1;
ans+=sum[bz^1];
st[bz][++cnt[bz]]=make_pair(INF,1);sum[bz]+=INF-k+1;//后来的串会更新INF
}
3.多个字符串问题
3.1公共子串问题
3.1.1在k个字符串中的出现的最长子串(poj3294)
给定n个字符串,求出现在不小于k个字符串中的最长子串。
二分即可。
(还有,多个字符串分隔符较多,可能会爆char,建议换成数组后再加分隔符)
3.1.2在每个字符串中出现k次的最长子串(spoj220)
给定n个字符串,求出现在不小于k个字符串中的最长子串。
同样二分。
3.1.3在每个字符串中或反转后出现的最长子串(poj1226)
给定n个字符串,求出现或反转后出现在每个字符串中的最长子串。
反转后连接,二分。
总结
1.二分
2.height分组