后缀数组!能让人理解的基数排序!

后缀数组

我感觉这个东西不难理解,难理解的是基数排序

首先一个长度为 \(n\) 的字符串可以有 \(n\) 个后缀,包括它本身。

那么我们定义 \(suf[i]\) 表示从 \(s[i]-s[n]\) 的后缀。

后缀数组是什么呢?

其实就是把这 \(n\) 个后缀拿出来按字典序排个序,然后记录下每个 \(suf[i]\) 排名的东东。

具体的:

  • \(sa\) 数组。\(sa[i]\) 代表了 排序后排名为 \(i\) 的后缀的起始位置,也就是说 \(suf[sa[i]]\) 这个后缀就是字典序第 \(i\) 小的。
  • \(rank\) 数组。 \(rank[i]\) 代表了 \(suf[i]\) 的排名,很好理解吧。比如说 \(rank[3] = 5\) 就代表 \(suf[3]\) 是字典序第 \(5\) 小的。

那么这两个数组显然就是一个点对 \((pos,rank)\) 的互相映射,然后有一个基本性质就是:

  • \(sa[rank[i]] = i\)。代表对应 \(suf[i]\) 的排名的后缀是 \(suf[i]\)
  • \(rank[sa[i]] = i\)。代表对应排名 \(i\) 的后缀的排名是 \(i\)

那么怎么求呢?

n^2logn Version

很简单啊,把后缀存下来暴力排序就好了,字符串比较复杂度 \(O(n)\),因此总体复杂度 \(O(n^2\log{n})\)

nlog^2n Version

这里用到了倍增的思想。
可以观察到一个字符串可以由两部分拼起来,也就是 \(l\)\(r\)
接下来考虑比较两个字符串的字典序,这两个字符串分别记为 \(l_1 + r_1\)\(l_2 + r_2\)

所以分类讨论:

  • \(l_1 == l_2\) 那么此时只需要比较 \(r_1,r_2\) 的字典序。
  • $l_1 < l_2 $ 或者 $ l_1 > l_2$ 那么字典序就显而易见了。

那么我们就有了倍增的思路:

  • 首先考虑长度为 \(1\) 的子串排序。
  • 用两个长度为 \(1\) 的拼成长度为 \(2\) 的并得到长度为 \(2\) 的排序。
  • 继续拼长度为 \(4\) 的。
  • ...
  • 排序完成。

注意因为我们的后缀长度不同,所以最后必定能排出来 \(n\) 个不同的排名,此时就可以结束了。

那么具体在实现上非常简洁易懂,就不多说了。

bool cmp(int x,int y){
	if(rank[x] != rank[y])return rank[x] < rank[y];
	int xx = x + k <= n ? rank[x+k] : -1;
	int yy = y + k <= n ? rank[y+k] : -1;
	return xx < yy;
}
void build_sa(char *s,int *sa){
	n = strlen(s+1);
	rep(i,1,n){
		sa[i] = i;
		rank[i] = s[i];//第一轮先用字符串当int来做
	}
	for(k=1;k<=n;k<<=1){
		sort(sa+1,sa+1+n,cmp);
		tmp[sa[1]] = 1;
		rep(i,2,n)tmp[sa[i]] = tmp[sa[i-1]] + (cmp(sa[i-1],sa[i]) ? 1 : 0);//统计排名,此时可能会有排名相同的哦。
		rep(i,1,n)rank[i] = tmp[i];
	}
}

nlogn Version

在此先膜膜 shr_ orz!!!
神仙教会了我基数排序的过程。

有一个东西叫做基数排序,非常神奇,复杂度是 \(O(n)\) 的,而且用在这里尤为合适,快的飞起,把复杂度优化掉了一只 \(\log\)

不难发现,我们倍增的每一轮实质上就是一个 \(sort\) 但是用了两个关键字来排序,第一个关键字是从左端点开始的排名,第二个关键字是从左端点加上倍增长度开始的排名,比如说当前倍增长度为 \(k\) ,那么第一关键字就是 \(rank[i]\) 第二关键字就是 \(rank[i+k]\) 这两部分。

那么分两类,一类是 \(i + k > n\) 的,这一类没有第二关键字,另一类自然就是有第二关键字的了。

对于第一类直接按第一关键字排就好了。

接下来我们考虑怎么用第二关键字来基数排序。

首先我们可以考虑从原先的 \(sa\) 数组里面直接对可以当作第二关键字的点进行统计,且由于 \(sa\) 的下标代表着排名,所以这个第二关键字是单调的。

那么我们考虑记录一个 \(id[i]\) 代表第二关键字排名为 \(i\) 的后缀的起点,对应到统计时的操作就是 \(sa[j] - k (sa[j] > k)\),因为当前这个 \(sa[i]\) 可以被当成第二关键字,那么就知道了他对应的第一关键字的起点,也就是他对应的后缀的起点。

有点绕,慢慢来。

这样的话我们就得到了一个根据第二关键字单调的 \(id\) 数组,里面存的都是后缀的起点,哦对了对于那些没有第二关键字的我们默认他的第二关键字是最小的,也就是都放在 \(id\) 的最前面。

