SA(后缀数组)板子

学习链接:

「后缀排序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练习题:

1.F - Two Strings 

 题意:给定一个字符串s和一个字符串t,两者长度相等。对于一个长度为n字符串x,定义f(x,i)为x字符串操作下列操作i次得到的字符串:把第一个字符删掉并插到最后面。(举个例子,x=abcd,则f(x,2)=cdab。)现在求有多少对(i,j)满足 f(s,i)\leqslant f(t,j),要求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

  1. f1<f2时,g1<g2,能产生贡献,不用标记也可
  2. f1>f2时,g1>g2,不会产生贡献,不用标记也可
  3. 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';
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值