后缀数组(SA)倍增法总结

前言

听老师讲之前听说是字符串,比较虚(因为一直认为这东西很抽象)。。听后才发现只要认真听还是不难的。。

引入

读入一个长度为 n n n 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为 1 1 1 n n n

思路

法一:暴力 O ( n 2 l o g 2 ( n ) ) O(n^2log_2(n)) O(n2log2(n))
法二:在 s o r t sort sort 中用二分+Hash, O ( n l o g 2 ( n ) ) O(nlog^2(n)) O(nlog2(n))
法三:SA。

正题

定义 s a [ i ] sa[i] sa[i] 为排名为 i i i 的后缀的起始位置的下标。

试想一下,我们怎么判断两个字符串谁大谁小?我们可以从最小位比起,将字典序高的放在前面,若前面的这一位 < 后面的这一位,则调换两个串。

我们每次比第 k k k 个位,可以用这个规则不断更新 s a [ i ] sa[i] sa[i]:将第 k k k 位设为第一关键字,则现在已经比好了 k k k 位之后的。将所有的第 k k k 位放进一个桶里,用基数排序的思想将这些进行排序,若第 k k k 位相等则比较第二关键字。那没有第 k k k 位怎么办呢(也可以理解为缺少第二关键字),我们就看成第二关键字为空,时间为 O ( n 2 ) O(n^2) O(n2)

这时需要用 倍增 的思想优化。我们不用一位一位地设为第一关键字比,而是 k k k k k k 位地设为关键字,每次 k < < = 1 k<<=1 k<<=1。将第一关键字放桶时,利用上一次算出来的 k k k 位后缀的大小关系,排好 2 k 2k 2k 长度的后缀,就一直这样重复即可。

代码

注: H e i g h t [ i ] Height[i] Height[i] 为以 i i i 开头的后缀与 排名为 k − 1 k-1 k1 的后缀最大公共前缀( k k k i i i 的排名)。其中有一个性质: H e i g h t [ i ] > = H e i g h t [ i − 1 ] − 1 Height[i] >= Height[i-1]-1 Height[i]>=Height[i1]1。利用这个性质,就可以用 O ( n ) O(n) O(n) 的时间解决这个问题。

总时间复杂度: O ( n l o g 2 ( n ) ) O(nlog_2(n)) O(nlog2(n))

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <climits>
#include <iostream>
using namespace std;
const int MAXN = 3e5 + 5;
int n, c[MAXN], m = 200, hs[MAXN], sa[MAXN], tmp[MAXN], t[MAXN];
int Height[MAXN], mp[MAXN];
char s[MAXN];
//sa:当前排的顺序
//tmp:过渡序列
//hs:即 hash, 映射值
//c:统计个数 
void write(int x) {
	if(x < 0) x = -x, putchar('-');
    if(x <= 9) {
        putchar(x + '0');
        return;
    }
    write(x / 10); putchar(x % 10 + '0');
}
void Clac_SA() {
	for(int i = 1; i <= n; i ++) hs[i] = s[i], c[hs[i]] ++;
	for(int i = 1; i <= m; i ++) c[i] += c[i - 1];
	for(int i = n; i >= 1; i --) sa[c[hs[i]] --] = i; // 排好第一轮 
	for(int i = 1; i <= n; i <<= 1) {
		int num = 0;
		for(int j = n - i + 1; j <= n; j ++) tmp[++ num] = j;
		for(int j = 1; j <= n; j ++) if(sa[j] > i) tmp[++ num] = sa[j] - i;// 转换为新的序列 (以第二关键字排序)
		for(int j = 1; j <= m; j ++) c[j] = 0;
		for(int j = 1; j <= n; j ++) c[hs[j]] ++;
		for(int j = 2; j <= m; j ++) c[j] += c[j - 1];
		for(int j = n; j >= 1; j --) sa[c[hs[tmp[j]]] --] = tmp[j];//注意要倒序 
		for(int j = 1; j <= n; j ++) t[j] = hs[j];
		hs[sa[1]] = 1; num = 1;
		for(int j = 2; j <= n; j ++) hs[sa[j]] = hs[sa[j - 1]] + (!((t[sa[j]] == t[sa[j - 1]]) & (t[sa[j] + i] == t[sa[j - 1] + i])));
		num = hs[sa[n]];
		if(num == n) break; // 可加可不加
		m = num; 
	}
}
int Max(int x, int y) { return x > y ? x : y; }
void Clac_Height() {// 利用性质: H[i] >= H[i - 1] - 1 (注意 i 是下标,不是排名) 
	for(int i = 1; i <= n; i ++) mp[sa[i]] = i; // sa[i] ~ n 的排名是 i  
	//sa:排名是 i 的为 sa[i] ~ n 
	for(int i = 1; i <= n; i ++) {
		if(mp[i] == 1) {
			Height[i] = 0; continue;
		}
		Height[i] = Max(0, Height[i - 1] - 1);
		while(i + Height[i] <= n && sa[mp[i] - 1] + Height[i] <= n && s[i + Height[i]] == s[sa[mp[i] - 1] + Height[i]]) Height[i] ++;
	}
}
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	Clac_SA(); Clac_Height();
	for(int i = 1; i <= n; i ++) write(sa[i] - 1), putchar(' ');
	putchar('\n');
	for(int i = 1; i <= n; i ++) write(Height[sa[i]]), putchar(' ');
	return 0;
}

