[学习笔记] 后缀数组

上一次把后缀自动机的博客补了之后,现在我又来把后缀数组这个坑给填了吧。

但有一说一这东西还是比后缀自动机好理解的,我完全看懂也没花多久。我还是尽量把关键点都讲清楚,在此基础上尽量缩小篇幅,首先感谢一下这位大佬的博客,我是看着他的博客学的。

后缀数组是什么?

后缀数组,顾名思义,我们肯定要求一个数组来完成许多复杂的功能。后缀数组通常指 \(sa[i]\) 即后缀 \(i\) 的字典序排序,还要 \(height[i]\) 等等。不要着急,下文我们会详细讲。

对比于后缀自动机,后缀数组的应用更为复杂,通常需要一些结论和高超的技巧。所以弱智的我通常选择使用后缀自动机,他们能解决的问题很大程度上是重合的。但也有不少毒瘤题只能用后缀数组来做,学习他是很有必要的。

如何求 sa?

先给出一些基础的定义:\(rk[i]\) 表示把所有的后缀都放在一起字典序排序,后缀 \(i\) 的排名。\(sa[i]\) 表示排名为 \(i\) 的后缀是什么(也就是他在原数组中的起始下标)

暴力求他们是 \(O(n\log n)\) 的,优化用到了 倍增 的思想。

所以倍增什么呢?我们选择倍增 当前比较的长度 \(k\),具体来说,我们只找到每个后缀的前 \(k\) 位得到的排序结果。然后尝试用这个结果快速扩展到 \(2k\),如果这个过程能做到 \(O(n)\) ,那么时间复杂度就能做到 \(O(n\log n)\)

我们设 \(x[i]\) 为后缀 \(i\) 的第一关键字,也就是只看前 \(k\) 位是排在第几名的(如果前 \(k\) 为相同的话排名是一样的),这个数组是我们的已知条件 ,一定要利用好。

