后缀数组 (SA) 学习笔记

写得很草率的一篇东西。

后缀排序

#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
	int xr=0,F=1;char cr=getchar();
	while(cr<'0'||cr>'9') {if(cr=='-') F=-1;cr=getchar();}
	while(cr>='0'&&cr<='9')
	    xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
	return xr*F;
}
const int N=2e6+5;
int n,m;
char s[N];
int sum[N],sa[N],tp[N],rk[N];
//编号:后缀 (i~n) 的编号为 i 
//sa[i] (上一轮)排名为 i 的后缀的编号 
//rk[i] (上一轮)编号为 i 的串的排名
//tp[i] 第二关键字排名为 i 的串的编号 
void qsort()
{
	//sum[i] 桶,存 rk[x]=i 的 x 数量 
	for(int i=0;i<=m;i++) sum[i]=0;//清空桶 
	for(int i=1;i<=n;i++) sum[rk[i]]++; 
	for(int i=1;i<=m;i++) sum[i]+=sum[i-1];//前缀和,此时 sum[i] 表示 rk[x]<=i 的 x 数量
	//所以 sum[i] 就是 rk[x]=i 的这个 x 排序后的排名 
	for(int i=n;i;i--) sa[sum[rk[tp[i]]]--]=tp[i];
	//这句话到底在干啥。 
	//因为 sum[i] 是所有 rk[x]=i 中最后一个位置,要倒序往里填
	//倒序枚举 i,实际求的是 tp[i] 的排名,这样枚举是保证第二关键字降序 
	//rk[tp[i]] 为 (第二关键字排名为 i 的串)的 第一关键字排名 
	//sum[rk[tp[i]]],现在要填的位置,填完后 sum--,代表下次往前一个位置填
	//sa[sum[rk[tp[i]]]] 现在位置是 sum[rk[tp[i]]] 的串,它的编号是 tp[i] 
}
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1),m=200;
	for(int i=1;i<=n;i++) rk[i]=s[i],tp[i]=i;
	//初始排名,第一关键字是起始字符,第二关键字是位置 
	qsort();
	for(int w=1,tot=0;tot<n;m=tot,w<<=1)  
	{
		//tot 当前有多少种不同的排名,如果已经有 n 个代表所有串都排出来了,退出
		//w 当前倍增的长度,即 rk 和 tp **分别** 已经排完的长度
		//实际上是在求长度为 2w 时的答案 
		tot=0;
		for(int i=n-w+1;i<=n;i++) tp[++tot]=i;
		//[n-w+1,n] 的长度不足 w,第二关键字为 0,按起始位置排名 
		for(int i=1;i<=n;i++)
			if(sa[i]>w) tp[++tot]=sa[i]-w;
			//按第二关键字的排名从小到大枚举串,sa[i] 即编号为 sa[i]-w 的串的第二关键字 
		qsort();swap(tp,rk);
		//现在的 sa 已经更新成长度为 2w 的答案了,但 rk 没更新
		//把长度为 w 时的答案备份进 tp 里,因为原来的 tp 没用了 
		//现在的 tp[i] 表示 上一轮排序中 编号为 i 的串的排名 
		tot=rk[sa[1]]=1;
		//tot 表示当前的排名 
		for(int i=2;i<=n;i++)
		//枚举新排名为 i 的串,如果它前一半和后一半都和上一个串的旧排名一样,它们新排名还是一样
		//否则排名是上一个串+1 
			rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+w]==tp[sa[i-1]+w])?tot:++tot;
		//这里面 rk[i] 是可以重复的,但 sa[i] 不可以,一定要绕明白( 
	}
	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
	return 0;
}
struct 封装版(方便复制)
struct suffix_array
{
    int n,m,h[N],sa[N],rk[N],tp[N],sum[N];
    char s[N];
    il void qsort()
    {
        for(int i=1;i<=m;i++) sum[i]=0;
        for(int i=1;i<=n;i++) sum[rk[i]]++;
        for(int i=1;i<=m;i++) sum[i]+=sum[i-1];
        for(int i=n;i;i--) sa[sum[rk[tp[i]]]--]=tp[i];
    }
    il void SA()
    {
        for(int i=1;i<=n;i++) rk[i]=s[i],tp[i]=i;
        m=200,qsort(); 
        for(int w=1,tot=0;tot<n;m=tot,w<<=1)
        {
            tot=0;
            for(int i=n-w+1;i<=n;i++) tp[++tot]=i;
            for(int i=1;i<=n;i++) if(sa[i]>w) tp[++tot]=sa[i]-w;
            qsort(),swap(rk,tp); rk[sa[1]]=tot=1;
            for(int i=2;i<=n;i++)
                rk[sa[i]]=(tp[sa[i]]!=tp[sa[i-1]]||tp[sa[i]+w]!=tp[sa[i-1]+w])?++tot:tot;
        }
        for(int i=1,now=0;i<=n;i++)
        {
            if(now) now--;
            while(s[i+now]==s[sa[rk[i]-1]+now]) now++;
            h[rk[i]]=now;
        }
    }
};

例题

P4051 [JSOI2007] 字符加密
SA 板子,把 s 复制两遍断环为链,再后缀排序。

height 数组

定义

\(height[i]=lcp(sa[i],sa[i-1])\),其中 \(height[1]=0\)

性质

\(height[rk[i]]\ge height[rk[i-1]]-1\)

求法

根据上面的性质即可 \(O(n)\) 求出。

void get_hgt()
{
	for(int i=1,now=0;i<=n;i++)
	{
		if(now) now--;
		while(s[i+now]==s[sa[rk[i]-1]+now]) now++;
		hgt[rk[i]]=now;
	}
}

例题

P2852 [USACO06DEC]Milk Patterns G

题意简述:给一个字符串,求至少出现 \(k\) 次的串的最长长度。
Solution:
一个串至少出现 \(k\) 次,则至少出现在 \(k\) 个后缀的 LCP 中。
所以答案是 所有连续 \(k-1\) 个 height 的最小值中的最大值。
单调队列即可做到 \(O(n)\)

P2743 [USACO5.1]乐曲主题Musical Themes

题意简述:找最长的 \(s\),满足 \(s\) 在字符串中至少不重叠地出现了两次。
Solution:
二分答案 \(mid\),将 height 数组划分成连续的几段,其中每一段除第一个外 height 都>=mid。
求出每一段 \(sa[i]\) 的最大最小值,若 \(mx-mn>=mid\),则 \(mid\) 合法,两个串起始位置分别为 mx 和 mn。
在本题中,由于求的是差分数组,mx-mn 不取等号。

POJ 1226/ybt 例2

题意简述:给定 \(n\) 个字符串,求一个最长的字符串,要求其在 \(n\) 个串或者它们翻转后的串中出现过。
Solution:
把这 \(n\) 个串和它们反转后的串连在一起,中间用不同且没出现过的字符隔开。
二分答案将 \(height[i]\) 分段,若有一段包含了这 \(n\) 个串,答案合法。用 set 维护。

posted @ 2023-02-17 16:03  樱雪喵  阅读(97)  评论(0编辑  收藏  举报