例题

Long Long Message
题意

DM星人的基因种碱基有26种,即26个小写字母。
现在已知两个DM星人的基因序列,求你输出这两个DM星人的最长公共基因。
公共基因表示从两个基因序列的某个位置开始有一段完全相同的连续基因序列。

思路

将一个串接在另一个串的后面,跑SA,求出Height。 a n s = M a x { H e i g h t [ i ] } ans = Max\{Height[i]\} ans=Max{Height[i]} i f if if i − 1 i-1 i1 i i i不在一个串中)。为什么这样是对的呢?这里有一个贪心:字典序排序后的字符串,他们的最优解就在相邻的两个串之间。

代码
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <climits>
#include <iostream>
using namespace std;
const int MAXN = 1e6 + 5;
int n, c[MAXN], m = 200, hs[MAXN], sa[MAXN], tmp[MAXN], t[MAXN];
int Height[MAXN], mp[MAXN];
char s[MAXN], s2[MAXN];
//sa:当前排的顺序
//tmp:过渡序列
//hs:即 hash, 映射值
//c:统计个数 
void write(int x) {
	if(x < 0) x = -x, putchar('-');
    if(x <= 9) {
        putchar(x + '0');
        return;
    }
    write(x / 10); putchar(x % 10 + '0');
}
void Clac_SA() {
	memset(c, 0, sizeof(c));
	for(int i = 1; i <= n; i ++) hs[i] = s[i], c[hs[i]] ++;
	for(int i = 1; i <= m; i ++) c[i] += c[i - 1];
	for(int i = n; i >= 1; i --) sa[c[hs[i]] --] = i; // 排好第一轮 
	for(int i = 1; i <= n; i <<= 1) {
		int num = 0;
		for(int j = n - i + 1; j <= n; j ++) tmp[++ num] = j;
		for(int j = 1; j <= n; j ++) if(sa[j] > i) tmp[++ num] = sa[j] - i;// 转换为新的序列 (以第二关键字排序)
		for(int j = 1; j <= m; j ++) c[j] = 0;
		for(int j = 1; j <= n; j ++) c[hs[j]] ++;
		for(int j = 2; j <= m; j ++) c[j] += c[j - 1];
		for(int j = n; j >= 1; j --) sa[c[hs[tmp[j]]] --] = tmp[j];//注意要倒序 
		for(int j = 1; j <= n; j ++) t[j] = hs[j];
		hs[sa[1]] = 1; num = 1;
		for(int j = 2; j <= n; j ++) hs[sa[j]] = hs[sa[j - 1]] + (!((t[sa[j]] == t[sa[j - 1]]) & (t[sa[j] + i] == t[sa[j - 1] + i])));
		num = hs[sa[n]];
		if(num == n) break; // 可加可不加
		m = num; 
	}
}
int Max(int x, int y) { return x > y ? x : y; }
void Clac_Height() {// 利用性质: H[i] >= H[i - 1] - 1 (注意 i 是下标,不是排名) 
	for(int i = 1; i <= n; i ++) mp[sa[i]] = i; // sa[i] ~ n 的排名是 i  
	//sa:排名是 i 的为 sa[i] ~ n 
	for(int i = 1; i <= n; i ++) {
		if(mp[i] == 1) {
			Height[i] = 0; continue;
		}
		Height[i] = Max(0, Height[i - 1] - 1);
		while(i + Height[i] <= n && sa[mp[i] - 1] + Height[i] <= n && s[i + Height[i]] == s[sa[mp[i] - 1] + Height[i]]) Height[i] ++;
	}
}
int main() {
	scanf("%s%s", s + 1, s2 + 1);
	n = strlen(s + 1);
	int len = strlen(s2 + 1);
	s[n + 1] = '#'; n ++;
	for(int i = 1; i <= len; i ++) s[i + n] = s2[i]; n += len;
	Clac_SA(); Clac_Height();
	int ans = 0;
	for(int i = 2; i <= n; i ++) {
		if((long long)(sa[i - 1] - (n - len)) * (sa[i] - (n - len)) < 0) ans = max(ans, Height[sa[i]]);// 贪心 (注意会爆int)
	}
	printf("%d\n", ans);
	return 0;
}