\(y[i]\) 为第二关键字,但是因为好写的原因所以他表示的是 第二关键字排名为 \(i\) 的后缀是什么 ,求出他对于求出在 \(2k\) 意义下的 \(x'\) 数组具有重要意义。

问题变成了怎么求 \(y\) 数组,首先对于 \([n-k+1,n]\) 这些后缀是没有第二关键字的,所以可以直接放在最前面(空串的字典序最小嘛)。然后倍增的思想一定会用到 \(k\) 意义下的结果的,最经典的就是 \(fa[i][j]=fa[fa[i][j-1]][j-1]\)。类似地,我们可以考虑用 \(i+k\) 的第一关键字来搞 \(i\) 的第二关键字。

我们按 \(k\) 意义下的排名枚举后缀(也就是当前的 \(sa\) 数组),如果 \(sa[i]>k\) ,那么可以把后缀 \(sa[i]-k\) 加入 \(y\) 数组中(所以我们要按字典序来嘛)

得到 \(y\) 以后我们先用 \(x\) 建一个桶,然后 \(y\) 在后面的后缀就先拿他的排名(保证 \(x\) 同类按 \(y\) 排序),这样我们就得到了新的 \(sa\) ,然后我们在处理新的 \(x\) 就可以了,这一部分不是关键,可以直接看代码。

但是我懒得写注释了,我就直接用了大佬带注释的代码,侵删(有些奇怪的宏定义不用管):

inv get_SA()
{
	for (rint i=1;i<=n;++i) ++c[x[i]=s[i]];
	//c数组是桶 
	//x[i]是第i个元素的第一关键字 
    for (rint i=2;i<=m;++i) c[i]+=c[i-1]; 
    //做c的前缀和,我们就可以得出每个关键字最多是在第几名 
    for (rint i=n;i>=1;--i) sa[c[x[i]]--]=i; //排名为...的后缀是i 
    for (rint k=1;k<=n;k<<=1)
    {
        rint num=0;
        for (rint i=n-k+1;i<=n;++i) y[++num]=i;
        //y[i]表示第二关键字排名为i的数,第一关键字的位置 
		//第n-k+1到第n位是没有第二关键字的 所以排名在最前面 
        for (rint i=1;i<=n;++i) if (sa[i]>k) y[++num]=sa[i]-k;
        //排名为i的数 在数组中是否在第k位以后
		//如果满足(sa[i]>k) 那么它可以作为别人的第二关键字,就把它的第一关键字的位置添加进y就行了
		//所以i枚举的是第二关键字的排名,第二关键字靠前的先入队 
		//所以这里应该是看排名为i的数的贡献 
        for (rint i=1;i<=m;++i) c[i]=0;
        //初始化c桶 
        for (rint i=1;i<=n;++i) ++c[x[i]];
        //因为上一次循环已经算出了这次的第一关键字 所以直接加就行了 
        for (rint i=2;i<=m;++i) c[i]+=c[i-1];//第一关键字排名为1~i的数有多少个 
        for (rint i=n;i>=1;--i) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        //因为y的顺序是按照第二关键字的顺序来排的 
        //第二关键字靠后的,在同一个第一关键字桶中排名越靠后 
        //基数排序 
        swap(x,y);
		//这里不用想太多,因为要生成新的x时要用到旧的,就把旧的复制下来,没别的意思 
        x[sa[1]]=1;num=1;
        for (rint i=2;i<=n;++i)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num;
        //因为sa[i]已经排好序了,所以可以按排名枚举,生成下一次的第一关键字 
        if (num==n) break;
        m=num;
        //这里就不用那个122了,因为都有新的编号了 
    }
    for (rint i=1;i<=n;++i) putout(sa[i]),putchar(' ');
}

如何求 height?

如果后缀数组只能排序的话那真是太鸡肋了,他的更广阔的应用需要 \(height[i]\) 也就是 \(sa[i-1]\)\(sa[i]\) 的最长公共前缀,我们来证明若干结论(有关求法和应用),约定 \(lcp(i,j)\) 表示后缀 \(sa[i],sa[j]\) 的最长公共前缀。

结论1:\(lcp(i,k)=\min(lcp(i,j),lcp(j,k))\;\;1\leq i\leq j\leq k\leq n\)

\(p=\min(lcp(i,j),lcp(j,k))\),那么 \(lcp(i,j)\geq p,lcp(j,k)\geq p\),这个不等式其实是有物理意义的,也就是 \(sa[i],sa[j]\) 的前 \(p\) 个字符相等,\(sa[j],sa[k]\) 的前 \(p\) 个字符相等,所以 \(lcp(i,k)\) 至少是 \(p\)

然后再用反证法,假设 \(lcp(i,k)\)\(p+1\),那么 \(s_i[p+1]=s_k[p+1]\),而我们知道 \(s_i[p+1]\not=s_j[p+1]\) 或者 \(s_j[p+1]\not=s_k[p+1]\),所以可以推出来是不成立的,那么 \(lcp(i,k)=p\)

这个结论有一些引申出来的结论,譬如:\(lcp(i,j)=\min(lcp(k,k+1))\;\;i\leq k<j\) ,其实这个就相当于一个 \(dp\),不难证明。那么我们求出来了 \(height[i]\) 之后就可以用 \(st\)\(O(1)\) 求出两个后缀 \(i,j\) 的最长公共前缀。

结论2:\(h[i]\geq h[i-1]-1\)

这个结论是用来求 \(height\) 数组的,\(h[i]\) 的定义是原来的后缀 \(i\) 与后缀 \(sa[rk[i]-1]\) 的最长公共前缀,它的定义是基于原串的,根据定义可以知道:\(height[i]=h[rk[i]]\) ,那么问题就转化成了求 \(h\) 数组。

设排名在后缀 \(i-1\) 的后缀是 \(k\) ,那么他们的最长公共前缀是 \(h[i-1]\),我们考虑后缀 \(k+1\)\(i\) 的关系,但请注意:他们两个的关系并不能直接计算 \(h[i]\) ,但是考虑他们的关系会有奇效。

后缀 \(k+1\) 可以看成后缀 \(k\) 去掉首字符,后缀 \(i\) 可以看成后缀 \(i-1\) 去掉首字符,所以他们的最长公共前缀是 \(h[i-1]-1\)(当前 \(h[i-1]=0\) 的情况显然成立所以不予讨论)

\(k+1\) 并不是排名在 \(i\) 前面的后缀,设真正排名在 \(i\) 前面的后缀是 \(j\),那么 \(i,j\) 的最长公共前缀一定大于等于 \(i,k+1\) 的最长公共前缀,因为如果他不成立的话排名在 \(i\) 前面的后缀就是 \(k+1\) 了(还是反证法)


知道了这两个结论求 \(height\) 不是有手就行?因为 \(h[i]\) 每次最多减少 \(1\),所以暴力跑的话复杂度是 \(O(n)\) 的,这种思想在字符串问题中很常见了,比如:\(\tt kmp,exkmp,manacher\) 都是这种先继承再暴力的思想。

inv get_height()
{
    rint k=0;
    for (rint i=1;i<=n;++i) rk[sa[i]]=i;
    for (rint i=1;i<=n;++i)  
    {
        if (rk[i]==1) continue;//第一名height为0 
        if (k) --k;//h[i]>=h[i-1]+1;
        rint j=sa[rk[i]-1];
        while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;//h[i]=height[rk[i]];
    }
    putchar(10);for (rint i=1;i<=n;++i) putout(height[i]),putchar(' ');
}

后缀数组应用

做到了的话会慢慢补充的

\[\tt To\;\;be\;\;continued...... \]

posted @ 2020-12-13 16:51  C202044zxy  阅读(112)  评论(0编辑  收藏  举报