字符串基础

KMP

考试实在忘记了的话,可以拿字符串哈希实现。

\(Next_i\) 代表的是以 \(i\) 为终点的后缀和以 \(Next_i\) 为终点的前缀相等。

注意 \(Next_1\) 的值为 \(0\),若为 \(1\) 则成环。

点击查看代码
void init(){
	Next[1]=0; int j=0;
	for(int i=2;i<=m;i++){//每次进入时,j=Next[i-1] 
		while(j&&p[i]!=p[j+1]) j=Next[j];
		if(p[i]==p[j+1]) j++;
		Next[i]=j;
	}
}
int main(){
	cin>>(t+1)>>(p+1);
	n=strlen(t+1),m=strlen(p+1);//t文本串 p模式串  
	init();
	for(int i=1,j=0;i<=n;i++){//每次进入时,j为上一次匹配到的位置 
		while(j&&t[i]!=p[j+1]) j=Next[j];
		if(t[i]==p[j+1]) j++;
		if(j==m) cout<<i-m+1<<endl;
	}
	for(int i=1;i<=m;i++) cout<<Next[i]<<" ";
	return 0;
}

经典应用P4391 [BOI2009] Radio Transmission 无线传输
\(i \bmod (i-next(i))=0\) 且$ i / (i-next(i))>1 $,则 \(S\) 有长度为 \(i-next(i)\) 的循环节。

border

  1. 字符串 \(s\) 所有不小于 $\lvert s \rvert $ 一半的 \(border\) 构成等差数列。
  2. 可以把字符串分成 $\log \lvert s \rvert $ 段,每一段的 \(border\) 构成等差数列。

失配树

\(KMP \to fail\)

Trie

