后缀数组SA


后缀数组SA

后缀数组SA

参考

题解 P3809 【【模板】后缀排序】 - xMinh 的博客 - 洛谷博客 (luogu.com.cn)

需要维护

  • sa[i]:排名为i的后缀的起点(左端点)位置
  • rk[i]:起点(左端点)为i的后缀的排名

理解:rk[sa[i]] = i 和 sa[rk[i]] = i

即【排名第i的后缀】的排名为i 和 排名第【左端点为i的后缀的排名】的左端点为i

使用倍增法

一张特别形象的图:sa

这样就能很形象的表示通过倍增进行后缀排序的过程,一直倍增直到排名各不相同为止,因为后缀的长度都不相等,所以结果肯定不会出现排名并列的情况。

图里面有个“没有则填0”是为了方便理解,在代码中应按照从左到右的顺序,先对这些没有第二关键字的后缀从左到右按顺序进行编号,作为第二关键字最小的那几个。

基数排序

一开始的序列是:21 31 32 22 23 13 12
从前往后检查低位放到对应的桶里
c[1]: 21 31
c[2]: 32 22 12
c[3]: 23 13

按照顺序取出来得到新序列:21 31 32 22 12 23 13

从前往后检查高位放入桶中
c[1]: 12 13
c[2]: 21 22 23
c[3]: 31 32

得到序列:12 13 21 22 23 31 32

实现!SA学习中最大的困难竟然在基数排序

代码里,先计算了第二关键字,得到rank
rk2: 1  1  2  2  3  3  2
序列:21 31 32 22 23 13 12

之后根据第一关键字放入桶中
rk2:   3  2
c[1]: 13 12
rk2:   1  2  3
c[2]: 21 22 23
rk2:   1  2
c[3]: 31 32

最后根据第二关键字的大小,按顺序从桶中取数即可。

c[i]是前缀和,记录第一关键字<=i的一共有几个,对于拥有相同第一关键字的数,根据第二关键字从大到小取出,取出得时候分配排名为c[i],并且让c[i]--,这样之后从c[i]取数的排名就比先取的考前。

代码

#include<iostream>
using namespace std;
#define ll long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define repb(i,a,b) for(int i=(a);i>=(b);i--)
const int MAXN = 1114514;
int n,m;
string s;
int rk[MAXN],sa[MAXN],c[MAXN],rk2[MAXN];
//sa[i]存排名i的原始编号 rk[i]存编号i的排名 第二关键字rk2
inline void get_SA(){
    rep(i,1,n) ++c[rk[i]=s[i]];//基数排序
    rep(i,2,m) c[i] += c[i-1];
    //c做前缀和,可以知道每个关键字的排名最低在哪里
    repb(i,n,1) sa[c[rk[i]]--] = i;//记录每个排名的原编号

    for(int w=1;w<=n;w<<=1){//倍增
        int num = 0;
        rep(i,n-w+1,n) rk2[++num] = i;//没有第二关键字的排在前面
        rep(i,1,n) if(sa[i]>w) rk2[++num] = sa[i]-w;
        //编号sa[i]大于w的才能作为编号sa[i]-w的第二关键字
        rep(i,1,m) c[i] = 0;
        rep(i,1,n) ++c[rk[i]];
        rep(i,2,m) c[i]+=c[i-1];
        repb(i,n,1) sa[c[rk[rk2[i]]]--]=rk2[i],rk2[i]=0;
        //同一个桶中按照第二关键字排序
        swap(rk,rk2);
        //这时候的rk2时这次排序用到的上一轮的rk,要计算出新的rk给下一轮排序

        rk[sa[1]]=1,num=1;
        rep(i,2,n)
            rk[sa[i]] = (rk2[sa[i]]==rk2[sa[i-1]]&&rk2[sa[i]+w]==rk2[sa[i-1]+w])?num:++num;
        //下一次排名的第一关键字,相同的两个元素排名也相同
        if(num==n) break;//rk都唯一时,排序结束
        m=num;
    }
}
inline void solve(){
    cin>>s;
    s = ' '+s;
    n = s.size()-1,m = 122;//m为字符个数'z'=122
    get_SA();
    rep(i,1,n) cout<<sa[i]<<' ';
    cout<<endl;
}
int main(){
    solve();
}
//P3809 【模板】后缀排序
//https://www.luogu.com.cn/problem/P3809

最长公共前缀 LCP

Longest Common Prefix Array 最长公共前缀,LCP(i,j)表示suff(sa[i])和suff(sa[j])的最长公共前缀。

LCP的性质

  • $LCP(i,j)=LCP(j,i)$
  • $LCP(i,i) = length(sa[i]) = n-sa[i]+1$,起点在sa[i]的后缀,长度为n-sa[i]+1

引理

  • 对任意的$k\in[i,j]$,有$LCP(i,k)=min{ LCP(i,k),LCP(k,j)}$

