后缀系列——后缀数组

后缀字符串有两个主要算法: 其一___后缀数组 ; 其二___后缀自动机

后缀数组——复杂度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[ ] 高度数组

  1. 先考虑一个简易问题 —— 一个字符串中最少出现两次 的最长子串  >>>> 它必然是 后缀数组中 相邻的两个字符串的最长前缀
  2. 那么问题升级 —— 给一个 S串 T串 ,问两个串的最长公共子串(注意这是连续的串) >>>> 可以插入 一个 在两串中都未出现的 字符 eg: '|' , 连接两个串 ,然后求后缀数组,再求高度数组 , 这个时候高度数组中的 Max(lcp) 就是 ans ,同是也可以通过 sa[i] 是 第几位(它在S还是T串中,判断完后可以得到这个子串是什么)
  3. 再次升级 —— 最长回文子串 >>>> 可以将原串翻转 用一个未出现的 字符连接 两个串 , 求 -> 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;
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值