【学习笔记】后缀数组和后缀自动机

后缀数组(后缀排序)Suffix Array

惯例:直接放题【模板】后缀排序

学习来源link

先口胡一下我对这玩意的理解吧

后缀是和前缀类似的东西,学后缀的一般都学过前缀,至少是前缀和呀啥的

但是一般后缀会针对字符串上的操作,就像这个题里面 “ 把字符串的所有非空后缀按字典序从小到大排序 ”

后缀排序就是构建后缀数组的过程(就是把后缀排个序,拍完序的数组就叫后缀数组)

后缀排序的实现主要依靠倍增法和基数排序来实现

定义

\(sa[i]:\) 排名为\(i\)的后缀的位置

\(rak[i]:\) 从第 \(i\) 个位置开始的后缀的排名,下文为了叙述方便,把从第\(i\)个位置开始的后缀简称为"后缀\(i\)"

\(tp[i]\):基数排序的第二关键字,意义与\(sa\)一样

\(tax[i]\)\(i\)号元素出现了多少次。辅助基数排序

算法原理

(构造后缀数组还有DC3算法(三分?)和建后缀树,跑dfs序的两种\(O(n)\)做法,但是常用的是后缀排序)

观察易得:

\(rk[sa[i]]=i\space \space \space \space \space \space \space \space sa[rk[i]]=i\)

如果直接sort会爆掉复杂度,因为我们的字符串长度和比较的时候复杂度和长度相关

所以我们就对每一位做文章

先是基数排序,把所有的后缀按照第一位的字符排个序

然后考虑倍增

这里的倍增代码和\(fft\)的有一点点像

for(int w=1,p=0;p

怕是天下倍增是一家

然后比较巧妙的一步就是在于“\(i\)号后缀的前\(\frac{w}{2}\)个字符形成的字符串是\(i-\) \(\frac{w}{2}\)号后缀的后\(\frac{w}{2}\)个字符形成的字符串”

这里大概需要意会我们去掉了后缀\(i\)的后面部分

我们每一次执行循环中的内容的时候有一个部分排好序的后缀数组(可以意会的)

这里每一次考虑倍增新出来的那些位数,对它们进行基数排序就可以了

CODE

#include 
using namespace std;
#define int long long
namespace yspm {
inline int read() {
    int res = 0, f = 1;
    char k;
    while (!isdigit(k = getchar()))
        if (k == '-')
            f = -1;
    while (isdigit(k)) res = res * 10 + k - '0', k = getchar();
    return res * f;
}
const int N = 1e6 + 10;
char s[N];
int n, m, rk[N], sa[N], tax[N], tp[N];
inline void qsort() {
    memset(tax, 0, sizeof(tax));
    for (int i = 1; i <= n; ++i) tax[rk[i]]++;
    for (int i = 1; i <= m; ++i) tax[i] += tax[i - 1];
    for (int i = n; i >= 1; --i) sa[tax[rk[tp[i]]]--] = tp[i];
    return;
}
inline void work() {
    m = 75;
    for (int i = 1; i <= n; ++i) rk[i] = s[i] - '0' + 1, tp[i] = i;
    qsort();
    for (int w = 1, p = 0; p < n; m = p, w <<= 1) {
        p = 0;
        for (int i = 1; i <= w; ++i) tp[++p] = n - w + i;
        for (int i = 1; i <= n; ++i)
            if (sa[i] > w)
                tp[++p] = sa[i] - w;
        qsort();
        swap(tp, rk);
        rk[sa[1]] = p = 1;
        for (int i = 2; i <= n; ++i) {
            if (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w])
                rk[sa[i]] = p;
            else
                rk[sa[i]] = ++p;
        }
    }
    for (int i = 1; i <= n; ++i) printf("%lld ", sa[i]);
    return puts(""), void();
}
signed main() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    work();
    return 0;
}
}  // namespace yspm
signed main() { return yspm::main(); }

然后后缀数组比较重要的是 \(h\) 数组和 \(height\) 数组

\[height[i]=LCP(sa[i-1],sa[i]) \]

\(height\) 数组有以下性质:

