基础字符串总结

讨厌字符串...

关于字符串的一些定义:

  • |s| 表示字符串 s 的长度。
  • sl,r 表示字符串 s 位置 lr 上的字符所连接成的子串。
  • lcp(s,t) 表示字符串 st最长公共前缀lcs(s,t) 则表示为最长公共后缀

1. 哈希(Hash)

没什么可说的,将字符串表示为 p 进制数,模数为 ^#%@&$

可能有哈希冲突,通常可用双哈希(不过我懒)。

一些题也可以用哈希冲过去,是一个不错的工具。

2. kmp算法

3. 后缀数组(SA)

一个稍难但极好用的算法。

3.1 定义

  • sui 表示字符串 s 中以 si 开头的后缀。
  • rki 表示 sui 在所有后缀中的字典序排名。
  • sai 表示 s 所有后缀中排名为 i 的后缀的开始位置,其与 rk 互逆,即 sarki=rksai=i
  • hti 表示 susai1susai 的最长公共前缀,即 |lcp(susai1,susai)|,且 ht1=0

3.2 后缀排序

后缀排序算法可以求出后缀数组,用的是倍增法

假设我们已经求出来所有 2w 级的子串,即所有 si,i+2w1 的排名 rki,则我们可以通过拼接 rkirki+2w 来求出 2w+1 级的子串,所以我们可以通过构造 (rki,rki+2w)二元组,经过排序得到所有 2w+1 级子串的排名,直接快排是 O(nlog2n) 的,观察到排名的值域非常小,可以通过基数排序来优化到 O(nlogn),不过这样常数还是极大。

考虑一种优化,我们发现对于二元组的第二维,如果 i+2w>n 则我们可以直接排到最前面,然后我们把剩下的按照 sai 的顺序依次填入 sai2w 即可,这样我们就可以用桶排序直接排第一维,常数较小。

