学习链接:
「后缀排序SA」学习笔记 - Rainy7の灯塔 - 洛谷博客 主要在这里学的
题解 P3809 【【模板】后缀排序】 - Nemlit 的博客 - 洛谷博客 这里对基数排序和倍增讲解的很好
板子题链接:【模板】后缀排序 - 洛谷
代码:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
const int N=1e6+5;
string s;
int x[N],y[N],c[N],sa[N];
int height[N],rk[N]; //height[i]=LCP(i,i-1), rk[i]是s[i~n]在所有后缀中的字典序排名
void SA(const string&s,int n,int m){
/**
* 首先定义一些变量
* suffix[i]: 表示s[i~n]这个后缀
* rk[i]: suffix[i]在所有后缀中的排名
* sa[i]: 满足suffix[sa[1]] < suffix[sa[2]] < ... < suffix[sa[n]],也就是说,排名为i的后缀的编号是sa[i]
* 有一个很重要的性质:sa[rk[i]] = rk[sa[i]] = i
*/
//对{s[i],i}二元组进行基数排序
for(int i=1;i<=n;i++){x[i]=s[i-1];++c[x[i]];} //c数组是桶,x[i]是第i个元素的第一关键字
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; //注意,这里为了防止sa值重复,c[x[i]]需要--,第一关键字相同时,位置越靠前的后缀排名越靠前
for(int k=1;k<=n;k=k<<1) //2.倍增进行基数排序
{
int num=0; //只是一个数组指针
for (int i=n-k+1;i<=n;++i) y[++num]=i; //定义y[num]表示第二关键字中,排名为num的数的位置,[n-k+1,n]这个区间后面没有字母了,所以是最小的,放在最前面
for(int i=1;i<=n;i++) //在第一关键字中排名越靠前,表示应该排在前面,所以从1~n遍历sa[i],也就是按照第一关键字从小到大遍历
if(sa[i]>k)y[++num]=sa[i]-k; //sa[i]表示排名为i的第一关键字pos,注意pos>k的才会作为当前的第二关键字
//按{第一关键字,第二关键字}进行基数排序
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
swap(x,y); //下面即将要生成新的x数组(新的第一关键字),它要用到旧的第一关键字,所以把旧的暂存在y这里
num=1;x[sa[1]]=1; //当前第一关键字是1,排名第一个的那个(sa[1])的第一关键字也当然是1
for(int i=2;i<=n;i++) //再计算排名是2~n的二元组的新的第一个关键字
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])x[sa[i]]=num; //第一关键字和第二关键字都和排名上一个的一样,那么新的第一个关键字就一样
else x[sa[i]]=++num; //否则就是排名前一个的新的第一关键字+1
}
if(num==n)break; //如果排名数量num已经是n了(已经排好了),就没必要继续排了,剪枝,结束
m=num; //新的关键字数,是旧的排名数
}
// for(int i=1; i<=n; i++) cout<<sa[i]<<' '; cout<<'\n';
}
void LCP(const string&s,int n){
/**
* 首先定义一些变量
* height[i]: 表示suffix[sa[i]]和suffix[sa[i-1]]的LCP长度,也就是排名相邻的两个后缀的最长公共前缀长度
* h[i]: 表示height[rk[i]],也就是suffix[i]和suffix[i-1]的LCP长度
* 用到一个很重要的性质:h[i] >= h[i-1]+1,它使得LCP函数的均摊复杂度变为O(N)
*
* 跑完这个函数之后,具体有什么用呢?用法如下:
* LCP(i,j) = min(height[k]) (i<k<=j)
*/
int k=0; //就是当前的h[i]
for(int i=1; i<=n; i++) rk[sa[i]]=i; //根据sa数组反推出rk数组
for(int i=1; i<=n; i++){
if(rk[i]==1) continue; //height[1]=0
if(k) k--; //用到h[i] >= h[i-1]+1的性质
int j=sa[rk[i]-1]; //排名在i前一位的后缀编号
while(i+k<=n && j+k<=n && s[i+k-1]==s[j+k-1]) k++; //逐位匹配
height[rk[i]] = k;
}
// for(int i=1; i<=n; i++) cout<<height[i]<<' '; cout<<'\n';
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>s;
SA(s,s.size(),122); //求解SA数组
FOR(i,1,s.size()) cout<<sa[i]<<' ';
//LCP(s,n); 这是求解n个后缀的LCP(最长公共前缀)的函数,本题用不上
}
以下是SA练习题:
题意:给定一个字符串s和一个字符串t,两者长度相等。对于一个长度为n字符串x,定义f(x,i)为x字符串操作下列操作i次得到的字符串:把第一个字符删掉并插到最后面。(举个例子,x=abcd,则f(x,2)=cdab。)现在求有多少对满足
,要求i,j都在[0,n-1]范围内。
解法:构造一个字符串X,它是这样构成的:
两倍的s + (char)('a'-1) + 两倍的t + (char)('z'+1)
其中(char)('a'-1)相当于-inf标记,(char)('z'+1)相当于+inf标记,注意由于(char)('z'+1)的引入,一开始的m要是123而不是122(因为123是'z'+1的ascii码)
然后对X求解SA数组,剩下的部分文字不好解释,自行看代码(看代码还是挺容易理解的):
int cnt=0, ans=0;
FOR(i,1,n){
if(sa[i]<=nn) cnt++;
else if(nn*2+2 <= sa[i] && sa[i] <= nn*3+1) ans+=cnt;
}
现在解释下为什么要引入-inf和+inf标记
如果没有标记,考虑这个样例:
1
ab
ab
构建的X是abababab,当我们比较f(s,0)和f(t,0)时,在X串中就是比较abababab和abab,那么后者更小,这里对答案没有贡献,但是实际上ab<=ab,应该有贡献。
引入标记之后,构建的X是abab小abab大(这里用“小”和“大”来表示-inf和+inf)。那么当我们比较f(s,0)和f(t,0)时,在X串中就是比较abab小abab大和abab大,前者更小,能产生贡献。
总的来说,所以其实是这样:假设我们比较两个f值,假设是f1和f2,而X中的对应后缀是g1和g2
- f1<f2时,g1<g2,能产生贡献,不用标记也可
- f1>f2时,g1>g2,不会产生贡献,不用标记也可
- f1==f2时,要产生贡献,但是若不加标记,此时X中会判定g1>g2,我们加入了标记之后,就能使得此时g1<g2,使之能产生贡献。所以此时必须加标记。
完整代码:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
#define int long long
const int N=1e6+5;
char s1[N], s2[N];
// char s[N]; int nn=0;
string s;
int n,m,x[N],y[N],c[N],sa[N];
int height[N],rk[N]; //height[i]=LCP(i,i-1), rk[i]是s[i~n]在所有后缀中的字典序排名
void SA(const string&s,int n,int m){
/**
* 首先定义一些变量
* suffix[i]: 表示s[i~n]这个后缀
* rk[i]: suffix[i]在所有后缀中的排名
* sa[i]: 满足suffix[sa[1]] < suffix[sa[2]] < ... < suffix[sa[n]],也就是说,排名为i的后缀的编号是sa[i]
* 有一个很重要的性质:sa[rk[i]] = rk[sa[i]] = i
*/
//对{s[i],i}二元组进行基数排序
for(int i=1;i<=n;i++){x[i]=s[i-1];++c[x[i]];} //c数组是桶,x[i]是第i个元素的第一关键字
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; //注意,这里为了防止sa值重复,c[x[i]]需要--,第一关键字相同时,位置越靠前的后缀排名越靠前
for(int k=1;k<=n;k=k<<1) //2.倍增进行基数排序
{
int num=0; //只是一个数组指针
for (int i=n-k+1;i<=n;++i) y[++num]=i; //定义y[num]表示第二关键字中,排名为num的数的位置,[n-k+1,n]这个区间后面没有字母了,所以是最小的,放在最前面
for(int i=1;i<=n;i++) //在第一关键字中排名越靠前,表示应该排在前面,所以从1~n遍历sa[i],也就是按照第一关键字从小到大遍历
if(sa[i]>k)y[++num]=sa[i]-k; //sa[i]表示排名为i的第一关键字pos,注意pos>k的才会作为当前的第二关键字
//按{第一关键字,第二关键字}进行基数排序
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
swap(x,y); //下面即将要生成新的x数组(新的第一关键字),它要用到旧的第一关键字,所以把旧的暂存在y这里
num=1;x[sa[1]]=1; //当前第一关键字是1,排名第一个的那个(sa[1])的第一关键字也当然是1
for(int i=2;i<=n;i++) //再计算排名是2~n的二元组的新的第一个关键字
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])x[sa[i]]=num; //第一关键字和第二关键字都和排名上一个的一样,那么新的第一个关键字就一样
else x[sa[i]]=++num; //否则就是排名前一个的新的第一关键字+1
}
if(num==n)break; //如果排名数量num已经是n了(已经排好了),就没必要继续排了,剪枝,结束
m=num; //新的关键字数,是旧的排名数
}
// for(int i=1; i<=n; i++) cout<<sa[i]<<' '; cout<<'\n';
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>n>>(s1+1)>>(s2+1);
// FOR(j,0,1) FOR(i,1,n) s[++nn]=s1[i]; s[++nn]=char('a'-1);
// FOR(j,0,1) FOR(i,1,n) s[++nn]=s2[i]; s[++nn]=char('z'+1);
FOR(j,0,1) FOR(i,1,n) s.push_back(s1[i]); s.push_back('a'-1);
FOR(j,0,1) FOR(i,1,n) s.push_back(s2[i]); s.push_back('z'+1);
SA(s,s.size(),123);
// FOR(i,1,n) cout<<sa[i]<<' '; cout<<endl;
int cnt=0, ans=0;
FOR(i,1,s.size()){
if(sa[i]<=n) cnt++;
else if(n*2+2 <= sa[i] && sa[i] <= n*3+1) ans+=cnt;
}
cout<<ans;
}
2.牛客17141
题解链接:139I-Substring_ACM竞赛_ACM/CSP/ICPC/CCPC/比赛经验/题解/资讯_牛客竞赛OJ_牛客网
代码:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
#define mst(a) memset(a,0,sizeof(a))
#define int long long
const int N=3e5+7;
string s,t; int n;
int x[N],y[N],c[N],sa[N];
int height[N],rk[N]; //height[i]=LCP(i,i-1), rk[i]是s[i~n]在所有后缀中的字典序排名
void SA(const string&s,int n,int m){
/**
* 首先定义一些变量
* suffix[i]: 表示s[i~n]这个后缀
* rk[i]: suffix[i]在所有后缀中的排名
* sa[i]: 满足suffix[sa[1]] < suffix[sa[2]] < ... < suffix[sa[n]],也就是说,排名为i的后缀的编号是sa[i]
* 有一个很重要的性质:sa[rk[i]] = rk[sa[i]] = i
*/
//对{s[i],i}二元组进行基数排序
for(int i=1;i<=n;i++){x[i]=s[i-1];++c[x[i]];} //c数组是桶,x[i]是第i个元素的第一关键字
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; //注意,这里为了防止sa值重复,c[x[i]]需要--,第一关键字相同时,位置越靠前的后缀排名越靠前
for(int k=1;k<=n;k=k<<1) //2.倍增进行基数排序
{
int num=0; //只是一个数组指针
for (int i=n-k+1;i<=n;++i) y[++num]=i; //定义y[num]表示第二关键字中,排名为num的数的位置,[n-k+1,n]这个区间后面没有字母了,所以是最小的,放在最前面
for(int i=1;i<=n;i++) //在第一关键字中排名越靠前,表示应该排在前面,所以从1~n遍历sa[i],也就是按照第一关键字从小到大遍历
if(sa[i]>k)y[++num]=sa[i]-k; //sa[i]表示排名为i的第一关键字pos,注意pos>k的才会作为当前的第二关键字
//按{第一关键字,第二关键字}进行基数排序
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]]++;
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
swap(x,y); //下面即将要生成新的x数组(新的第一关键字),它要用到旧的第一关键字,所以把旧的暂存在y这里
num=1;x[sa[1]]=1; //当前第一关键字是1,排名第一个的那个(sa[1])的第一关键字也当然是1
for(int i=2;i<=n;i++) //再计算排名是2~n的二元组的新的第一个关键字
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])x[sa[i]]=num; //第一关键字和第二关键字都和排名上一个的一样,那么新的第一个关键字就一样
else x[sa[i]]=++num; //否则就是排名前一个的新的第一关键字+1
}
if(num==n)break; //如果排名数量num已经是n了(已经排好了),就没必要继续排了,剪枝,结束
m=num; //新的关键字数,是旧的排名数
}
// for(int i=1; i<=n; i++) cout<<sa[i]<<' '; cout<<'\n';
}
void LCP(const string&s,int n){
/**
* 首先定义一些变量
* height[i]: 表示suffix[sa[i]]和suffix[sa[i-1]]的LCP长度,也就是排名相邻的两个后缀的最长公共前缀长度
* h[i]: 表示height[rk[i]],也就是suffix[i]和suffix[i-1]的LCP长度
* 用到一个很重要的性质:h[i] >= h[i-1]+1,它使得LCP函数的均摊复杂度变为O(N)
*
* 跑完这个函数之后,具体有什么用呢?用法如下:
* LCP(i,j) = min(height[k]) (i<k<=j)
*/
int k=0; //就是当前的h[i]
for(int i=1; i<=n; i++) rk[sa[i]]=i; //根据sa数组反推出rk数组
for(int i=1; i<=n; i++){
if(rk[i]==1) continue; //height[1]=0
if(k) k--; //用到h[i] >= h[i-1]+1的性质
int j=sa[rk[i]-1]; //排名在i前一位的后缀编号
while(i+k<=n && j+k<=n && s[i+k-1]==s[j+k-1]) k++; //逐位匹配
height[rk[i]] = k;
}
// for(int i=1; i<=n; i++) cout<<height[i]<<' '; cout<<'\n';
}
char fun[3]={'a','b','c'};
void init() {
mst(x); mst(y); mst(c); mst(sa);
mst(height); mst(rk);
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
while(cin>>n){
cin>>s; t="";
char tag='d';
do{
for(char ch:s) t+=fun[ch-'a'];
t += tag++;
}while(next_permutation(fun,fun+3));
t.pop_back(); //最后一个标记去掉
init();
SA(t,t.size(),122); LCP(t,t.size());
int cnt=1,tmp=0;
for(int i=1; i<s.size(); i++){
s[i]==s[i-1] ? ++tmp : tmp=1;
cmax(cnt,tmp);
}
int len=t.size();
// int ans=cnt*3 + len*(len+1)/2 - 15*(n+1)*(n+1); //15=C[2,5]+C[1,5],也就是选定起点终点的分隔符,然后(n+1)*(n+1)表示左右两边拓宽
// for(int i=1; i<=len; i++) ans-=height[i];
int ans=cnt*3 - 15*(n+1)*(n+1); //15=C[2,5]+C[1,5],也就是选定起点终点的分隔符,然后(n+1)*(n+1)表示左右两边拓宽
for(int i=1; i<=len; i++) ans+=len-sa[i]-height[i]+1; //求本质不同的子串个数板子
cout<<ans/6<<'\n';
}
}