后缀数组

复习提纲:
(一)后缀排序的定义是什么?
(二)后缀排序如何利用倍增进行快速求解?
提示:双关键字都是什么?
(三)归并排序如何融入后缀排序之中?如何优化?
提示:有一维简单调整即可,无需排序。
(四)你能准确说出 \(h\) 数组的定义吗?
(五)记得 \(h\) 数组自身的优秀性质吗?如何求 \(h\) 数组?
(六)子串 LCP 问题怎么解?怎么转 RMQ?
(七)码?
upd 2026/6/13

后缀排序

后缀排序,指的是对于字符串的每个后缀按照字典序大小进行排序。
这里为了方便,每个后缀可以用后缀首字母下标代指。
定义后缀 \(i\) 的排名为 \(rk_i\),字符串排序结果的数组为 \(sa\),简称为后缀数组。

后缀排序基于倍增。
首先按照单字符排序,每个字符获得初始排名 \(rk\) 数组。
会进行 \(\log_n\) 轮排序,第 \(i\) 轮排序块长 \(k=2^{i-1}\)
每个位置第一关键字为 \(rk_i\),第二关键字为 \(rk_{i+k}\)(越界赋无穷小)。
排序时使用基数排序,先计数排序第二关键字,再计数排序第一关键字即可。

优化一:发现,由于第二关键字原本是有序的,只需要排序时把无穷小的放在数组头即可。
优化二:维护前缀和操作值域。
优化三:发现 \(rk\) 数组互异时直接退出。

#include<bits/stdc++.h>
using namespace std;
const int N=1000009,M=127;
int n,rk[N],sa[N],trk[N<<1],tsa[N],cnt[N];
char s[N];
int main(){
    scanf("%s",s+1),n=strlen(s+1);
    //预备排序
    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[s[i]]--]=i;

    //正式排序
    for(int w=1,cur,lim=M;w<n;w<<=1,lim=cur){
        memset(cnt,0,sizeof cnt),cur=0;
        //处理空位置第二关键字
        for(int i=n-w+1;i<=n;++i) tsa[++cur]=i;
        //将 sa 数组中有可能作为第二关键字的位置顺序找出
        //这些位置实际对应 i-w
        for(int i=1;i<=n;++i) if(sa[i]>w) tsa[++cur]=sa[i]-w;

        //计数排序第一关键字
        for(int i=1;i<=n;++i) cnt[rk[i]]++;
        for(int i=1;i<=lim;++i) cnt[i]+=cnt[i-1];
        //实际上在实现数组 tsa->sa 的转换
        for(int i=n;i>=1;--i) sa[cnt[rk[tsa[i]]]--]=tsa[i];

        memcpy(trk,rk,sizeof rk),cur=0;
        //注意这里要实现并列情况 rk 相等
        for(int i=1;i<=n;i++)
            cur+=!(trk[sa[i]]==trk[sa[i-1]]&&trk[sa[i]+w]==trk[sa[i-1]+w]),rk[sa[i]]=cur;
        if(cur==n) break;
    }
    for(int i=1;i<=n;i++) printf("%d ",sa[i]);
    return 0;
}

后缀数组的直接利用

例一 「JSOI2007」字符加密
环上问题转换为链上问题,是为破环为链。
对于二倍字符串进行后缀排序即可。

例二「USACO07DEC」Best Cow Line
贪心选取,
发现队头队尾一样时需要一个“比较当前字串正倒序字典序”的操作。
直接做大概就是哈希正反字典串,二分哈希去相同。
后缀排序简单粗暴,字符串回文处理(\(\mathrm{abc\to abccba}\)),
排序后就实现了这个需求。
本题不需要使用分割字符。

例二「P5353」树上后缀排序
我们原来的操作是对于一条链排序。
想到几个思路,如下:

  1. 暴力合并:
    完全二叉树卡到不低于平方,放弃。
  2. 树剖:
    看起来困难,迫不得已再说。
  3. 扩展理解:
    整体排序,按层倍增,依旧归并。
    归并排序变成了四个关键字,而已。

