基础字符串总结

讨厌字符串...

关于字符串的一些定义:

  • \(|s|\) 表示字符串 \(s\) 的长度。
  • \(s_{l,r}\) 表示字符串 \(s\) 位置 \(l \sim r\) 上的字符所连接成的子串。
  • \(\mathrm{lcp(s,t)}\) 表示字符串 \(s\)\(t\)最长公共前缀\(\mathrm{lcs(s,t)}\) 则表示为最长公共后缀

1. 哈希(Hash)

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

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

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

2. kmp算法

3. 后缀数组(SA)

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

3.1 定义

  • \(su_i\) 表示字符串 \(s\) 中以 \(s_i\) 开头的后缀。
  • \(rk_i\) 表示 \(su_i\) 在所有后缀中的字典序排名。
  • \(sa_i\) 表示 \(s\) 所有后缀中排名为 \(i\) 的后缀的开始位置,其与 \(rk\) 互逆,即 \(sa_{rk_i} = rk_{sa_i} = i\)
  • \(ht_i\) 表示 \(su_{sa_{i-1}}\)\(su_{sa_i}\) 的最长公共前缀,即 \(|lcp(su_{sa{i-1}},su_{sa_i})|\),且 \(ht_1 = 0\)

3.2 后缀排序

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

假设我们已经求出来所有 \(2^w\) 级的子串,即所有 \(s_{i,i+2^w-1}\) 的排名 \(rk_i\),则我们可以通过拼接 \(rk_i\)\(rk_{i+2^w}\) 来求出 \(2 ^{w+1}\) 级的子串,所以我们可以通过构造 \((rk_i,rk_{i+2^w})\)二元组,经过排序得到所有 \(2^{w+1}\) 级子串的排名,直接快排是 \(O(n\log^2{n})\) 的,观察到排名的值域非常小,可以通过基数排序来优化到 \(O(n\log{n})\),不过这样常数还是极大。

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

(还有 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\) 主要依据的是一个性质:\(ht_{rk_i} \geq ht_{ri_{i-1}} - 1\)


(图源为 Alex_wei)

  • 证明,首先我们设 \(p\)\(sa_{rk_{i-1} - 1}\),即 \(i-1\) 后缀的前一名,则若 \(ht_{rk_{i-1}} > 1\),则必然有 \(s_{i} = s_{p+1}\),又因为 \(p\) 的排名小于 \(i-1\),则 \(p+1\) 的排名一定小于 \(i\),而排名在 \(p+1\)\(i\) 之间的 LCP 长度一定不小于 \(ht_{rk_{i-1}} - 1\) (因为字典序是递增的),即得 \(ht_{rk_{i}} \geq ht_{rk{i-1}} - 1\),得证。

所以我们可以 \(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

\(\mathrm{lcp(i,j)} = \mathrm{lcp(su_i,su_j)}\),若 \(i \not = j\) 则有:

\[\mathrm{lcp(i,j)} = \min\limits_{k=min(rk_i,rk_j)+1}^{max(rk_i,rk_j)}ht_k \]

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

3.4.2 本质不同子串个数

首先总数有 \(\dbinom {n+1} 2\)

我们考虑重复子串,对于从 \(i\) 开始的子串,重复的子串个数即最长公共前缀长度,即 \(ht_{rk_i}\)

得到本质不同子串个数为:\(\dbinom {n+1} 2 - \sum\limits_{i = 2}^n ht_i\)

P2408 不同子串个数
模板代码

3.4.3 结合单调栈

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

P4248 [AHOI2013] 差异
模板代码

3.5 例题

I P4051 [JSOI2007] 字符加密

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

II P3763 [TJOI2017] DNA

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

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

复杂度 \(O(n\log{n})\)

代码

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

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

代码

IV P4070 [SDOI2016] 生成魔咒

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

复杂度 \(O(n\log{n})\)

代码

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

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

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

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

代码

VI P2463 [SDOI2008] Sandy 的卡片

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

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

复杂度 \(O(n\log{n})\),若 DC3 + 单调队列 则为 \(O(n)\)

代码

VII P4094 [HEOI2016/TJOI2016] 字符串

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

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

: )

最后别忘记与 \(d-c+1\)\(min\),复杂度 \(O(n\log^2{n})\)
为啥暴力 SA 跑的比正解快 几十倍?

代码

VIII P2178 [NOI2015] 品酒大会

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

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

复杂度 \(\mathcal{O}(n\log{n})\),若 SA 用 DC3 则复杂度为 \(\mathcal{O}(n\alpha(n))\)

这个 \(\mathcal{O}\) 好好看 : )

代码

IX CF822E Liar

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

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

对于每一个 \(f_{i,j}\),令 \(k = \mathrm{lcp(i,l1 + 2 + f_{i,j})}\),则若不匹配 \(i\),则 \(f_{i+1,j}\gets\max(f_{i+1,j},f_{i,j})\);若匹配 \(i\),则 \(f_{i+k,j+1}\gets\max(f_{i+k,j+1},f_{i,j} + k)\)

复杂度 \(\mathcal{O}(n\log{n} + nx)\)

代码

X P1117 [NOI2016] 优秀的拆分

神仙题不过 \(\mathcal{O}(n^2)\) 95pts 谁写正解啊!,首先 \(AABB\) 可拆分,设 \(f_i/g_i\) 表示以 \(i\) 结尾/开头的 \(AA\) 造型方案数,则答案即为 \(\sum\limits_{i=1}^{n-1}f_i \times g_{i+1}\)

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

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

枚举 \(len\) 的复杂度是调和级数的,总复杂度 \(\mathcal{O}(Tn(\log{n} + \ln{n}))\)

代码

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

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

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

复杂度 \(\mathcal{O}(n\log{n})\)

代码

posted @ 2024-08-15 22:28  oXUo  阅读(77)  评论(0)    收藏  举报
网站统计