后缀字符串有两个主要算法: 其一___后缀数组 ; 其二___后缀自动机
后缀数组——复杂度O( n * log(n) )
明确定义:
前提条件:一个长度为 n 的字符串S
后缀:当前所说的后缀 ,它的意思是字符串S ,以每个下标开始i 到 字符串结束 (即 [Si , Si+1 , ... , Sn]),总共有n+1个字符串,因为包括 "" 空字符串
后缀数组:就是将上面的n+1个字符串 按字典序 排序后的字符串数组
("abracadabra" , 长度11)
内部算法:
因为每次字符串直接比较 会将 复杂度提高到 O( n^2 * log(N) ) , 采用倍增法 , 每次 我们只取 这些子串的 前 1 位 , 前 2 位
前 2 ^ k 位, 用了倍增法
倍增法:中间compare函数中应用了 一个 rank数组,rank[ ] 数组记录着 当前比较位数(2^k位) 的rank等级(即i + 2 ^ k 位置字符的 rank),如果不相同等级 , 直接排序 , 相同的时候 , 通过 一个 全局变量K 增加 K 位 【#重点: 增加 k位 相当于 比较 i 位置 后 第 i+k 位 的字符】来比较 两个 字符串 的大小。
比较完成后,要更新 rank[ ] 数组 , 用临时的 tmp[ ] 数组 更新rank[ ]数组。【更新方式:tmp[sa[i]] 表示 需要比较的 字符串长度 增加到 了 2 ^ k 位 后 , 最后一个 字符 的 rank , 那么 它是怎么得来的? 即 它原本 2^(k-1) 位 的 rank + ( compare(sa[i-1] , sa[i]) ? 1 : 0 )】
rank[j] 表示以第 j 位起始的字符串排第几
sa[i] 表示在后缀数组中 排第几的 字符串的起始位置
加强后缀数组应用的 高度数组 O(n)
高度数组(lcp[ ])是存储 后缀数组中 相邻的两个后缀 的最长相同前缀长度
可以通过简单地线段树维护 达到 求任意的 i , j , 在 rank[i] < rank[j] 时的 最长前缀
int n , k;
int rank[Maxn] , tmp[Maxn];
//比较(rank[i] , rank[i+k]) 和 (rank[j] , rank[j+k])
bool compare_sa(int i , int j){
if(rank[i] != rank[j]) return rank[i] < rank[j];
else{
int ri = i + k <= n ? rank[i+k] : -1;
int rj = j + k <= n ? rank[j+k] : -1;
return ri < rj;
}
}
//比较字符串S的后缀数组
/*
* 接口 sa数组 是 后缀数组的 排序后了的 原下标顺序
* 红书上有 也有另一个板子
*/
void construct_sa(string S , int* sa){
n = S.length();
//初始长度为1 , rank直接去字符的编码
for(int i = 0 ; i <= n ; i++){
sa[i] = i;
rank[i] = i < n ? S[i] : -1;
}
//利用对长度为k的排序的结果对长度为2k的排序
for(k = 1 ; k <= n ; k *= 2){
sort(sa , sa+n+1 , compare_sa);
//先在tmp中临时存储新计算的rank,再转存回rank中
tmp[sa[0]] = 0;
for(int i = 1 ; i <= n ; i++){
tmp[sa[i]] = tmp[sa[i-1]] + (compare_sa(sa[i-1] , sa[i]) ? 1 : 0);
}
for(int i = 0 ; i <= n ; i++){
rank[i] = tmp[i];
}
}
}
//基于后缀数组的字符串匹配
/*
* 能返回是否匹配成功 和 匹配的下标b
*/
bool contain(string S , int* sa , string T){
int a = 0 , b = S.length();
while(b - a > 1){
int c = (a+b) / 2;
//比较S从位置 sa[c] 开始长度为 |T| 的子串 与 字符T
if(S.compare(sa[c] , T.length() , T) < 0) a = c;
else b = c;
}
return S.compare(sa[b] , T.length() , T) == 0;
}
//高度数组 lcp
/*
* 高度数组是基于 后缀数组 来求 相邻的两个 后缀字符串中 最长公共前缀
* lcp[i] 是后缀数组第i 和 第i+1个 的最长前缀
* 假设有 rank[i] < rank[j] , 那么从位置i和位置j开始的后缀的最长公共前缀的长度是
* min(lcp[rank[i]] , lcp[rank[i]+1] , ... , lcp[rank[j]-1])
* 可以用线段树或rmq维护lcp数组
*/
void construct_lcp(string S , int* sa , int* lcp){
int n = S.length();
for(int i = 0 ; i <= n ; i++) rank[sa[i]] = i;
int h = 0;
lcp[0] = 0;
for(int i = 0 ; i < n ; i++){
//计算字符串从位置i开始的后缀及其在后缀数组中的前一个后缀的lcp
int j = sa[rank[i]-1];
//将h减去首字母的1长度,在保持前缀相同前提下不断增加
if(h>0) h--;
for(;j + h < n && i + h < n ; h++){
if(S[j+h] != S[i+h]) break;
}
lcp[rank[i]-1] = h;
}
}
后缀数组的应用:
sa[ ] 后缀数组 , lcp[ ] 高度数组
- 先考虑一个简易问题 —— 一个字符串中最少出现两次 的最长子串 >>>> 它必然是 后缀数组中 相邻的两个字符串的最长前缀
- 那么问题升级 —— 给一个 S串 T串 ,问两个串的最长公共子串(注意这是连续的串) >>>> 可以插入 一个 在两串中都未出现的 字符 eg: '|' , 连接两个串 ,然后求后缀数组,再求高度数组 , 这个时候高度数组中的 Max(lcp) 就是 ans ,同是也可以通过 sa[i] 是 第几位(它在S还是T串中,判断完后可以得到这个子串是什么)
- 再次升级 —— 最长回文子串 >>>> 可以将原串翻转 用一个未出现的 字符连接 两个串 , 求 -> sa -> lcp , 因为 最长回文子串分 奇串 和 偶串 , 所有求ans 的时候还要最一些 小变化 ,插入部分代码
string S;
int sa[Maxn] , lcp[maxn] , rank[Maxn];
int ans;
void solve(){
int n = S.length();
string T = S;
reverse(T.begin() , T.end());
S += '$' + T;
construct_sa(S,sa);
construct_lcp(S , sa , lcp);
for(int i = 0 ; i <= S.lenth() ; i++) rank[sa[i]] = i;
construct_rmq(lcp , S.length() + 1); //初始化rmp
int ans = 0;
//以第i个字符串对称的奇数长回文
for(int i = 0 ; i < n ; i++){
int j = n * 2 - i;
int l = query_rmq(min(rank[i] , rank[j]) , max(rank[i] , rank[j]));
ans = max(ans , 2 * l - 1);
}
//以第i-1 和 第 i 个字符对称的偶数长回文
for(int i = 1 ; i < n ; i++){
int j = n * 2 - i + 1;
int l = query_rmq(min(rank[i] , rank[j]) , max(rank[i] , rank[j]));
ans = max(ans , 2 * l);
}
}
字典树的数组版:
有的时候动态开点会MLE , 数组模拟不会爆
//HDU 1251
#include<stdio.h>
#include<iostream>
#include<string.h>
using namespace std;
const int maxn =2e6+5;
int tree[maxn][30];
int sum[maxn];
int tot;
void insert_(char *str)
{
int len = strlen(str);
int root = 0;
for(int i = 0 ; i < len ; i++) {
int id = str[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
sum[tree[root][id]]++;
root = tree[root][id];
}
}
int find_(char *str) {
int len = strlen(str);
int root = 0;
for(int i = 0 ; i < len ; i++) {
int id = str[i] - 'a';
if(!tree[root][id]) return 0;
root = tree[root][id];
}
return sum[root];
}
char ss[maxn];
int main()
{
tot=0;
while(gets(ss)) {
if(ss[0]=='\0') break;
insert_(ss);
}
while(scanf("%s",ss)!=EOF) {
printf("%d\n",find_(ss));
}
return 0;
}