• 后缀数组小结


    前言

    一道模板题
    后缀数组(SA)是一个比较强大的处理字符串的算法,是有关字符串的比较基础是吗?算法,所以必须掌握
    实现主要有倍增和(DC3),而我太弱了只学了倍增

    目录

    知识点

    1.基数排序+倍增

    2.最长公共前缀Height

    一些要维护的东西

    (s):就是这个字符串,长度为(len)
    (rank[i]):表示(i到len)这个后缀在所有后缀中的排名
    (sa[i]):表示排名为(i)的后缀的首字符下标
    (height[i]):表示相邻两个排名的后缀的的最长公共前缀的长度

    基数排序+倍增构造SA

    首先基数排序:就是低位到高位一位一位桶排,详见baidu
    怎么排序?
    如果一位一位来,那不就成了暴力了
    所以我们要倍增排序
    具体来说,对于一个长度为(2^k)的字符串,我们可以把它看成是两个长度为(2^{k-1})的字符串组成的,那么就相当于它有两个关键字,我们就从长度为(1)开始,每次对这两个关键字排序不就行了
    一张古老的图

    看代码

    # include <bits/stdc++.h>
    # define IL inline
    # define RG register
    # define Fill(a, b) memset(a, b, sizeof(a))
    using namespace std;
    typedef long long ll;
    const int _(1e6 + 5);
    
    int len, sa[_], t[_], a[_], rk[_], y[_];
    char s[_];
    //t是桶;a和s一样;rk就是rank;y是辅助rk的数组,和sa性质相似;sa就是sa
    
    IL bool Cmp(RG int i, RG int j, RG int k){  return y[i] == y[j] && y[i + k] == y[j + k];  } //确定两个子串是否相同
    
    IL void Sort(){
    	RG int m = 80; //初始字符集大小
    	for(RG int i = 1; i <= len; ++i) ++t[rk[i] = a[i]];
    	for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1];
    	for(RG int i = len; i; --i) sa[t[rk[i]]--] = i;  //初始一个字符的排序
    	for(RG int k = 1; k <= len; k <<= 1){  //倍增
    		RG int l = 0;
    		for(RG int i = 0; i <= m; ++i) y[i] = 0;
    		//先按第二关键字排序,y记下编号
    		for(RG int i = len - k + 1; i <= len; ++i) y[++l] = i;  //越界的第二关键字没有,放前面
    		for(RG int i = 1; i <= len; ++i) if(sa[i] > k) y[++l] = sa[i] - k;  //剩下的按顺序排
    		//再按rk第一关键字排序
    		for(RG int i = 0; i <= m; ++i) t[i] = 0;
    		for(RG int i = 1; i <= len; ++i) ++t[rk[y[i]]];
    		for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1];
    		for(RG int i = len; i; --i) sa[t[rk[y[i]]]--] = y[i];
    		//用这次的rk更新下次的
    		swap(rk, y); rk[sa[1]] = l = 1;
    		for(RG int i = 2; i <= len; ++i) rk[sa[i]] = Cmp(sa[i], sa[i - 1], k) ? l : ++l;  //相同的rk一样
    		if(l >= len) break;  //此时肯定不用排了,大家都不同
    		m = l;  //记录下次排序的字符集大小
    	}
    }
    
    int main(RG int argc, RG char* argv[]){
    	scanf(" %s", s + 1); len = strlen(s + 1);
    	for(RG int i = 1; i <= len; ++i) a[i] = s[i] - '0';
    	Sort();
    	for(RG int i = 1; i <= len; ++i) printf("%d ", sa[i]);
    	return puts(""), 0;
    }
    
    

    有点难理解,多看几个博客就可以了

    最长公共前缀---Height

    有个(sa)(rank)并没有什么卵用,这个时候就有(Height)这个美妙的东西
    怎么求?
    暴力求(O(n^2))显然不行
    那么这个时候要利用(h)的美妙性质:(h[i]≥h[i-1]-1)
    证明:设后缀(suf[k]和suf[i-1])为两个相邻排名的后缀,它们的最长公共前缀就是(h[i-1])
    同时去掉第一个字符,那么就是(suf[k+1]和suf[i]),那它们两个的最长公共前缀显然就是(h[i-1]-1)
    所以(suf[i])和排在它前面的后缀的最长公共前缀至少是(h[i-1]-1)

    那么(h)就可以很快求出来了,那么(height)也就能很快求出来了

    for(RG int i = 1; i <= len; ++i){
    		h[i] = max(0, h[i - 1] - 1);
    		if(rk[i] == 1) continue;
    		while(a[i + h[i]] == a[sa[rk[i] - 1] + h[i]]) ++h[i];
    	}
    	for(RG int i = 1; i <= len; ++i) height[i] = h[sa[i]];
    

    刚刚学代码比较丑,而且可能有问题

    应用

    题去这里找
    题去这里找

    1.最长公共前缀

    题:给定一个字符串,询问某两个后缀的最长公共前缀。
    分析:
    就是区间(height)的最小值,(RMQ)问题

    重复子串:字符串A在字符串B中至少出现两次,则称A是B的重复子串。

    2.可重叠最长重复子串

    题:给定一个字符串,求最长重复子串,重复的两个子串可以重叠。
    分析:
    只需要求 height 数组里的最大值即可

    3.不可重叠最长重复子串

    题:给定一个字符串,求最长重复子串,重复的两个子串不能重叠。
    分析:
    考虑二分答案,每次二分一个答案(k),把(height)(>=k)分组

    最长公共前缀不小于 k 的两个后缀一定在同一组。对于每组后缀,只须判断每个后缀的 sa 值的最大值和最小值之差是否不小于k即可
    另外:利用 height 值对后缀进行分组的方法很常用

    4.可重叠的 k 次最长重复子串

    题:给定一个字符串,求至少出现 k 次的最长重复子串,这 k 个子串可以重叠
    分析:
    也是二分答案+分组,判断有没有一个组的后缀个数不小于 k

    5.不相同的子串的个数

    题:给定一个字符串,求不相同的子串的个数。
    分析:
    每个子串一定是某个后缀的前缀,也就是求所有后缀不同前缀的个数
    每来一个后缀(suf(i))就会有,(len-sa[i]+1)的新的前缀,又由于有(height)个重复的,那么就是(len-sa[i]+1-height)的贡献

    还有很多用法见下面的文献


    参考文献

    罗穗骞 --算法合集之《后缀数组——处理字符串的有力工具》

  • 相关阅读:
    Vue.js
    docker搭建pxc
    mkvirtualenv管理python版本
    gitlab
    centos7安装kvm及kvm管理
    centos7安装docker
    【从NIO到Netty】8.零拷贝案例2-网络传输
    【从NIO到Netty】7.零拷贝案例1-复制文件
    【从NIO到Netty】6.零拷贝介绍
    【从NIO到Netty】5.NIO EchoServer
  • 原文地址:https://www.cnblogs.com/cjoieryl/p/8334691.html
Copyright © 2020-2023  润新知