(还有 DC3 O(n) 算法,不过不想学 : ( )

[模版]后缀排序

int n,m,p;
char c[N]; 
int sa[N],rk[N],ork[N<<1],id[N],cnt[N];
bool cmp(int a,int b,int w){return ork[a] == ork[b] && ork[a + w] == ork[b + w];}
//若与相等的不加,否则加
int main(){
	scanf("%s",c+1);
	n = strlen(c+1),m = 128;
	for(int i = 1;i <= n;i++)cnt[rk[i] = c[i]]++;
	for(int i = 1;i <= m;i++)cnt[i] += cnt[i-1];
	for(int i = n;i >= 1;i--)sa[cnt[rk[i]]--] = i;

	for(int w = 1;;w <<= 1,m = p,p = 0){
		for(int i = n - w + 1;i <= n;i++)id[++p] = i;
		for(int i = 1;i <= n;i++)
			if(sa[i] > w)id[++p] = sa[i] - w;
		memset(cnt,0,sizeof cnt);
		for(int i = 1;i <= n;i++)cnt[rk[i]]++;
		for(int i = 1;i <= m;i++)cnt[i] += cnt[i-1];
		for(int i = n;i >= 1;i--)sa[cnt[rk[id[i]]]--] = id[i];
		p = 0;
		memcpy(ork,rk,sizeof rk);
		for(int i = 1;i <= n;i++)rk[sa[i]] = cmp(sa[i-1],sa[i],w) ? p : ++p;
		if(p == n)break;
	}
	for(int i = 1;i <= n;i++)printf("%d ",sa[i]);
	printf("\n");

	return 0;

}

3.3 height 数组

ht 主要依据的是一个性质:htrkihtrii11


(图源为 Alex_wei)

  • 证明,首先我们设 psarki11,即 i1 后缀的前一名,则若 htrki1>1,则必然有 si=sp+1,又因为 p 的排名小于 i1,则 p+1 的排名一定小于 i,而排名在 p+1i 之间的 LCP 长度一定不小于 htrki11 (因为字典序是递增的),即得 htrkihtrki11,得证。

所以我们可以 O(n) 求出 ht 数组。

for(int i = 1;i <= n;i++){
	if(p)p--;
	while(c[i + p] == c[sa[rk[i] - 1] + p])p++;
	ht[rk[i]] = p;
}

3.4 应用

3.4.1 求两个后缀的 LCP

lcp(i,j)=lcp(sui,suj),若 ij 则有:

lcp(i,j)=mink=min(rki,rkj)+1max(rki,rkj)htk

ij 的后缀最大公共前缀是在两排名之间 ht 数组最小值,可以 ST 表维护。

3.4.2 本质不同子串个数

首先总数有 (n+12)

我们考虑重复子串,对于从 i 开始的子串,重复的子串个数即最长公共前缀长度,即 htrki

得到本质不同子串个数为:(n+12)i=2nhti

P2408 不同子串个数
模板代码

3.4.3 结合单调栈

我们观察 ht 数组,可以把其看作 n 个矩形的并,而知周所众单调栈可以解决类似问题,如我们要求 1i<jnlcp(sui,suj),我们考虑依据排名依次加入,则可以看作加入一个宽为 1 高为 hti 的矩形,而我们求得答案即矩形面积和,即可单调栈维护,复杂度 O(n)

P4248 [AHOI2013] 差异
模板代码

3.5 例题

I P4051 [JSOI2007] 字符加密

复制一遍拼下,然后就是板子。

II P3763 [TJOI2017] DNA

首先我们把 S0 串与 S 拼接起来(常用技巧),中间补个分割符 #,跑个后缀数组。

然后我们考虑每个 iS 匹配,我们假设当前已经匹配长度为 p1 的串了,则我们可以找出 |lcp(i+p1,|S0|+p+1)|ST 表维护即可,我们循环三次,若匹配长度超过 |S| 则答案加 1

复杂度 O(nlogn)

代码

III P3181 [HAOI2016] 找相同字符
答案就是一些 lcp(i,j) 的和。

我们首先拼接一下,然后我们考虑后缀的贡献,只需要跑三遍 SA + 单调栈 即可(即 总的贡献 - 两字符串自己对自己的贡献)。

代码

IV P4070 [SDOI2016] 生成魔咒

首先可以想到本质不同子串,但本题会加字符,如果我们加在字符串尾部的话,整个字符串的后缀都会改变,这是不好的,所以我们考虑反转倒序加入,这样字符串仅仅只是多了一个后缀,其他后缀都不变,考虑如何求不同子串,即求,当前后缀与所有后缀的最长前缀,我们只需维护一个 set,考虑当前排名前后的 lcp 即可。

复杂度 O(nlogn)

代码

V P5341 [TJOI2019] 甲苯先生和大中锋的字符串

题目即求字符串中出现次数 恰好k 的子串中,长度出现次数最多的长度是多少。

我们考虑如何找恰好出现 k 次的子串,在每个后缀字符串中,我们发现出现 k,即在 ht 数组里有长度为 k 的连续排列(因为排序后相同的前缀一定在一个连续的区间),则可以枚举排名,假设当前为 i,则我们只需要找到 ii+k1最长前缀即可,ST 表即可,而又因为恰好,这说明不能有长度小于等于 lcp(i1,i)lcp(i+k1,i+k) 的子串(因为超过 k 次了),这样我们差分一下,找个最大值即可。

复杂度 O(nlogn),注意多组数据,i+k 可能会越位,注意清空。

代码

VI P2463 [SDOI2008] Sandy 的卡片

首先有差分,然后转化为求 n 个串的最长公共子串

然后拼接到一起,考虑在 n 个串中选出 n 个起点,则答案即为 n 个起点最小与最大 rk 之间 ht 最小值,即 max1l<rnmink=l+1rhtk,条件就是 [l,r] 的排名区间内必须包含 n 个子串的至少一个点,可以用桶简单处理,外层可以双指针处理,内层可以 ST 表 也可以 单调队列

复杂度 O(nlogn),若 DC3 + 单调队列 则为 O(n)

代码

VII P4094 [HEOI2016/TJOI2016] 字符串

首先我们知道,一个后缀 i 与一些后缀的 lcp 是与 rki 最近的后缀的 rk 值的 lcp,根据 应用I 易证,所以我们只需要找到区间 abrkc前后继,可用 可持久化线段树 解决,当然也可以用可持久化平衡树

但是这是错的,原因是因为有右界,导致我们需要对每个答案取 min,导致 rkc前后继 不一定最大(可能被取 min 了),我们需要二分 mid,单调性是显然的,则我们只需要找区间 [a,bmid+1] 中的结果即可,码量稍大(约 4K)。

: )