证明:是左右两边两个小于等于号来”夹住”的

(1)设$suff(sa[i]),suff(sa[j]),suff(sa[k])$三个串串分别是$a,b,c$

(2)设$p = min(LCP(i,k),LCP(k,j))$,则有$LCP(i,j)\geq p$。(左边夹住了)

(3)用反证法,若$LCP(i,j)\geq p+1$,因为$p = min(LCP(i,k),LCP(k,j))$,有$a[p+1]\neq b[p+1]$或$b[p+1]\neq c[p+1]$,与$a[p+1]=b[p+1]$矛盾。故征得$LCP(i,j)\leq p$

  • 补充:上面的步骤(3)是摘抄自参考博客,但是有些许不严谨,可能存在$a[p+1]\neq b[p+1]$且$b[p+1]\neq c[p+1]$的情况,这样的证明漏了$a[p+1]=c[p+1]$的可能性。我的想法是:由于是经过排序后的后缀,故不等号可以修改为$<$号,即$a[p+1]< b[p+1]$或$b[p+1]<c[p+1]$,可推得$a[p+1]<c[p+1]$。

去博主下面提了一嘴w

这么一来,我们要求$LCP(i,j)$,就可以拆成$LCP(i,k),LCP(k,j)$,之后$LCP(k,j)$可以再拆,这么转化,就有点区间DP合并的感觉了

求LCP

处理height数组,$height[i]$表示$LCP(i-1,i)$,定义$height[1]=0$,这样求$LCP(i,j)$就变成了求区间最小值$min{height(x)|x\in[i+1,j]}$。但是height怎么来?

设$h[i]=height[rk[i]]$,即左端点为i的后缀的height

关键定理:$h[i]>=h[i-1]-1$

证明为摘抄,侵删

题解 P3809 【【模板】后缀排序】 - xMinh 的博客 - 洛谷博客 (luogu.com.cn)

首先我们不妨设第i-1个字符串按排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。

这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rk[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。

第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rk[i-1]]就是0了呀,那么无论height[rk[i]]是多少都会有height[rk[i]]>=height[rk[i-1]]-1,也就是h[i]>=h[i-1]-1。

第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rk[i-1]],

那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rk[i-1]]-1。

到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。但是我们前面求得,有一个排在i前面的字符串k+1,LCP(rk[i],rk[k+1])=height[rk[i-1]]-1;

又因为height[rk[i]]=LCP(i,i-1)>=LCP(i,k+1)

所以height[rk[i]]>=height[rk[i-1]]-1,也即h[i]>=h[i-1]-1。

实现

根据上面的定理:h[i]>=h[i-1]-1 ,即height[rk[i]]>=height[rk[i-1]]-1

用k记录当前的h[i],因为h[i]>=h[i-1]-1,i每次往后移动时k–。

i从头扫到尾,k会减少n,因此我们在处理h[i]时完全可以逐字符比较,最多也只比较2n次。

int height[MAXN];
inline void get_height(){
    int k = 0,j;
    rep(i,1,n) rk[sa[i]] = i;
    rep(i,1,n){
        if(rk[i]==1) continue;//第一名往前没有前缀
        if(k) k--;//h[i]>=h[i-1]-1 即height[rk[i]]>=height[rk[i-1]]-1
        j = sa[rk[i]-1];//找排在rk[i]前面的
        while(j+k<=n&&i+k<=n&&s[i+k]==s[j+k]) ++k;//逐字符比较
        //因为每次k只会-1,故++k最多只会加2n次
        height[rk[i]] = k;
    }
}

写题

第一题

[P4051 JSOI2007]字符加密 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给一个字符串,如JSOI07,对这个串的所有移动得到的串进行排序,如 JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0,输出这些串按顺序排列后最后一个字母组成的串。

是很普通的一题后缀排序,把字符串复制一遍(JSOI07变成JSOI07JSOI07)再对所有的后缀进行排序即可。

#include<iostream>
using namespace std;
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define repb(i,a,b) for(int i=(a);i>=(b);i--)
const int MAXN = 114514<<2;
int n,m;
string s;
int rk[MAXN],sa[MAXN],c[MAXN],rk2[MAXN];

