基础字符串 学习笔记
本文主要整理了:
- 字符串的基础概念与定义
- 基础 Border 理论
- KMP 算法
有些贺自 ix35 的博客。限于篇幅和本文偏应用的目的,会略去大多数性质的证明。
概念与定义
(只会记录一些我不太会的定义)
基础概念
- 字符集
\Sigma 生成的所有字符串的集合为\Sigma^* 。可以看到,若\Sigma\ne \varnothing ,则\Sigma^* 为无穷集。 - 定义空串为长度为
0 的串,记为\epsilon 。\epsilon 是任意群(\Sigma^*,+) 中唯一的零元。(+ 表示拼接操作) - 拼接操作可被记为
C=A+B ,也可写作C=AB 。 - 记一个字符串
S 的反串为S^R ,那么S 为回文串当且仅当S=S^R 。注意:\epsilon 也是回文串。
周期结构
-
定义
x 是S 的周期(Period),当且仅当x\le |S| 且\forall i\in[1,|S|-x],\ S_i=S_{i+x} 。若
x 整除|S| ,则称x 是S 的一个整周期。 -
定义
T 是S 的 Border(缩写为 Bd),当且仅当T 同时是S 的真前缀和真后缀。同时也称
|T| 是S 的 Border。
容易看出
Border 理论的基本性质
- 性质:
p 是S 的周期,当且仅当|S|-p 是S 的 Border.
十分显然,证明略。
不过这确实是一条非常有用的结论,借助它可以用 KMP 在
- 弱周期引理(Weak Periodicity Lemma):若
p,q 是S 的周期,且p+q\le |S| ,则\gcd(p,q) 也是S 的一个周期。
证明:观察有
\gcd(p,q) 的形式,猜想可以用辗转相除的思路证明。当
p=q 时显然成立,因此不妨设p>q ,我们证明p-q 也是S 的一个周期。而 $\forall x>q,\ S_x=S_{x-q}=S_{x+p-q}$,可知 $p-q$ 是 $S$ 的一个周期,再借用辗转相除法得 $\gcd(p,q)$ 是 $|S|$ 的周期。
实际上这个引理可以进一步加强:
- 周期引理(Periodicity Lemma):若
p,q 是S 的周期,且p+q-\gcd(p,q)\le |S| ,则\gcd(p,q) 也是S 的一个周期。
取
同时我们注意到
具体证明用到生成函数,就不管了。
- 长字符串的匹配定理:若
2|S|\geq|T| ,则S 在T 中的匹配位置必为等差序列。
证明:易见
S 在T 中的多个匹配必然有重叠的部分。考虑去掉
T 中没有被覆盖的字符,然后考虑反证。设有三个最近的匹配相距分别
p,q(p\ne q) ,我们发现这三个匹配覆盖的字符串一定有周期p,q 。于是便可以利用弱周期引理证明
\gcd(p,q) 也是它们的周期,于是便可以构造出它们中间的一组匹配,推翻假设。
- 短周期结构:字符串
S 有不超过\dfrac{|S|}{2} 的周期y ,当且仅当y 是其最短周期x 的倍数。
证明:充分性是显然的,下证必要性。
若
x>\dfrac{|S|}{2} 或y=x ,则结论已成立。下面假设x\le \dfrac{|S|}{2},y>x ,对任意S 的周期y\le \dfrac{|S|}{2} ,即有x+y\le|S| 。因此
y-x 也是|S| 的周期。若x\nmid y ,设y\equiv r\pmod{x} ,则可以找出一个周期r<x ,构成矛盾。故总有
x\mid y 成立。
- 长 Border 结构:字符串
S 的所有不小于\dfrac{S}{2} 的 Border 构成等差数列,且如果排序后延申这个数列,下一项就是|S| 。
证明:由短周期结构的结论,字符串
S 的所有不超过\dfrac{|S|}{2} 的周期成等差数列。由上面的性质可知长 Border 也成一个这样的等差数列。
简单的说,以
- 字符串的周期/Border 结构:字符串
S 的所有周期或 Border 形成了\mathcal O(\log n) 个值域不交的等差数列。
证明:有一个结论是,若
p 和q\ (p<q) 都是S 的 Border,则p 也是S[1\cdots q] 的 Border。
设
S 的最长 Border 长度为b_0 ,有b_0<|S| ,且:
- 长度在
[\dfrac{b_0}{2},b_0] 之间的 Border 都是S[1\cdots b_0] 的 Border,成一个等差数列;设最长的小于
\dfrac{b_0}{2} 的 Border 长度为b_1 ,有:
- 长度在
[\dfrac{b_1}{2},b_1] 之间的 Border 都是S[1\cdots b_1] 的 Border,成一个等差数列;此时我们已证明长度在
[\dfrac{|S|}{2^2},|S|] 之间的 Border 成至多2 个等差数列,同上简单归纳可知结论成立。
这里刻画了大于
- 例题:P4156 [WC2016]论战捆竹竿
这里是一个错解,没有考虑到短周期结构只对不大于
那么还怎么做呢?可以考虑同余最短路,跑出来再进行转移,具体看题解。
可以发现,求一个数列的线性组合一般使用同余最短路。
(好像写歪了)总复杂度
一直调不出!!!!!!
前缀函数与 KMP 算法
Knuth-Morris-Pratt 算法是一种能在
定义:
为引出 KMP 算法,给出如下结论:
- 性质:设
S[1\cdots i] 的所有 Border 长度所形成的集合为\mathrm{Bd}_i ,则有:\mathrm{Bd}_i=\begin{cases} \{0\} & \pi(i)=0\\ \mathrm{Bd}_{\pi(i)}\cup{\pi(i)} & \pi(i)>0 \end{cases}
证明:易证
\complement_{\mathrm{Bd}_{i}}\{0\}\subseteq\mathrm{Bd}_{\pi(i)} ,再由反证法可证明\mathrm{Bd}_{\pi(i)}\subseteq\mathrm{Bd}_i 。详细证明略去。
现在来研究如何求出
由于复杂度要求是
我们发现,由前缀函数的定义,
同理也可以得到
一般在代码中把
int j=0; Next[1]=0;
fo(i,2,n) {
while( j>0 && s[j+1]!=s[i] )
j=Next[j];
if(s[j+1]==s[i]) j++;
Next[i]=j;
}
然后处理如何在
下面我们来分析它的复杂度。
可以看出,第 3 行的 while 循环每执行一次至少会使
因此第 3 行的 while 循环至多会执行不超过
- 例题:P3375 【模板】KMP字符串匹配
直接把上面的东西写出来:
#include<bits/stdc++.h>
using namespace std;
#define fo(v,a,b) for(int v=a;v<=b;v++)
#define fr(v,a,b) for(int v=a;v>=b;v--)
#define il inline
const int N=1000010;
int n,m,Next[N];
char s[N],t[N];
int main()
{
scanf("%s",t+1), m=strlen(t+1);
scanf("%s",s+1), n=strlen(s+1);
int j=0; Next[1]=0;
fo(i,2,n) {
while( j>0 && s[j+1]!=s[i] )
j=Next[j];
if(s[j+1]==s[i]) j++;
Next[i]=j;
}
j=0;
fo(i,1,m) {
while( j>0 && s[j+1]!=t[i])
j=Next[j];
if(s[j+1]==t[i]) j++;
if(j==n) printf("%d\n",i-j+1);
}
fo(i,1,n) printf("%d ",Next[i]);
puts("");
return 0;
}
实际上,由于 NOI 大纲中不高于提高级的字符串算法只有 KMP,因此碰到联赛范围以内的字符串题不会做时,一般都是先跑一遍 KMP 求出
例题
感觉有点偏(
- CF1200E Compress Words
维护一个答案字符串,每次都对需要加入的字符串算出
具体做法可参考代码。Code
- P3193 [HNOI2008]GT考试
看到
我们考虑一个 DP,记
然后跑一遍 KMP 处理矩阵的系数,就可以愉快地矩阵快速幂了。
Code
失配树
给出失配树(Border Tree)的定义:
对于每个
那么,一个前缀
失配树有什么用呢?它可以将一个字符串问题转化为我们熟悉的树论问题。
- 性质:失配树中的每个点的编号都比其父结点大。
这是显然的。这样我们可以给出这棵树的一个拓扑序为
这样我们在失配树上 DP 时就不用写 DFS 了
例题
- 模板:P5829 【模板】失配树
给出一个字符串
S ,求S 的两个长度分别为n 和m 的前缀的最长公共 Border 长度。
不难发现,只要建出失配树,
但是,真的是
特判即可。Code
- 例题:P2375 [NOI2014] 动物园
这题大致是让我们对每个
暴力肯定要超时,貌似可以倍增,但算算复杂度发现它没有前途。
我们考虑从树上入手。
一种做法是 DFS 一遍树然后移指针,但是移指针很费时间,能卡到
我的做法是先正着算出每个点的深度,再从大到小枚举
Code
题解区有一种做法是算出深度后模拟一遍 KMP 过程:
fo(i,1,n) {
while( j>0 && s[j+1]!=s[i]))
j=Next[j];
if(s[j+1]==s[i]) j++;
while(2*j>i) j=fail[j];
res=res*(ll)(ans[j]+1)%P;
}
这个做法主要考虑到如果一个 Border 在某个位置不合法,那么由它扩展的都不会合法。
- P3426 [POI2005]SZA-Template
非常神仙的一道题。
我们考虑一个 DP,设
也就是说,
若
因此有
然后就不会了,看了这篇题解,然后就发现自己是 sb。
因为如果
因此它能生成的所有串一定是
考虑什么时候我们被迫取
这可以用一个桶维护,代码见下。
#include<bits/stdc++.h>
using namespace std;
#define fo(v,a,b) for(int v=a;v<=b;v++)
#define fr(v,a,b) for(int v=a;v>=b;v--)
#define il inline
const int N=500010;
int n; char s[N];
int Next[N],dp[N],pre[N];
int main()
{
scanf("%s",s+1), n=strlen(s+1);
int j=0; Next[1]=0;
fo(i,2,n) {
while( j>0 && s[j+1]!=s[i] )
j=Next[j];
if(s[j+1]==s[i]) j++;
Next[i]=j;
}
dp[1]=1, pre[1]=1;
fo(i,2,n) {
if( pre[dp[Next[i]]]+dp[Next[i]] >= i)
dp[i]=dp[Next[i]];
else dp[i]=i;
pre[dp[i]]=i;
}
printf("%d\n",dp[n]);
return 0;
}