后缀数组
2015-09-07 19:27
309 查看
问题 : 求一个字符串的不可重叠最长重复子串
二分答案 + hashhash 的思路是比较显然的 , 时间复杂度 O(N∗(logN)2)O(N*(logN)^2) , 然而这个算法有可能超时。
字符串的每个子串都是后缀的前缀 , 所以后缀蕴含着很大的信息量
这个时候后缀数组就可以派上用场了~
设 sa[i]sa[i] 为排名为 i 的后缀的起始位置 , rank[i]rank[i] 为以 ii 为起始位置的后缀的排名
rankrank 和 sasa 满足这样的关系 : rank[sa[i]]=irank[sa[i]] = i , sa[rank[i]]=isa[rank[i]] = i
求出 sa[]sa[] 之后, 可以直接求出 rank[]rank[]
首先我们对字符串 SS 的后缀排序
直接排序时间复杂度是 O(N2∗logN)O(N^2*logN), 显然不能满足需要
利用后缀的性质,我们可以利用倍增的思想排序
假设我们的得到了只考虑后缀最多前 kk 位时的排名
那么以 rank[i]rank[i] 为第一关键字, rank[i+k]rank[i + k] 为第二关键字排序
就可以得到考虑后缀最多前 2∗k2*k 位时的排名
(为了使得排序具有稳定的效率 , 一般采用基数排序)
代码实现有一定技巧
时间复杂度: O(N∗logN)O(N*logN)
但是只有 sa[]sa[] , rank[]rank[] 还不够 , 我们还想求出
height[i]height[i] : 排名为 ii 的后缀与排名为 i−1i - 1 的后缀的最长公共前缀(LCP)
可以证明 , height[rank[i]]≥height[rank[i−1]]−1height[rank[i]] \geq height[rank[i - 1]] - 1
时间复杂度: O(N)O(N)
证明
定义函数 LCP(x , y) 表示 sa[x ~ y] 的后缀的 LCP
LCP(x , y) = min{LCP(x + t, x + t + 1)} (0 <= t < y - x)
设 rank[k] = rank[i - 1] - 1
∴\therefore LCP(rank[k], rank[i - 1]) = height[rank[i - 1]]
∴\therefore LCP(rank[k + 1], rank[i]) >= LCP(rank[k], rank[i - 1]) - 1 = height[rank[i - 1]] - 1
此时如果 rank[k + 1] = rank[i] - 1, 结论显然成立
如果 rank[k + 1] <> rank[i] - 1 , 即 rank[k + 1] < rank[i] - 1
显然有
LCP(rank[i] - 1, rank[i])
>= LCP(rank[k + 1], rank[i])
>= height[rank[i - 1]] - 1
∴\therefore 结论成立
代码解释
nn : 字符串的长度
mm : 排名的最大值
c[i]c[i] : 关键字的值小于或等于 ii 的元素个数
x[i]x[i] : 以 ii 为起始位置的后缀的排名
y[i]y[i] : (按第二关键字排序) 排名为 ii 的后缀的起始位置
模板提交链接 : uoj #35
回到最开始的问题 , 如何求一个字符串的不可重叠最长重复子串?
先二分答案,把题目变成判定性问题 : 判断是否存在两个长度为 k 的子串, 它们是相同的 , 且不重叠
如果 height[i]≥kheight[i] \geq k, 我们认为 sa[i]sa[i] 和 sa[i−1]sa[i - 1] 在同一个集合内
(同一个集合中的点对(x,y)(x , y) 都满足 LCP(x,y)≥kLCP(x, y) \geq k)
然后判断是否存在点对 (x,y)(x , y) , sa[x]sa[x] 和 sa[y]sa[y] 在同一个集合内 且 |sa[x]−sa[y]|≥k|sa[x] - sa[y]| \geq k
我们只需要对 sa[]sa[] 求区间最值即可 , 时间复杂度为 O(N)O(N)
总时间复杂度: O(N∗logN)O(N*logN)
提交链接 : poj 1743
题意 : 将长度为 nn 的序列做差分 , 得到一个长度为 n−1n - 1 的新序列 , 求新序列的最长不重叠重复子串
记最长不重叠重复子串长度为 PP , 如果 P+1 P + 1 ≤\leq 55 , 直接输出 0 0 , 否则输出 P+1P + 1
二分答案 + hashhash 的思路是比较显然的 , 时间复杂度 O(N∗(logN)2)O(N*(logN)^2) , 然而这个算法有可能超时。
字符串的每个子串都是后缀的前缀 , 所以后缀蕴含着很大的信息量
这个时候后缀数组就可以派上用场了~
设 sa[i]sa[i] 为排名为 i 的后缀的起始位置 , rank[i]rank[i] 为以 ii 为起始位置的后缀的排名
rankrank 和 sasa 满足这样的关系 : rank[sa[i]]=irank[sa[i]] = i , sa[rank[i]]=isa[rank[i]] = i
求出 sa[]sa[] 之后, 可以直接求出 rank[]rank[]
首先我们对字符串 SS 的后缀排序
直接排序时间复杂度是 O(N2∗logN)O(N^2*logN), 显然不能满足需要
利用后缀的性质,我们可以利用倍增的思想排序
假设我们的得到了只考虑后缀最多前 kk 位时的排名
那么以 rank[i]rank[i] 为第一关键字, rank[i+k]rank[i + k] 为第二关键字排序
就可以得到考虑后缀最多前 2∗k2*k 位时的排名
(为了使得排序具有稳定的效率 , 一般采用基数排序)
代码实现有一定技巧
时间复杂度: O(N∗logN)O(N*logN)
但是只有 sa[]sa[] , rank[]rank[] 还不够 , 我们还想求出
height[i]height[i] : 排名为 ii 的后缀与排名为 i−1i - 1 的后缀的最长公共前缀(LCP)
可以证明 , height[rank[i]]≥height[rank[i−1]]−1height[rank[i]] \geq height[rank[i - 1]] - 1
时间复杂度: O(N)O(N)
证明
定义函数 LCP(x , y) 表示 sa[x ~ y] 的后缀的 LCP
LCP(x , y) = min{LCP(x + t, x + t + 1)} (0 <= t < y - x)
设 rank[k] = rank[i - 1] - 1
∴\therefore LCP(rank[k], rank[i - 1]) = height[rank[i - 1]]
∴\therefore LCP(rank[k + 1], rank[i]) >= LCP(rank[k], rank[i - 1]) - 1 = height[rank[i - 1]] - 1
此时如果 rank[k + 1] = rank[i] - 1, 结论显然成立
如果 rank[k + 1] <> rank[i] - 1 , 即 rank[k + 1] < rank[i] - 1
显然有
LCP(rank[i] - 1, rank[i])
>= LCP(rank[k + 1], rank[i])
>= height[rank[i - 1]] - 1
∴\therefore 结论成立
代码解释
nn : 字符串的长度
mm : 排名的最大值
c[i]c[i] : 关键字的值小于或等于 ii 的元素个数
x[i]x[i] : 以 ii 为起始位置的后缀的排名
y[i]y[i] : (按第二关键字排序) 排名为 ii 的后缀的起始位置
//Suffix Array const int maxn = 1e5 + 50; char s[maxn]; int sa[maxn], t0[maxn], t1[maxn], c[maxn], n; int rank[maxn], height[maxn]; void build_SA(int m) { int *x = t0, *y = t1; for(int i = 0; i < m; i++) c[i] = 0; for(int i = 0; i < n; i++) c[x[i] = s[i]]++; for(int i = 1; i < m; i++) c[i] += c[i - 1]; for(int i = n - 1; i >= 0; i--) sa[--c[x[i]]] = i; for(int k = 1; k <= n; k <<= 1) { int p = 0; for(int i = n - k; i < n; i++) y[p++] = i; for(int i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i] - k; for(int i = 0; i < m; i++) c[i] = 0; for(int i = 0; i < n; i++) c[x[y[i]]]++; for(int i = 1; i < m; i++) c[i] += c[i-1]; for(int i = n - 1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i]; std::swap(x, y); p = 1, x[sa[0]] = 0; for(int i = 1; i < n; i++) x[sa[i]] = y[sa[i-1]] == y[sa[i]] && sa[i-1] + k < n && sa[i] + k < n && y[sa[i-1]+k] == y[sa[i]+k] ? p-1 : p++; if(p == n) break; m = p; } } void build_Height() { int k = 0; for(int i = 0; i < n; i++) rank[sa[i]] = i; for(int i = 0; i < n; i++) { if(k != 0) k--; if(!rank[i]) continue; int j = sa[rank[i] - 1]; while(s[i + k] == s[j + k]) k++; height[rank[i]] = k; } }
模板提交链接 : uoj #35
回到最开始的问题 , 如何求一个字符串的不可重叠最长重复子串?
先二分答案,把题目变成判定性问题 : 判断是否存在两个长度为 k 的子串, 它们是相同的 , 且不重叠
如果 height[i]≥kheight[i] \geq k, 我们认为 sa[i]sa[i] 和 sa[i−1]sa[i - 1] 在同一个集合内
(同一个集合中的点对(x,y)(x , y) 都满足 LCP(x,y)≥kLCP(x, y) \geq k)
然后判断是否存在点对 (x,y)(x , y) , sa[x]sa[x] 和 sa[y]sa[y] 在同一个集合内 且 |sa[x]−sa[y]|≥k|sa[x] - sa[y]| \geq k
我们只需要对 sa[]sa[] 求区间最值即可 , 时间复杂度为 O(N)O(N)
总时间复杂度: O(N∗logN)O(N*logN)
提交链接 : poj 1743
题意 : 将长度为 nn 的序列做差分 , 得到一个长度为 n−1n - 1 的新序列 , 求新序列的最长不重叠重复子串
记最长不重叠重复子串长度为 PP , 如果 P+1 P + 1 ≤\leq 55 , 直接输出 0 0 , 否则输出 P+1P + 1
// poj 1743 #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <ctime> #include <iostream> #include <algorithm> template<class Num>void read(Num &x) { char c; int flag = 1; while((c = getchar()) < '0' || c > '9') if(c == '-') flag *= -1; x = c - '0'; while((c = getchar()) >= '0' && c <= '9') x *= 10, x += c - '0'; x *= flag; } template<class Num>void write(Num x) { if(!x) {putchar('0'); return;} static char s[20]; int sl = 0; if(x < 0) putchar('-'), x = -x; while(x) s[sl++] = x%10 + '0', x /= 10; while(sl) putchar(s[--sl]); } const int maxn = 2e4 + 20, base = 100; int n, s[maxn]; int sa[maxn], rank[maxn], t0[maxn], t1[maxn]; int height[maxn], c[maxn]; void build_sa(int m) { int *x = t0, *y = t1; for(int i = 1; i <= m; i++) c[i] = 0; for(int i = 1; i <= n; i++) c[x[i] = s[i]]++; for(int i = 1; i <= m; i++) c[i] += c[i - 1]; for(int i = n; i >= 1; i--) sa[c[x[i]]--] = i; for(int k = 1; k <= n; k <<= 1) { int p = 0; for(int i = 0; i < k; i++) y[++p] = n - i; for(int i = 1; i <= n; i++) if(sa[i] > k) y[++p] = sa[i] - k; for(int i = 1; i <= m; i++) c[i] = 0; for(int i = 1; i <= n; i++) c[x[y[i]]]++; for(int i = 1; i <= m; i++) c[i] += c[i - 1]; for(int i = n; i >= 1; i--) sa[c[x[y[i]]]--] = y[i]; std::swap(x, y); p = 1, x[sa[1]] = 1; for(int i = 2; i <= n; i++) x[sa[i]] = y[sa[i]] == y[sa[i - 1]] && sa[i] + k <= n && sa[i - 1] + k <= n && y[sa[i] + k] == y[sa[i - 1] + k] ? p : ++p; if(p == n) break; m = p; } } void build_height() { int k = 0; for(int i = 1; i <= n; i++) rank[sa[i]] = i; for(int i = 1; i <= n; i++) { if(k) k--; if(rank[i] == 1) continue; int j = sa[rank[i] - 1]; while(s[j + k] == s[i + k]) k++; height[rank[i]] = k; } } bool check(int k) { int e; for(int i = 1; i <= n; i = e) { int min = sa[i], max = sa[i]; e = i + 1; while(e <= n && height[e] >= k) { min = std::min(sa[e], min); max = std::max(sa[e], max); e++; } if(max - min >= k) return true; } return false; } int solve() { int l = 4, r = n; if(!check(l)) return -1; if(check(r)) return r; while(l + 1 != r) { int mid = (l + r) >> 1; if(check(mid)) l = mid; else r = mid; } return l; } void init() { for(int i = 1; i <= n; i++) read(s[i]); for(int i = 1; i < n; i++) s[i] = s[i + 1] - s[i] + base; s[n --] = 0; } int main() { #ifndef ONLINE_JUDGE freopen("1743.in","r",stdin); freopen("1743.out","w",stdout); #endif while(true) { read(n); if(!n) break; init(); build_sa(256); build_height(); write(solve() + 1), puts(""); } #ifndef ONLINE_JUDGE fclose(stdin); fclose(stdout); #endif return 0; }
相关文章推荐
- 关于c++显示调用析构函数的陷阱
- Snooker(2060)
- Redis中连接池使用的相关问题
- 1140 鸡蛋栈【模拟栈】
- 关于类和对象
- R语言高级程序设计 - 《Advanced R》中文版
- java万能转移编码
- hadoop集群默认配置和常用配置
- altera fpga 约束
- 自定义toast、toast重复显示、切换界面toast自动消失
- js时间转化为指定格式时间
- 15/9/7/SharePreference/内部存储/外部存储
- MFC 键盘响应
- Cassandra 技术选型的问题
- hdu5426 Rikka with Game
- linux的shell
- shell脚本编写使用
- CodeForces 46D Parking Lot(线段树区间合并)
- shell变量
- HTML5结合ajax实现文件上传以及进度显示