inline void get_SA(){
    rep(i,1,n) ++c[rk[i]=s[i]];
    rep(i,2,m) c[i] += c[i-1];
    repb(i,n,1) sa[c[rk[i]]--] = i;

    for(int w=1;w<=n;w<<=1){
        int num = 0;
        rep(i,n-w+1,n) rk2[++num] = i;
        rep(i,1,n) if(sa[i]>w) rk2[++num] = sa[i]-w;

        rep(i,1,m) c[i]=0;
        rep(i,1,n) ++c[rk[i]];
        rep(i,2,m) c[i] += c[i-1];
        repb(i,n,1) sa[c[rk[rk2[i]]]--]=rk2[i],rk2[i]=0;
        swap(rk,rk2);
        
        rk[sa[1]]=1,num=1;
        rep(i,2,n)
            rk[sa[i]] = (rk2[sa[i]]==rk2[sa[i-1]]&&rk2[sa[i]+w]==rk2[sa[i-1]+w])?num:++num;
        if(num==n) break;
        m = num;
    }
}
inline void solve(){
    cin>>s;
    int tmp = s.size();
    s = ' '+s+s;
    n = s.size()-1;m = 256;
    get_SA();
    rep(i,1,n)
        if(sa[i]<=tmp){
            //rep(j,sa[i],sa[i]+tmp-1) cout<<s[j];
            //cout<<' ';
            cout<<s[sa[i]+tmp-1];
        }
    cout<<endl;
}
int main(){
    solve();
}

第二题

[P4248 AHOI2013]差异 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意:给长n(5e5)的串,问$\sum len(Ti)+len(Tj)-2\cdot LCP(Ti,Tj)|1\leq i<j\leq n$

解:首先常规的使用sa计算出height数组,之后发现这个式子的前半部分可以直接计算,即$\sum len(Ti)+len(Tj) = \frac{(n-1)n(n+1)}{2}$

后半部分$\sum LCP(Ti,Tj)$,因为有$lcp(i,j) = min(lcp(i,k),lcp(k,j))$,即$lcp(i,j) = min{height(x)|i\leq x\leq j}$故转变成 求所有区间最小值之和

所有区间最小值之和的问题,用单调栈实现即可:

  • 维护单调栈,栈底是从$height(2)$到$height(i)$最小的元素,从底到顶单调递增

  • 用$dp[i]$记录以$i$为右端点的所有区间的最小值之和。

  • 对于一个新的元素$height(i)$,要找到它左边最近的比他小的元素p,在访问到$height(i)$时,把栈顶大于$height(i)$的都出栈即可。那么则有$dp[i] = dp[j]+(i-p)*height(i)$

  • 当然,如果$height(i)$就是最小的元素,直接$dp[i] = (i-1)*height(i)$

代码:

#include<iostream>
using namespace std;
#define rep(i,a,b)  for(int i=(a);i<=(b);i++)
#define repb(i,a,b) for(int i=(a);i>=(b);i--)
#define ll long long
const int MAXN = 114514<<3;
int n,m;
string s;
int rk[MAXN],sa[MAXN],c[MAXN],rk2[MAXN];
inline void get_SA(){
    rep(i,1,n) ++c[rk[i]=s[i]];
    rep(i,2,m) c[i] += c[i-1];
    repb(i,n,1) sa[c[rk[i]]--] = i;

    for(int w=1;w<=n;w<<=1){
        int num = 0;
        rep(i,n-w+1,n) rk2[++num] = i;
        rep(i,1,n) if(sa[i]>w) rk2[++num] = sa[i]-w;

        rep(i,1,m) c[i] = 0;
        rep(i,1,n) ++c[rk[i]];
        rep(i,2,m) c[i]+=c[i-1];
        repb(i,n,1) sa[c[rk[rk2[i]]]--]=rk2[i],rk2[i]=0;

        swap(rk,rk2);
        rk[sa[1]]=1,num=1;
        rep(i,2,n)
            rk[sa[i]] = (rk2[sa[i]]==rk2[sa[i-1]]&&rk2[sa[i]+w]==rk2[sa[i-1]+w])?num:++num;
        if(num==n) break;
        m = num;
    }
}
int height[MAXN];
inline void get_height(){
    int k = 0,j;
    rep(i,1,n) rk[sa[i]] = i;
    rep(i,1,n){
        if(rk[i]==1) continue;
        if(k) k--;
        j = sa[rk[i]-1];
        while(j+k<=n&&i+k<=n&&s[i+k]==s[j+k]) ++k;
        height[rk[i]] = k;
    }
}
//上面都是板子
ll res;
ll dp[MAXN];//dp[i]以i为右端点的左侧最小值之和
int stk[MAXN],ed;//单调栈
inline void solve(){
    cin>>s;
    n = s.length();
    s = ' '+s;
    m = 256;
    get_SA();
    get_height();

    //单调栈计算所有区间的最小值之和
    ed = 0;
    res = (n-1)*n/2*(n+1);
    rep(i,2,n){
        while(0<ed&&height[stk[ed]]>=height[i]) ed--;
        if(0<ed) dp[i] = dp[stk[ed]] + (i-stk[ed])*height[i];
        else dp[i] = (i-1)*height[i];
        stk[++ed] = i;
    }
    ll summ = 0;
    rep(i,2,n) summ += dp[i];
    res -= 2*summ;
    cout<<res<<endl;
}
int main(){
    solve();
}

文章作者: REXWind
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 REXWind !
评论
  目录