[JSOI2007]字符加密
题面

喜欢钻研问题的JS 同学,最近又迷上了对加密方法的思考。一天,他突然想出了一种他认为是终极的加密办法:把需要加密的信息排成一圈,显然,它们有很多种不同的读法。

例如‘JSOI07’,可以读作: JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0 把它们按照字符串的大小排序: 07JSOI 7JSOI0 I07JSO JSOI07 OI07JS SOI07J 读出最后一列字符:I0O7SJ,就是加密后的字符串(其实这个加密手段实在很容易破解,鉴于这是突然想出来的,那就^^)。 但是,如果想加密的字符串实在太长,你能写一个程序完成这个任务吗?

思路

版题。注意必须把这个字符串扩展一倍(因为是环),不然会被 zaba 这样的数据卡掉。

代码
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <climits>
#include <iostream>
using namespace std;
const int MAXN = 2e5 + 5;
int n, c[MAXN], m = 200, hs[MAXN], sa[MAXN], tmp[MAXN], t[MAXN];
int Height[MAXN], mp[MAXN];
char s[MAXN], s2[MAXN];
//sa:当前排的顺序
//tmp:过渡序列
//hs:即 hash, 映射值
//c:统计个数 
void write(int x) {
	if(x < 0) x = -x, putchar('-');
    if(x <= 9) {
        putchar(x + '0');
        return;
    }
    write(x / 10); putchar(x % 10 + '0');
}
void Clac_SA() {
	memset(c, 0, sizeof(c));
	for(int i = 1; i <= n; i ++) hs[i] = s[i], c[hs[i]] ++;
	for(int i = 1; i <= m; i ++) c[i] += c[i - 1];
	for(int i = n; i >= 1; i --) sa[c[hs[i]] --] = i; // 排好第一轮 
	for(int i = 1; i <= n; i <<= 1) {
		int num = 0;
		for(int j = n - i + 1; j <= n; j ++) tmp[++ num] = j;
		for(int j = 1; j <= n; j ++) if(sa[j] > i) tmp[++ num] = sa[j] - i;// 转换为新的序列 (以第二关键字排序)
		for(int j = 1; j <= m; j ++) c[j] = 0;
		for(int j = 1; j <= n; j ++) c[hs[j]] ++;
		for(int j = 2; j <= m; j ++) c[j] += c[j - 1];
		for(int j = n; j >= 1; j --) sa[c[hs[tmp[j]]] --] = tmp[j];//注意要倒序 
		for(int j = 1; j <= n; j ++) t[j] = hs[j];
		hs[sa[1]] = 1; num = 1;
		for(int j = 2; j <= n; j ++) hs[sa[j]] = hs[sa[j - 1]] + (!((t[sa[j]] == t[sa[j - 1]]) & (t[sa[j] + i] == t[sa[j - 1] + i])));
		num = hs[sa[n]];
		if(num == n) break; // 可加可不加
		m = num; 
	}
}
int Max(int x, int y) { return x > y ? x : y; }
void Clac_Height() {// 利用性质: H[i] >= H[i - 1] - 1 (注意 i 是下标,不是排名) 
	for(int i = 1; i <= n; i ++) mp[sa[i]] = i; // sa[i] ~ n 的排名是 i  
	//sa:排名是 i 的为 sa[i] ~ n 
	for(int i = 1; i <= n; i ++) {
		if(mp[i] == 1) {
			Height[i] = 0; continue;
		}
		Height[i] = Max(0, Height[i - 1] - 1);
		while(i + Height[i] <= n && sa[mp[i] - 1] + Height[i] <= n && s[i + Height[i]] == s[sa[mp[i] - 1] + Height[i]]) Height[i] ++;
	}
}
int main() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	for(int i = n + 1; i <= (n << 1); i ++) s[i] = s[i - n]; n <<= 1;
	Clac_SA(); Clac_Height();
	for(int i = 1; i <= n; i ++) {
		if(sa[i] <= n / 2) printf("%c", s[sa[i] + n / 2 - 1]);
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值