若两个下标 \(j,k\) 满足 \(rk_j

那么\(LCP(j,k)=\min\limits_{l=j+1}^k height_l\)

这个感觉就比较有用了

然而不太好求

再定义:\(h_i=height[rk_i]\)

然后 \(h\) 数组有以下性质:

\[h_i\ge h_{i-1}-1 \]

关于上面两条性质的证明?link

运用 \(h\) 数组的性质,我们就可以在较低复杂度内暴力计算出 \(h,height\)

然后我们就可以来做题了


\(Suffix\ Array\) 求本质不同的子串个数的方法:

求出来 \(sa\)\(height\) 数组,然后

\[ans=\sum _ {i=1}^n n+1-sa_i-height_i \]


后缀数组求任意子串 \(lcs\) 的方法:

把原串逆过来复制一次放到原串末尾(中间要添加字符的)

然后就又变成了 \(lcp\) 问题


求一些后缀的 \(lcp\) 和的问题

(好像可以用后缀虚树,即 \(bzoj\ \ SvT\) 一题的全称就是 \(Suffix\ virtual\ Tree\)

用后缀数组就单调栈就好了(大概从贡献的角度想一下,不难看到单调栈的影子)

后缀自动机 Suffix Automation

前置扯淡

作为一个没有学会AC自动机的蒟蒻就敢来学后缀自动机……

回到正题

这篇博客写得是真的好,相当清楚,而且有图link

后缀自动机是一个对\(trie\)树的压缩版,就是把\(trie\)树上的东西合并了一下

应该还是在一个\(DAG\)上,这个和\(AC\)自动机有点像?

首先要理解\(endpos\)的简单定义和与它相关的一坨结论(定理&&推论)

一个子串的\(endpos\)就是它在原串中出现的时候,所有末尾的下标所构成的集合

这里有两点:

1-子串可能不止在原串中出现一次,所以这可能是一个多元集;

2-这个集合里面都是数字,别弄混(里面都是下标)

其次就是构造和应用了(去链接里看吧)

构造

要分3种情况,和\(trie\)有点点类似(但是跳\(fa\)跳来跳去真复杂)

应用

1.判断子串

\(trie\)树类似

2.不同子串个数

问题2:不同子串个数
\(DAG\)\(DP\) 。对于一个节点 \(i\)\(f[i]\) 表示从 \(i\) 出发的子串个数(不含空串)。那么,\(f[i]\) 就等于 \(\sum_{(i,j)\in Edge}(f[j]+1)\)

\(f[1]\) 即是答案。

3.问题3:在原串所有子串中(相同的不算一个)字典序第 \(i\) 大的是哪个

P3975 TJOI2015 弦论

直接放博客My Solution

4.判断两个串的最长公共子串

把两个串拼起来,中间加特殊字符,跑后缀自动机。

然后用类似于上面处理出现次数的方法,跑出一个子串在拼起来的串前半部分出现的次数和后半部分出现的次数。然后遍历节点,找 \(len\) 最大的前后出现次数都不为\(0\) 的节点。

以上思路还可以处理多个字符串的最长公共子串。

CODE

最后是模板题代码(题解看这里link)

#include
using namespace std;
#define int long long
#define reg register
namespace yspm{
	inline int read()
	{
		int res=0,f=1; char k;
		while(!isdigit(k=getchar())) if(k=='-') f=-1;
		while(isdigit(k)) res=res*10+k-'0',k=getchar(); 
		return res*f;
	}
	const int N=2e6+10;
	int tot=1,las=1,head[N],cnt,val[N];
	struct edge{int to,nxt;}e[N];
	struct node{
		int ch[26],len,fa;
		node(){memset(ch,0,sizeof(ch)); len=fa=0; return ;}
	}p[N];
	inline void adde(int u,int v)
	{
		e[++cnt].nxt=head[u]; e[cnt].to=v; return head[u]=cnt,void();
	}
	inline void addp(int x)
	{
		int tmp=las,np=las=++tot; val[tot]=1; p[np].len=p[tmp].len+1;
		for(;tmp&&!p[tmp].ch[x];tmp=p[tmp].fa) p[tmp].ch[x]=np;
		if(!tmp) p[np].fa=1;
		else 
		{
			int q=p[tmp].ch[x];
			if(p[q].len==p[tmp].len+1) p[np].fa=q;
			else 
			{
				int nq=++tot;
				p[nq]=p[q]; p[nq].len=p[tmp].len+1;
				p[q].fa=p[np].fa=nq;
				for(;tmp&&p[tmp].ch[x]==q;tmp=p[tmp].fa) p[tmp].ch[x]=nq;
			}
		}
		return ;
	}
	char s[N]; int len,ans;
	inline void dfs(int x)
	{
		for(reg int i=head[x];i;i=e[i].nxt)
		{
			dfs(e[i].to); val[x]+=val[e[i].to];
		}
		if(val[x]!=1) ans=max(ans,val[x]*p[x].len); 
		return ;
	}
	signed main()
	{
		scanf("%s",s); len=strlen(s);
		for(reg int i=0;i

应用方面:


求本质同的子串个数:

考虑到 \(sam\) 上根节点到任意一个点都是一个子串,且其本身是个 \(Dag\)

所以拓扑 \(dp\) 一下就好了

如果在线的话,那就考虑每个点的贡献吧:\(max_i-min_i+1\)\(max_i-max[fa[i]]\)


两串 \(LCP\) :

其实就是后缀树上点的 \(lca\)\(len\)(然后“差异”那个题就可以考虑每条边的长度是 \(len_i-len[fa[i]]\) 然后贡献法一下了)

关于后缀树:

这个比较就是每个点的父亲向点建边,建成了一棵树,就是后缀树

关于 \(parent\) 树: 反串的后缀树


\(k\) 小子串:

先算出来每个点的点权,然后再求一遍点权和,然后在后缀自动机上像线段树二分那样跑就好了


最小表示法:

这个比较简单了,把原串复制一遍,然后直接在后缀自动机上面跑,能往小的点上跑,就跑,没有后继节点就跳 \(fail\)


求最长公共子串的长度:

用其中一个字符串建后缀自动机,然后把其它的子串放在上面匹配

记录下来每个点匹配的最长长度,然后在每个点上面取记录下来 \(max\)\(min\)

最后的答案就是所有点 \(min\)\(max\)

(很绕,但是很好理解)

你可能感兴趣的:(【学习笔记】后缀数组和后缀自动机)