如此一来,我们可以重新把每个后缀按照原先的 \(rank\) 插进去,也就是 \(cnt[rank[id[i]]]++\) ,然后做一次前缀和就变成了“排名”。然后倒序遍历,那么就有 \(sa[cnt[rank[id[i]]--]] = id[i]\) ,这样子就完成了对 \(sa\) 的排序。

别着急,是不是被绕晕了!因为我也在这里被绕晕了

考虑这样的事情,对于那些之前的没拼完的前缀 \(rank\) 相同的后缀,他们的 \(rank[id[i]]\) 肯定也相同对吧,根据我们最开始 \(O(n^2\log{n})\)算法的比较流程,\(rank[l]\) 相同的是不是要比较 \(rank[r]\) 也就是第一关键字相同的比较第二关键字啊。那么在 \(cnt[rank[id[i]]]\) 这个位置存了有几个这样的第一关键字相同的后缀,但同样的,\(id\) 的下标是根据第二关键字递增的,也就是说当我倒序遍历 \(id\) 的时候,先遍历到的后缀对于跟他第一关键字相同的,他的第二关键字是最大的,那么这时取出来就是他真正的排名了。

非常巧妙的做法,也非常绕,绕出来就好了qwq。

namespace SA{
const int N = 1e6 + 5;
int n,m,k,p;
int sa[N],rank[N],oldrk[N],id[N],px[N],tong[N];
char s[N];
bool cmp(int x,int y){//判断排名相同
    return oldrk[x] == oldrk[y] && oldrk[x + k] == oldrk[y + k];
}
void build(){
    n = strlen(s+1);
    m = 300;
    rep(i,1,n)tong[rank[i] = s[i]] ++;
    rep(i,1,m)tong[i] += tong[i-1];
    rep(i,1,n)sa[tong[rank[i]]--] = i;
    for(k = 1;k <= n;k <<= 1,m = p){
        p = 0;
        rep(i,n-k+1,n)id[++p] = i;//没有第二关键字的扔前面
        rep(i,1,n)if(sa[i] > k)id[++p] = sa[i] - k;//第二关键字递增的存储后缀开始的位置
        rep(i,1,m)tong[i] = 0;
        rep(i,1,n)tong[px[i] = rank[id[i]]] ++ ;//统计,拿px(排序)记一下 rank[id[i]] 内存连续访问,可卡常
        rep(i,1,m)tong[i] += tong[i-1];//前缀和,这样取出来代表真实排名
        rrep(i,n,1)sa[tong[px[i]]--] = id[i];//rrep是倒序遍历,如前面所说
        rep(i,1,n)oldrk[i] = rank[i];//这里要更新 rank ,所以存一下方便询问
        p = 0;
        rep(i,1,n)rank[sa[i]] = cmp(sa[i],sa[i-1]) ? p : ++p;
        if(p == n)return;
    }
}
}

height 数组

这个东西炒鸡重要的说。

  • \(height[i]\) 代表 \(LCP(sa[i],sa[i-1])\) 也就是排名为 \(i\) 和排名为 \(i-1\) 的最长公共前缀(Longest Common Prefix)

那么首先有个简单的结论就是:

  • \(LCP(suf[i],suf[j]) = \min(height[k]), k \in [rank[i]+1,rank[j]]\)

这个很好理解吧,字典序排好后肯定是越远 LCP 越小,所以取个 \(\min\)

然后在这里有一个非常强的结论,有了它就可以 \(O(n)\)\(height\) 数组了

  • \(height[rank[i]] \ge height[rank[i-1]] - 1\)

证明一下:

  • \(height[rank[i-1]] \le 1\) ,显然。
  • 这么考虑,此时有 \(height[rank[i-1]] > 1\) 也就是说和 \(i\) 相邻的这个 \(i-1\) 后缀与它前一名的后缀的 LCP 大于 \(1\) .
    • 也就是说我们现在有 \(suf[i]\),\(suf[i-1]\),\(suf[sa[rank[i-1]-1]]\)
      不妨令 \(sa[rank[i-1]-1] = k\) ,这个 \(suf[k]\) 就是排名在 \(suf[i-1]\) 前一名的后缀。
    • 回到开头那句话,\(suf[k]\)\(suf[i-1]\) 的第一位必然相同,所以对字典序没有影响。
    • 那么也就是说 \(suf[k+1]\)\(suf[i]\) 的相对排名就是 \(suf[k]\)\(suf[i]\) 的相对排名,因为这等价于 \(suf[k],suf[i-1]\) 扣掉了第一位。
    • 也就是说 \(suf[k + 1]\) 必然在 \(suf[i]\) 的前面,且它俩的 LCP 长度恰好是 \(height[rank[i-1]] - 1\) 因为扣掉了第一位。
    • 那也就是说 \(height[rank[i]]\) 至少是 \(height[rank[i-1]] - 1\)
    • 证毕。

有了这个强结论,就很好做了,模拟过程即可。

void get_height(){
    k = 0;
    rep(i,1,n){
        if(rnk[i] == 1)continue;//第一名默认height[1] = 0;
        if(k)--k;//height[rank[i-1]] - 1
        int j = sa[rnk[i] - 1];
        while(a[i+k] == a[j+k])++k;
        height[rnk[i]] = k;
    }
}
posted @ 2022-09-15 14:53  Xu_brezza  阅读(89)  评论(0编辑  收藏  举报