字符串
为字符串 的长度
为字符串 的中第 个字符,
,即 区间 的子串
后缀数组
我们研究字符串
把 为字符串 的第 个后缀,即
把所有的后缀按字典序排序, 表示 的排名, 就表示排名为 的后缀是字符串 的第几个后缀
,
例如,对于 :
1 | vamamadn |
2 | amamadn |
3 | mamadn |
4 | amadn |
5 | madn |
6 | adn |
7 | dn |
8 | n |
排名 | 对应字符串 | |
---|---|---|
1 | 6 | adn |
2 | 4 | amadn |
3 | 2 | amamadn |
4 | 7 | dn |
5 | 5 | madn |
6 | 3 | mamadn |
7 | 8 | n |
8 | 1 | vamamadn |
LCP:我们定义 为,排名为 和排名为 的字符串的最长公共前缀,即 和 的最长公共前缀
显然的两条性质
LCP 引理
证明:
令 ,则有 且
设
那么就说明 和 前 个字符相等, 和 前 个字符相等,所以 和 前 个字符相等,即
然后下面用反证法,假设 ,那么就有
但是,,那么就意味要么有 要么有 ,所以不可能会有 ,假设不成立。
因此
LCP定理
证明:
我们把 到 拆成 到 到 到 ,应用 LCP引理,则有
我们把 到 再拆,这么递归拆下去,就证明了上式
hight引理
令 ,。特别地,定义 。 可以看作排名为 的后缀与排名为 的后缀的最长公共前缀长度
令 , 可以看作第 个后缀与和他排名紧挨着靠前的那个后缀的最长公共前缀长度
然后有
证明:
如果第 个后缀与排名为 的后缀之间没有公共前缀,那么 ,而 必然为非负数,那么上式成立
反之,我们假设排名为 的后缀是第 个后缀。那么第 个后缀是第 个后缀去掉首字母得到的,第 个后缀是第 个后缀去掉首字母得到的。且由于第 个后缀的排名比第 个后缀的排名靠前,则第 个后缀的排名也应该比第 个后缀的排名靠前,并且两者的最长公共前缀,即
然后我们考虑,两个排名紧挨着的后缀的最长公共前缀应该是最大的。第 个后缀的排名不一定紧挨着第 个后缀的排名,排名可能要更靠前一点。那么此时我们的 就表示的是和第 个后缀和紧挨着第 个后缀的后缀的 LCP,必然有 ,证毕
求LCP
由LCP定理可知 ,
而由 的定义,我们可以得到
然后我们可以借助 引理,先求出 ,来求出
具体代码见题目部分
https://www.luogu.com.cn/problem/P4051
求后缀数组的板子题
cpp12345678910111213141516171819202122232425262728293031323334353637383940#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
#define _ 1000006
char s[_];
int n,cnt[_],m,rk[_<<1],sa[_],nd[_],oldrk[_<<1];
inline void SA(){
m=300;//设定字符值域
for(int i=1;i<=n;i++) ++cnt[rk[i]=s[i]]; //先来一波基数排序
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int p=0,k=1;k<n;k<<=1,p=0){
for(int i=n;i>=n-k+1;i--) nd[++p]=i;//后半截空串,第二关键字肯定排名考前
for(int i=1;i<=n;i++) if(sa[i]>k) nd[++p]=sa[i]-k;//通过上回的整体排名,推本次的后半截排名
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) ++cnt[rk[nd[i]]];//再根据第一关键字排名,第一关键字就是上次的整体排名
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[nd[i]]]--]=nd[i];
swap(rk,oldrk);m=0;//更新排名
for(int i=1;i<=n;i++) rk[sa[i]]=(m+=((oldrk[sa[i]]==oldrk[sa[i-1]] && oldrk[sa[i]+k]==oldrk[sa[i-1]+k])?0:1));
}
}
inline void subtask(){
cin>>(s+1);n=strlen(s+1);
SA();
for(int i=1;i<=n;i++) cout<<sa[i]<<' ';
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}
https://www.luogu.com.cn/problem/P4051
题目描述
JS 同学把需要加密的信息排成一圈,显然,它们有很多种不同的读法。
例如‘JSOI07’,可以读作: JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0 把它们按照字符串的大小排序: 07JSOI 7JSOI0 I07JSO JSOI07 OI07JS SOI07J 读出最后一列字符:I0O7SJ,就是加密后的字符串。你能写一个程序完成这个任务吗?
数据范围
笔记
断环成链,即把原串复制一遍,然后跑 SA。取 SA 中编号小于等于 的后缀。
cpp12345678910inline void subtask(){
cin>>s+1,n=strlen(s+1);
for(int i=1;i<=n;i++) s[i+n]=s[i];
n<<=1;
SA();
for(int i=1;i<=n;i++){
if(sa[i]>(n>>1)) continue;
cout<<s[sa[i]+(n>>1)-1];
}
}
https://www.luogu.com.cn/problem/SP705
题目数据
组数据,每次给定一个字符串 ,求该字符串本质不同的子串数量。
两个子串本质不同,当且仅当两个子串长度不等,或长度相等但有任意一位不同。
数据范围
,
笔记
我们发现如果存在两个本质相同的子串,那么这两个子串一定是某两个后缀的公共前缀,而且这两个后缀的排名接近
于是我们考虑按排名的顺序逐个添加后缀。考虑添加后缀 后对本质不同子串数量的影响。增加的子串数量即为后缀 的长度,其中有 个子串是已经在添加后缀 的时候已经算进去了,需要减掉。
于是答案即为:
求 height 数组的模板
cpp12345678910void LCP(){
int k=0;//表示h数组
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;//hight[1]=0;
if(k) k--;//h[i]>=h[i-1]-1
int j=sa[rk[i]-1];//排名紧靠前的后缀
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
height[rk[i]]=k;//h[i]=height[rk[i]]
}
}
cpp1234567inline void subtask(){
cin>>s+1,n=strlen(s+1);
SA();LCP();
int sum=0;
for(int i=1;i<=n;i++) sum+=height[i];
cout<<n*(n+1)/2-sum<<endl;
}
https://www.luogu.com.cn/problem/SP1811
题目描述
给定两个字符串,求最长公共子串长度,长度不超过
笔记
把两个字符串首尾拼接起来,中间加一个分隔符,于是答案即为
因为我们加了个特殊分割符,所以正确性是能保证的
不过直接暴力枚举 , 的话时间复杂度还是太大了。实际上我们考虑,排名紧挨着的字符串的 LCP 是最大的,所以我们直接找后缀起始点分处于两个字符串的 height 的值的最大值即可
不过可能还有一个问题:有可能排名为 和 的后缀的起始点在同一字符串,而排名 和 的后缀不在同一字符串,且这两个后缀的 LCP 也不小,那么我们不就忽略掉了
我们可以这么考虑,两个字符串的公共部分起始点构成的后缀,排名要么紧挨着,于是上述算法成立。排名也可能不紧挨着,如果不紧挨着的话,排名位于中间的后缀也包含这个公共部分。这些后缀中必然会有起始点来自不同的字符串的,于是这两个后缀的 LCP 应该是另一个 ,我们上述做法也没问题
cpp12345678910111213inline void subtask(){
cin>>s+1,n=strlen(s+1);s[n+1]='$';
int n1=n;
cin>>s+n+2,n=strlen(s+1);
SA();LCP();
int ans=-inf;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;
int j=sa[rk[i]-1];
if((i<=n1&&j>=n1+2)||(i>=n1+2&&j<=n1)) ans=max(ans,height[rk[i]]);
}
cout<<ans;
}
https://www.luogu.com.cn/problem/P4248
题目描述
给定一个长度为 的字符串 ,令 表示它从第 个字符开始的后缀。求
其中, 表示字符串 的长度, 表示字符串 和字符串 的最长公共前缀。
数据范围
对于 的数据,保证 ,且 中均为小写字母。
笔记
结合公式 上述式子可以变为
后面那部分我们可以考虑使用线段树来做。具体来说就是维护求和以及取 min 操作
首先一个想法是可以使用吉司机线段树来做。不过我们考虑 数组的值域不大,也可以使用权值线段树,对 取 min 的时候就是先计算有几个值比 大,然后把这些值的数量归到 的数量
线段树还是太吃操作了。我们进一步考虑,后面那部分也可以视为 数组的所有子区间的最小值加和
这是经典问题,可以使用单调栈来做
cpp1234567891011121314151617181920int sta[_],top,l[_],r[_];
inline void subtask(){
cin>>s+1;n=strlen(s+1);
SA();
//for(int i=1;i<=n;i++) debug(height[i]);
sta[top=1]=1;
for(int i=2;i<=n;i++){
while(top&&height[sta[top]]>height[i]){
r[sta[top]]=i;
top--;
}
l[i]=sta[top];
sta[++top]=i;
}
while(top) r[sta[top--]]=n+1;
//for(int i=1;i<=n;i++) debug(l[i]),debug(r[i]);
int ans=n*(n+1)/2*(n-1);
for(int i=2;i<=n;i++) ans-=2*(i-l[i])*(r[i]-i)*height[i];
cout<<ans;
}
https://www.luogu.com.cn/problem/P3181
题目描述
给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。
数据范围
,字符串中只有小写字母。
笔记
把两个字符串首尾相接,然后就转为求所有后缀起点位于不同字符串的 LCP 之和
这个和上一题类似,不过这题对后缀有限制要求了
我们可以考虑容斥,先把拼接后的整个的字符串的所有后缀组合 LCP 求出来,然后再减去后缀起点位于同一字符串的所有组合的 ,即得到答案了
cpp1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define ls(k) (k)<<1
#define rs(k) (k)<<1|1
#define debug(x) cout<<#x<<"="<<x<<endl
using namespace std;
const int inf=0x3f3f3f3f3f3f3f3f;
const int mod=0;
typedef pair<int,int> pii;
#define _ 400005
char s1[_],s2[_],s[_];
int n1,n2,n,m,cnt[_],nd[_],sa[_],rk[_<<1],oldrk[_<<1],height[_],l[_],r[_],sta[_],top;
int SA(){
m=300;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) ++cnt[rk[i]=s[i]];
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int p=0,k=1;k<n;k<<=1,p=0){
for(int i=n;i>=n-k+1;i--) nd[++p]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) nd[++p]=sa[i]-k;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) ++cnt[rk[nd[i]]];
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[nd[i]]]--]=nd[i];
swap(rk,oldrk);m=0;
for(int i=1;i<=n;i++) rk[sa[i]]=(m+=((oldrk[sa[i]]==oldrk[sa[i-1]] && oldrk[sa[i]+k]==oldrk[sa[i-1]+k])?0:1));
}
int k=0;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
sta[top=1]=1;
for(int i=2;i<=n;i++){
while(top&&height[sta[top]]>height[i]) r[sta[top--]]=i;
l[i]=sta[top];
sta[++top]=i;
}
while(top) r[sta[top--]]=n+1;
int res=0;
for(int i=2;i<=n;i++) res+=(i-l[i])*(r[i]-i)*height[i];
return res;
}
inline void subtask(){
cin>>s1+1;n1=strlen(s1+1);
cin>>s2+1;n2=strlen(s2+1);
int ans=0;
for(int i=1;i<=n1;i++) s[i]=s1[i];
s[n1+1]='$';
for(int i=1;i<=n2;i++) s[i+n1+1]=s2[i];
n=n1+n2+1;
ans+=SA();
for(int i=1;i<=n1;i++) s[i]=s1[i];
n=n1;
ans-=SA();
for(int i=1;i<=n2;i++) s[i]=s2[i];
n=n2;
ans-=SA();
cout<<ans;
}
signed main(){
ios::sync_with_stdio(false);
int t=1;//cin>>t;
while(t--) subtask();
return 0;
}