前言
听老师讲之前听说是字符串,比较虚(因为一直认为这东西很抽象)。。听后才发现只要认真听还是不难的。。
引入
读入一个长度为 nnn 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为 111 到 nnn。
思路
法一:暴力 O(n2log2(n))O(n^2log_2(n))O(n2log2(n))。
法二:在 sortsortsort 中用二分+Hash,O(nlog2(n))O(nlog^2(n))O(nlog2(n))。
法三:SA。
正题
定义 sa[i]sa[i]sa[i] 为排名为 iii 的后缀的起始位置的下标。
试想一下,我们怎么判断两个字符串谁大谁小?我们可以从最小位比起,将字典序高的放在前面,若前面的这一位 < 后面的这一位,则调换两个串。
我们每次比第 kkk 个位,可以用这个规则不断更新 sa[i]sa[i]sa[i]:将第 kkk 位设为第一关键字,则现在已经比好了 kkk 位之后的。将所有的第 kkk 位放进一个桶里,用基数排序的思想将这些进行排序,若第 kkk 位相等则比较第二关键字。那没有第 kkk 位怎么办呢(也可以理解为缺少第二关键字),我们就看成第二关键字为空,时间为 O(n2)O(n^2)O(n2)。
这时需要用 倍增 的思想优化。我们不用一位一位地设为第一关键字比,而是 kkk 位 kkk 位地设为关键字,每次 k<<=1k<<=1k<<=1。将第一关键字放桶时,利用上一次算出来的 kkk 位后缀的大小关系,排好 2k2k2k 长度的后缀,就一直这样重复即可。
代码
注: Height[i]Height[i]Height[i] 为以 iii 开头的后缀与 排名为 k−1k-1k−1 的后缀最大公共前缀(kkk 为 iii 的排名)。其中有一个性质:Height[i]>=Height[i−1]−1Height[i] >= Height[i-1]-1Height[i]>=Height[i−1]−1。利用这个性质,就可以用 O(n)O(n)O(n) 的时间解决这个问题。
总时间复杂度:O(nlog2(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。ans=Max{Height[i]}ans = Max\{Height[i]\}ans=Max{Height[i]}(ififif i−1i-1i−1 与 iii不在一个串中)。为什么这样是对的呢?这里有一个贪心:字典序排序后的字符串,他们的最优解就在相邻的两个串之间。
代码
#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;
}
本文详细介绍字符串后缀数组SA的构建方法及应用实例,包括暴力解法、二分+Hash优化解法以及SA算法,并通过具体例题展示如何利用SA解决实际问题。
倍增法总结&spm=1001.2101.3001.5002&articleId=111467501&d=1&t=3&u=f93bfdf7c1ae4856935a213d036084f6)
988

被折叠的 条评论
为什么被折叠?