最后别忘记与 dc+1min,复杂度 O(nlog2n)
为啥暴力 SA 跑的比正解快 几十倍?

代码

VIII P2178 [NOI2015] 品酒大会

好题,首先我们考虑 r 相似 的性质,可以发现 r 相似,即在 ht 数组中一段连续区间 [l,r] 使得任意 i[l,r] 都有 htik,则该排名区间任意两个后缀都有 r 相似,这样的操作可以用并查集维护 ht 数组中大于等于 k 的区间,我们只需要从大到小枚举 k,合并即可。

然后考虑算答案,第一问是好求的,在并查集合并时加上 sizex×sizey 即可,第二问有乘积,负负得正,我们需要维护并查集内 最大值,次大值,最小值,次小值,则答案即为 max(mx1×mx2,mi1×mi2)

复杂度 O(nlogn),若 SA 用 DC3 则复杂度为 O(nα(n))

这个 O 好好看 : )

代码

IX CF822E Liar

好题,先拼接,题目中有一句话是 "按照原顺序合并",这启发我们可以枚举每个 i,贪心找 lcp,复杂度是 O(n2) 的。

观察数据范围看到 x 较小,可以考虑 DP,设 fi,j 表示在前 i1 个字符组成的字符串中,选最多 j不相交的子串所构成字符串与 T 串前缀的最长匹配长度。

对于每一个 fi,j,令 k=lcp(i,l1+2+fi,j),则若不匹配 i,则 fi+1,jmax(fi+1,j,fi,j);若匹配 i,则 fi+k,j+1max(fi+k,j+1,fi,j+k)

复杂度 O(nlogn+nx)

代码

X P1117 [NOI2016] 优秀的拆分

神仙题不过 O(n2) 95pts 谁写正解啊!,首先 AABB 可拆分,设 fi/gi 表示以 i 结尾/开头的 AA 造型方案数,则答案即为 i=1n1fi×gi+1

然后就牛了,我们钦定一个 len,我们每隔 len 标记一个点,则 AA 需要恰好经过两个点,我们假设 i,j (i+len=j),我们令 l=lcs(i,j)r=lcp(i+1,j+1),则若 l+rlen,则该区间存在 AA,考虑 f,可知结尾点可以是 [j+max(0,lenl),j+min(len1,r)] 区间内的点,差分即可,g 同理。

不懂的可以画图,下图绿色部分即可用起点区间,棕色部分即可以结尾区间
image

枚举 len 的复杂度是调和级数的,总复杂度 O(Tn(logn+lnn))

代码

XI P4081 [USACO17DEC] Standing Out from the Herd P
神秘题。

首先拼接,注意这样多个串拼接要用 不同拼接符号,所有字串总答案即 (n+12)

然后我们按排名枚举,对于每个 i,我们需要知道前面所有后缀的 lcp,即 hti,而且要知道后面 不是当前字符串lcp,所以我们可以找到每一段 在同一字符串 中的区间 [l,r],倒序依次减去 max(hti,mink=i+1r+1htk) 的贡献,复杂度是 O(n) 的。

复杂度 O(nlogn)

代码

posted @   oXUo  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
网站统计
点击右上角即可分享
微信分享提示