求两个子串的最长公共前缀

后缀 \(i\) 和后缀 \(j\) 的最长公共前缀(长度)记作 \(lcp(i,j)\)
定义 \(h_i=lcp(sa_i,sa_{i-1})\),特别的,\(h_1=0\)
根据定理 \(h_{rk_i}\ge h_{rk_{i-1}}-1\)\(h\) 数组暴力去求即可。

理解

定义后缀 \(i\) 的前驱为所有字典序比他小的后缀,
直接前驱为前驱中字典序最大的一个。
这个定理的意思是,后缀 \(i\) 和其直接前驱的 LCP 最少为后缀 \(i-1\) 对应值减一。
这里不妨将后缀 \(i-1\) 记作 \(cS\),将后缀 \(i\) 记作 \(S\)

如果后缀 \(cS\) 直接前驱形如 \(cK\) ,那么 \(K\) 一定是 \(S\) 的前驱。
发现 \(K\)\(S\) 的 LCP 就是 \(h_{i-1}-1\),而 \(S\) 与其直接前驱的 LCP 至少为该值。
否则 \(h_{i-1}=0\),自然有 \(h_i\ge h_{i-1}-1\)

综上,得证。

发现对于 \(rk_i<rk_j\)\(lcp(i,j)=\min\limits_{x=rk_i+1}^{rk_j} h_x\),进而转化成 RMQ 问题。
扩展到子串上,对于子串长度取最小值即可。

评测资源

#include<bits/stdc++.h>
using namespace std;
const int N=1000009,M=127,K=21;
char s[N];
int cnt[N],rk[N],sa[N],trk[N<<1],tsa[N],h[N][K],lg[N],n,T;
int main(){
    scanf("%d%s",&T,s+1),n=strlen(s+1);
    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 w=1,cur,p=M;;w<<=1,p=cur){
        memset(cnt,0,sizeof cnt),cur=0;
        for(int i=n-w+1;i<=n;++i) tsa[++cur]=i;
        for(int i=1;i<=n;++i) if(sa[i]>w) tsa[++cur]=sa[i]-w;
        for(int i=1;i<=n;++i) ++cnt[rk[i]];
        for(int i=1;i<=p;++i) cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;--i) sa[cnt[rk[tsa[i]]]--]=tsa[i];
        memcpy(trk,rk,sizeof rk),cur=0;
        for(int i=1;i<=n;i++)
            cur+=(trk[sa[i]]!=trk[sa[i-1]]||trk[sa[i]+w]!=trk[sa[i-1]+w]),rk[sa[i]]=cur;
        if(cur==n) break;
    }
    for(int i=1,j,k=0;i<=n;i++){
        if(rk[i]==1) continue;
        if(k) --k;
        j=sa[rk[i]-1];
        while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
        h[rk[i]][0]=k;
    }
    for(int k=1;(1<<k)<=n;k++) for(int i=1;i+(1<<k)-1<=n;i++) h[i][k]=min(h[i][k-1],h[i+(1<<(k-1))][k-1]);
    lg[1]=0; for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
    for(int i=1,lu,lv,ru,rv,l,r,len,ans;i<=T;i++){
        scanf("%d%d%d%d",&lu,&lv,&ru,&rv),l=rk[lu],r=rk[ru];
        if(lu==ru){
            printf("%d\n",min(rv,lv)-lu+1);
            continue;
        }
        if(l>r) swap(l,r);
        ++l;
        len=lg[r-l+1];
        ans=min(h[l][len],h[r-(1<<len)+1][len]);
        printf("%d\n",min(ans,min(lv-lu+1,rv-ru+1)));
    }
    return 0;
}

h 数组其他应用

  • 判断子串大小关系
    用 LCP 判定子串是否相同,
    若不相同,则直接利用后缀排序求解即可。
  • 本质不同子串数
posted @ 2026-06-07 17:37  2025ing  阅读(6)  评论(0)    收藏  举报