后缀数组讲解
参考:
https://www.cnblogs.com/shanchuan04/p/5324009.html
首先铭记几个数组的含义 在看代码的时候一定要记住到底是什么意思
把含义写在纸上
一定要明确第一次循环以后x[i]和c[i]所代表的意义和第一次循环不同
sa[i] 排名为i的后缀第一关键字的下标
x[i] 下标为i的字符是谁(经过第一次排序后 会变成下标为i的位置上的字符的排名)
y[i] 第k关键字排名为i的数的第一关键字的位置
这是原代码 lrj白书上的
char s[maxn]; int sa[maxn], t[maxn], t2[maxn], c[maxn], n; //sa[i]排名第i大的第一关键字的下标
//c[i]就是统计i字符有多少个的(第一次循环) 统计排名为i的后缀有多少个(从第二次循环开始)
void build_sa(int m) { int i, *x = t, *y = t2; //基数排序 for(i=0; i<m; i++) c[i] = 0; //每一个元素出现的次数 for(i=0; i<n; i++) c[x[i] = s[i]]++; //这个前缀和可以理解为当前元素i最多是第几名 for(i=1; i<m; i++) c[i] += c[i-1]; //从后向前遍历每个位置 保证了相同元素位置靠后的 排名也靠后 for(i=n-1; i>=0; i--) sa[--c[x[i]]] = i; //依次增加关键字 for(int k=1; k<=n; k<<=1) { int p = 0; //因为从n-k到n-1 的这些元素不存在第k关键字(i+k > n-1) 所以他们的第k关键字为0 所以排名在最前面 //再明确一下 y[i] 表示第k关键字排名为i的第一关键字的位置 for(i=n-k; i<n; i++) y[p++] = i; //因为第k关键字还是从这些元素中去找 所以第一关键字的排名顺序 即为第k关键字的顺序 依次录入即可 for(i=0; i<n; i++) if(sa[i] >= k) y[p++] = sa[i] - k; //初始化c for(i=0; i<m; i++) c[i] = 0; //x[y[i]]即为第k关键字对应的第一关键字 第二次循环以后 意思为起点下标为i的后缀的排名 for(i=0; i<n; i++) c[x[y[i]]]++; for(i=0; i<m; i++) c[i] += c[i-1]; //第一关键字相同的 第k关键字位置越靠后的 排名越靠后 for(i=n-1; i>=0; i--) sa[--c[x[y[i]]]] = y[i]; // 计算新的x数组 swap(x, y); p = 1; x[sa[0]] = 0; //对原x中的每一个位置的元素进行排名 //因为sa中所有元素的排名都不同 (即使第一关键字和第k关键字相同) 所以判断sa中连续排名的第一关键字和第k关键字是否相同 //如果第一关键字和第k关键字相同 则排名相同 不相同则向后累加 for(i=1; i<n; i++) x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k] ? p-1 : p++; if(p >= n) break; m = p; } }
基数排序倍增思想:
一首先考虑到比较方便我们把所有的字母都减去 a-1 这里我只考虑所有字母都是小写字母的方式
加入字符串是 aabaaaab
下面将相邻俩个数合并为一个整数
这样下面使用基数排序对这个合并后的整数进行排序 为什么使用基数排序 因为它的位数固定 也许你会问那
字母 ‘z’ 减去‘a’- 1 不是大于10了吗 那不是3位数了吗 不是这样的 把 z 减去‘a’- 1 =26 看做是一个数 而不是二十六
将相当于16进制 一样15不是看做两位数 而是用F来表示 当然你高兴 完全可以把26写作Z以后 Z就是26
下面我讲解一下 这个很重要 为什么要两两合并为一个数
首先求所有后缀数组最后组成为下图
那么每一个后缀之间都是有重复的 第1个后缀的前两个就是第0个后缀的第一到第三个字母
那么一次类推 也就是说我按下图分为两两一组
将上图的两两一组一个整数按照基数排序的结果为
解释一下 第一个11 排第一名 第二个12 排第二名
那么你有没有发现第0个后缀到第7个后缀的前两个字母的比较已经出来了 因为第一个11 就是第1个后缀的前两个字母 第二个12 就是第2个后缀的前两个字母
什么意思 看图
好了 现在我们已经比较所有后缀的前两个字母 下面我开始比较后面 那么我怎么比较前两个字母后面的字符串呢 因为刚才我已经把所有的两两字母的大小已经比较出来了 我现在可以利用下面的结果再比较 看图 其中合并后的 1121 就是第一个后缀的前四个字母 1211 就是第二个后缀的前四个字母
下面开始再次拼接 如图 最后这号拼成八位数 也就是正好字符串的长度 这时候可以使用基数排序来比较 但是假如字符串10000个呢 那么有10000个后缀 每个后缀的长度是10000 意味着最后拼接成的数也是有10000位 10000*10000我们需要开辟这么大数据这是不可行的 那么我们能不能将每次拼接的大数缩小呢?
先看图
首先后缀数组最终要获得的是后缀的排名 那么到底是1112 还是 11 是1221 还是24 无所谓
我只要把他们保持合适的大小 就比如说 小明考了100分 小红考了89分 小刚考了55分
那么我现在把小刚设为0分 小红设为1分 小明设为2分 那么对他们最后的排名有影响吗 没有
小明还是第一名 就是这个道理 这样我们可以最大程度减小存储的开销
所以我只要每次对合并的数据进行按照从小到大排个序 用序号替换它 然后再次按照之前的步骤再次合并 再次排序替换 (什么时候结束)当全部的字符串都参与了比较就停止了
那么现在对1121 1211 2111 1111 1112 1120 1200 2000进行排序 分成两组 前两个字母一组后两个字母一组 比如 1121这四个数字 11 与 21 两份来基数排序
等等 你有没有发现 我们上面的排序后的排名 跟第一关键字与第二关键字 有关系 也就是说
排名的大小就是第二关键字排名的 为什么 因为排序后的排名就是 第二关键字的排序结果
那么与第一关键字有什么关系 ? 有没有发现 就是把第一关键字的11去掉 然后再加一个00
举个生动的例子 现在有很多人在排队 高矮不等 ,一开始是乱序的 现在保安要求 按从矮到高排列
排好序之后 大家都有了自己的位置 现在保安走开了 队伍又回到一开始的状态 并且原来站在最开始的人(乱序是的站在最开始的人)走了 来了一个小矮人 肯定是最矮的 保安回来 要求再次排队 那么小矮人肯定站在最前面 下面保安喊道 上次排序排第一的人接上 如果走的那个人是第一 那么就继续后面 如果不是上次排名第一的人就站上来 然后保安继续叫 一直到上次排名最后的一个
上面这个故事就对应于下面的代码
//初始化c for(i=0; i<m; i++) c[i] = 0; //x[y[i]]即为第k关键字对应的第一关键字 for(i=0; i<n; i++) c[x[y[i]]]++; for(i=0; i<m; i++) c[i] += c[i-1]; //第一关键字相同的 第二关键字位置越靠后的 排名越靠后 for(i=n-1; i>=0; i--) sa[--c[x[y[i]]]] = y[i];
最后还要保证待排序的s数组数组的最后一个元素 要小于前面的元素
这是百度百科上的 这里的r数组 即为s数组
即如果有n个数且输入数据范围为1-xxx 那么手动设置s[n++] = 0;
如果输入数据范围为0-xxx 那么把每个输入数据s[i]++ 然后最后s[n++] = 0;
如果输入的是字符串 那先在main里用一个数组转化为int型 末尾再加0
最长公共前缀LCP
看白书证明把 很清晰
height[i]等级i与等级(i - 1)的最长公共前缀
int rank[maxn], height[maxn]; void get_hight() { int i, j, k = 0; for(i=0; i<n; i++) rank[sa[i]] = i; //rank[i]后缀i的等级 for(i=0; i<n; i++) //h[i] >= h[i-1] - 1; { if(k) k--; int j = sa[rank[i]-1]; while(s[i+k] == s[j+k]) k++; height[rank[i]] = k; } }
对于height[i]定义为sa[i]和 sa[i-1]的最长公共前缀
这个最长公共前缀的值肯定是最大的
证明:
设rank[j] < rank[k], 则不难证明后缀j和k的LCP的长度等于height[rank[j]+1], height[rank[j]+2],```, height[rank[k]]中的最小值
所以设后缀j和后缀k的最长公共前缀为h
则h <= height[i] i为( rank[j], rank[k] ]
即可用于解决 不可重叠最长重复子串
二分枚举长度k
然后遍历height
因为height[i] 即为最大 所以只要在height[i] >= k 时判断sa[i] 和 sa[i-1] 的差值是否大于k即可(即能保证不重叠)