首页 > 技术文章 > 后缀数组学习笔记

xzyxzy 2018-06-14 19:13 原文

后缀数组(SA)

Tags:字符串

作业部落


一、概述

一个可以对一个字符串的所有后缀处理的算法
网上优秀博客

二、题单

三、题解

[luogu3809]【模板】后缀排序
板子,求\(SA\)数组

[hihocoder1403] 后缀数组一·重复旋律
\(height\)后单调队列求解至少出现\(k\)次的最长子串

[hihocoder1407] 后缀数组二·重复旋律2
[hihocoder1415] 后缀数组三·重复旋律3
[POJ2774]Long Long Message
[Luogu2852][USACO06DEC]牛奶模式Milk Patterns
求两个字符串中的最长公共子串,把两串中间用一个从未出现过的字符连起来,跑\(SA\)后二分答案,如果有互不重叠且\(height\)\(min\)大于\(mid\)的那么返回\(1\)

[BZOJ3238][AHOI2013]差异Hard
求串\(s\)中任意两个子串的\(lcp\)(最长公共前缀),利用性质\(i\),\(j\)\(lcp\)\(min(height[i+1]...height[j])\),所以维护一个单调递增栈,设\(f[i]\)表示栈中第\(i\)个位置到\(1..i-1\)个位置的所有贡献,通过\(f[i]=height[i]*(i-sta[top])+f[sta[top]];\)可以\(O(1)\)算贡献

[Luogu2463][SDOI2008]Sandy的卡片
[BZOJ2946]公共串
求多串中的最长公共子串,将所有串连接成一个串\(s\),中间用一个从未出现过的字符隔开,那么\(Get\_SA\)后二分最长公共子串的长度,若\(height\)数组中有连续的一段\(\ge mid\),且这一段中所有的子串都出现过,那么合法

[BZOJ1031]字符加密
把字符串\(×2\),在按照\(rank\)数组输出符合条件的字符

[BZOJ4199][NOI2015]品酒大会Hard
求最长公共前缀长度为\(l\)的后缀对数以及得分最大的一对的得分。
那么一段区间都大于等于\(l\),它会被\(0...l-1\)都计算贡献,所以大的贡献不会影响小的贡献,把height排序之后从大到小处理,每处理一个\(height[k]\)就把\(SA[k],SA[k-1]\)两个串并起来,表示之后会一起处理,同时答案加上两个并查集的\(siz\)之积(两并查集内任意一对都符合条件),最大值便是两并查集最大值之积或最小值之积(负数)

[BZOJ4556][TJOI2016&&HEOI2016]字符串Hard
\(O(logn)\)求一个串\(s_{c..d}\)与一段区间\([a,b]\)内的串的最长前缀。
二分答案,那么符合答案的是\(SA\)上一段连续的区间,分别二分左右端点,现在就是判断\(SA\)\([l,r]\)上是否存在一个数在\([a,b]\)之间,转化为二维数点问题,用主席树完成

四、经验

\(A.\)
多个字符不好处理的时候在中间依次添加从未出现过的字符(第一次加\(#\)第二次加\(&\)什么的),使得前后两个字符不会有\(height\)的贡献

\(B.\)
排好序后,二分一段区间使得以区间任意一个位置为开头的字符串与字符串\(x\)(\(x\)也在区间内)的最长前缀时,建议分别二分左右端点,而不是倍增地跳(调试2hTJOI2016字符串,因为不好选取倍增起点,ST表中任意一个数值都至少由两个字符串得来)

五、模板

洛谷3809后缀排序
记忆方式
Sort里面4行,预处理1行,倍增求SA枚举1行,中间5行
height2行枚举3行内容

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;
const int MAXN=2200000;
char ch[MAXN];//字符串
int m=1000;//字符集大小
int s[MAXN],l,len1,len2;//字符串长度
int SA[MAXN];//SA[i]表示排名为i的字符串是以SA[i]为开头的后缀
int x[MAXN],y[MAXN];//记录两维信息方便排序
int t[MAXN];//桶
int Rank[MAXN];
int height[MAXN];
/*
  SA[i]=k 表示排名为i的是第k号串
  x[i]=k 表示i号串的第一关键字排名为k
  y[i]=k 表示第二关键字排名为i的是第k号串
  t[i]=k (统计前缀和之前)表示排名并列为i的串的数量是k
         (统计前缀和之后)表示排名为1~i的串的数量是k
  Rank[i]=k 表示第i号串的排名是k(与SA互逆)
  height[i]=k 表示lcp(Suffix(SA[i-1]),Suffix(SA[i]))
              就是排名为i-1的串和排名为i的串的最长公共前缀
 */
int cmp(int i,int j,int k)
{
	return y[i]==y[j]&&y[i+k]==y[j+k];
}
void Sort()
{
	for(int i=1;i<=m;i++) t[i]=0;
	for(int i=1;i<=l;i++) t[x[i]]++;
	for(int i=1;i<=m;i++) t[i]+=t[i-1];
	for(int i=l;i>=1;i--) SA[t[x[y[i]]]--]=y[i];//容易打反
}
void Get_SA()
{
	for(int i=1;i<=l;i++) x[i]=s[i],y[i]=i; Sort();
	for(int k=1,p=0;k<=l;k<<=1)
	{
		for(int i=l-k+1;i<=l;i++) y[++p]=i;
		for(int i=1;i<=l;i++) if(SA[i]>k) y[++p]=SA[i]-k;
		Sort();swap(x,y);x[SA[1]]=p=1;
		for(int i=2;i<=l;i++) x[SA[i]]=cmp(SA[i],SA[i-1],k)?p:++p;//容易x中不写SA[i]
		if(p==l) break; m=p;p=0;
        //如果第一维已经有序,那么就不需要继续排序了~
	}
	/*
	for(int i=1;i<=l;i++) Rank[SA[i]]=i;
	for(int i=1,j=0;i<=l;i++)
	{
	    if(j) j--; //利用性质:height[Rank[i]]>=height[Rank[i-1]]-1
		while(s[i+j]==s[SA[Rank[i]-1]+j]) j++;
		height[Rank[i]]=j;
	}
	 */
}
int main()
{
	scanf("%s",ch); int len=strlen(ch);
	for(int i=0;i<len;i++) s[++l]=ch[i];
	Get_SA();
	for(int i=1;i<=l;i++) printf("%d ",SA[i]);
	puts("");return 0;
}

推荐阅读