P7537 [COCI2016-2017#4] Rima

先考虑暴力,就是两两算一下能不能押韵,然后建边,以每个点为起点跑一遍最长链。
我们可以在字典树上解决这个问题,我们建反串,仔细思考一下这个问题,其实就是在从一个节点开始在字典树上行走,每次可以到父亲或者同父的兄弟,问最多能到达多少节点,树形 dp 一下即可。

AC自动机

\(Tire\)\(\to\) \(Tire\) 图。
构建过程:先建立 Trie 树,然后把与根相连的第一个字符入队,同时 \(f\) 设为 \(0\),防止自环。然后开始扩展,如果当前 \(r\) 点没有字符 \(c\),那么 \(ch_{r,c}\gets ch_{f_r,c}\)。否则入队,然后 \(f_u\gets ch_{f_r,c}\),同时更新 \(last\) 数组,如果 \(f\) 位置有尾节点,那就是 \(f\) 了,否则就是 \(last_f\)
bfs 性质保证了长度小的串先被遍历到,所以 \(f\)\(last\) 数组一定可以保证连到。

CF710F String Set Queries

三种做法。

二进制分组

二进制分组,发现每一个字符串最多被重构 \(\log n\) 次,于是时间复杂度为 \(O(n\log n)\)。我们发现到答案的可减性,于是可以建立两个 AC 自动机分别负责添加和删除然后最后的答案就是添加组减去删除组。这里注意写法,直接创立两个个 AC 自动机的结构体,数组都在结构体里面开,每次调用不同结构体即可这样代码难度就减了很多。

实现细节,用每次当 \(sz_{top}=sz_{top-1}\) 的时候进行合并,合并的过程就是对应节点 \(val\) 相加。为了方便维护,我们在 getfail 的时候不能随便连 ch 了,必须新建立一个 sh 来连。

一种很典的做法

神奇的思路,我们设 \(\sum \lvert S_i \rvert=m\),于是\(\lvert S_i \rvert\) 的不同个数为 \(O(\sqrt n)\) 级别,每次枚举 \(T\) 中长度为 \(\lvert S_i \rvert\) 的字串,然后哈希判断即可。

根号分治

根号分治,较短串用字典树维护,较长串用 KMP 维护。

P2444 [POI2000] 病毒

直接在 AC 自动机上不碰到尾节点搜索,如果有环就可以无限绕着环走,这样是安全的。
注意有向图判环不能 \(0~1\) 标记,应该是 \(0~1~2\) 标记。

P2414 [NOI2011] 阿狸的打字机

我们发现其实就是对每个串建立一个 KMP,多个串的 KMP 自然就想到了 AC 自动机。
每次查询对于 \(y\) 链上的每个子串暴力跳 fail 的话太慢了,可以离线存下所有询问对于每个 \(y\) 链一起跳。
其实反过来考虑,这本质就是一个 \(x\) 的 fail 树的子树对于 \(y\) 求和问题。我们发现 \(y\) 是 AC 自动机上的一条链。于是可以顺着 AC 自动机的边走,进入累加,离开减去。这样所有时刻被累加的都是一条链上的贡献。
注意上述沿着 AC 自动机的边,指的是未创立 fail 之前的边,否则会成环。

bitset 完成字符串匹配

基本思路用 bitset 移位维护模式串的每个终止位置。
具体来说用 \(c_s\) 表示字符 \(s\) 在文本串中出现位置,其中 \(c\) 为一个 bitset,出现则该位为 \(1\)
对于模式串 \(t\) 的每一位 \(t_i\) 都将 \(c_{t_i}\) 左移 \(m-i\) 位和 \(ans\) 按位与。
CF914F Substrings in a String
法一:bitset 匹配。注意细节如果模式串长大于区间长度可能会减出负数,又因为 cout() 的类型是 unsigned int 所以需要我们取 int,然后和 \(0\) 比一下大小。
法二:这其实是一个 kmp 的过程。记住这种询问多个字符串的问题,询问的总长是一定的!!每次都重构一次 kmp 显然会被长度很小的查询复杂度卡掉。这启发我们进行根号分治,大于阀值 \(B\) 的查询我们直接进行 kmp 匹配,这种串不会超过 \(\frac{n}{B}\) 种。对于长度小于阀值 \(B\) 的查询,我们维护从每个下标开始的不超过 \(B\) 的每个哈希值,查询的时候暴力匹配即可。
法三:SAM 分块。
CF963D Frequency of String
法一:bitset 匹配
法二:考虑暴力每次暴力扫描时间复杂度为 \(O(\lvert S\rvert\sum\lvert m_i\rvert)\) ,也就是说用哈希匹配字符串,然后找到所有 \(m_i\) 出现位置,用滑动窗口扫一遍就行了。可以根号分治,对于长度大于 \(B\) 的串,直接执行上述操作。对于长度小于 \(B\) 的串,离线下来,从 \(s\) 的每一个位置开始
同理维护即可。这里需要用到一个结论保证复杂度: \(m\) 个不同的(长度之和为 \(n\) 的)串在同阶长度的中的文本串中的 endpos 集合大小为 \(O(n \sqrt{n})\)

Manacher

首先为了避免分类讨论,我们应该统一奇偶,在所有串每个空隙(包括首尾)之间插入一个无关字符,这样子回文中心就一定是某个字符了,而非空隙。

可以得到原串中最长回文子串的长度等于新串最长回文子串的半径减一,即 \(d=R-1\)。不是最长的未必满足这个性质。

我们维护当前右端点最远的对称中心 \(c\),设其半径为 \(r_c\),右端点为 \(R=c+r_c-1\)。设我们当前在考虑位置 \(i<R\),那么 \(i\) 关于 \(c\) 的对称点就是 \(2c-i\)

观察这张图,我们发现在 \(2c-i\) 地方的小区域对称正好也可以通过 \(c\) 点对称到 \(i\) 点来,但是注意如果 \(2c-i\) 的对称左端点越过了 \(L\) 就意味着多出来的那一部分无法通过 \(c\) 点传递我们也无法确认。于是 \(r_i \gets \min(r_{2c-i},R-i+1)\)。如果 \(r_i\) 取的是前者,那就意味着后面已经无法再继续匹配了,直接终止,如果取的是后者,我们暴力继续往外扩展即可。

可以发现最远右端点的移动的是 \(O(n)\) 的。所以时间复杂度线性。

这也可以证明一个字符串的本质不同回文子串个数不超过 \(n\) 个。

写代码时千万别忘记在开头加上分隔字符。

P4555 [国家集训队] 最长双回文串

我们只要求出 \(x_i\)\(y_i\) 表示原串中以 \(i\) 开头和结尾的最长回文子串的长度即可,然后 \(\max x_i+y_{i+1}\) 拼接即可。如果求 \(x_i\) 呢?首先原串的每个位置 \(i\),在加入额外字符之后会变成 \(2\times i\),于是我们只要在新串的偶数位置更新即可,自己模拟几种情况我们可以发现 \(x_{i/2}=\max\limits_{c+r_c-1\ge i}\{i-c+1\}\)。除以 \(2\) 是因为要映射回原串,为了满足 \(c+r_c-1\ge i\) 的要求,我们在每次 \(i+r_i-1 > R\) (不能取等) 的时候暴力用 \(i\) 作为中心更新 \((R,i+r_i-1]\) 即可。

UVA11475 Extend to Palindrome

有点构造题的感觉。找到最小的 \(l\),满足 \([l,n]\) 为回文,然后直接将 \(s[1,l-1]\) 翻转后放到末尾即可。

exKMP

\(z_i\) 表示字符串 \(s\)\(suf_i\) 的最长匹配长度,其中 \(z_1=n\)

对于 \(z\) 的求法与思想 Manacher 类似,称 \([i,i+z_i-1]\) 为匹配段,我们只要维护最靠右的匹配段即可,记为 \([l,r]\)。假如当前考虑到了 \(i\),对于 \(i>r\),我们暴力匹配,对于 \(i\le r\),我们可以通过之前求出的结论得到 \(s[i,r]=s[i-l+1,r-l+1]\),这不正好和 \(z_{i-l+1}\) 有关吗,于是 \(z_i \gets \min(r-i+1,z_{i-l+1})\)

和 Manacher 同理就是右端点的移动是线性的,所以复杂度为 \(O(n)\)

CF432D Prefixes and Suffixes

首先用 KMP 匹配可以求出所有前缀等于后缀的地方。

然后这其实是一个关于前缀计数的东西,我们要统计出某些前缀在串中出现了几次,我们发现较短的前缀被包含在较大的前缀中,如果位置 \(i\)\(Z\) 函数为 \(z_i\),那么 \([1,z_i]\) 的前缀都出现过,直接差分即可。

PKUSC2023 Border

面对这种类似单点修改全局查询的东西,且各个操作独立的题,一定要注意信息变化量很小,且只允许一个信息与原始的偏差

本题就是可以发现,可能产生贡献的位置必须满足之前 \(s[i,n]\)\(s[1,n-i+1]\) 之间最多有一个位置不同。可以想到用 Z 函数。对于我们对于 \(i\) 找到 \(i+z_i\) 这个位置代表是第一个不同的位置,我们只需要用字符串哈希判断后面的一段是否相等即可。然后同时判断一下加入的 \(t_{i+z_i}\) 能否符合要求。

posted @ 2024-02-27 12:01  Mirasycle  阅读(3)  评论(0编辑  收藏  举报