基础字符串 学习笔记

djwj233

2021-10-03 20:40:21

Personal

本文主要整理了: - 字符串的基础概念与定义 - 基础 Border 理论 - KMP 算法 有些贺自 [ix35 的博客](https://www.luogu.com.cn/blog/ix-35/noi-yi-lun-fu-xi-ii-zi-fu-chuan)。限于篇幅和本文偏应用的目的,会略去大多数性质的证明。 ## 概念与定义 (只会记录一些我不太会的定义) #### 基础概念 - 字符集 $\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。 容易看出 $|S|$ 是任意字符串 $S$ 的**平凡周期**,$0$ 或 $\epsilon$ 是其**平凡 Border**。 ## Border 理论的基本性质 - **性质:$p$ 是 $S$ 的周期,当且仅当 $|S|-p$ 是 $S$ 的 Border.** 十分显然,证明略。 不过这确实是一条非常有用的结论,借助它可以用 KMP 在 $\mathcal O(n)$ 内求出字符串的所有周期。 - **弱周期引理(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\in[p+1,p+q]$,有 $S_x=S_{x-p}=S_{x-q}$。因此有 $\forall x\in [1,q],\ S_x=S_{x+p-q}$。 > > 而 $\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$ 的一个周期。** 取 $S=\texttt{abacaba}$,$p=6,q=4$。则 $\gcd(6,4)=2$ 不是 $S$ 的一个循环节。 同时我们注意到 $p+q-\gcd(p,q)=8=|S|+1$,因此周期引理的上界是紧的。 具体证明用到生成函数,就不管了。 - **长字符串的匹配定理:若 $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 也成一个这样的等差数列。 简单的说,以 $\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$ 个等差数列,同上简单归纳可知结论成立。 这里刻画了大于 $\dfrac{|S|}{2}$ 的周期 / 小于 $\dfrac{|S|}{2}$ 的 Border 的性质。 - 例题:[P4156 [WC2016]论战捆竹竿](https://www.luogu.com.cn/problem/P4156) [这里](https://www.luogu.com.cn/paste/t00cpyq7)是一个错解,没有考虑到**短周期结构只对不大于 $\dfrac{|S|}{2}$ 的周期适用**。 那么还怎么做呢?可以考虑**同余最短路**,跑出来再进行转移,具体看题解。 可以发现,**求一个数列的线性组合一般使用同余最短路**。 (好像写歪了)总复杂度 $\mathcal O(Tn\log n)$。 一直调不出!!!!!! ## 前缀函数与 KMP 算法 Knuth-Morris-Pratt 算法是一种能在 $\mathcal O(n)$ 内求出字符串 $S$ 的前缀函数 / 将另一个字符串 $T$ 和 $S$ 进行匹配的算法。 定义:$S$ 的**前缀函数(Prefix function)**$\pi(i)$ 表示 $S[1\cdots i]$ 的**最长 Border 长度**。 为引出 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$。详细证明略去。 现在来研究如何求出 $\pi(i)$。 由于复杂度要求是 $\mathcal O(n)$ 的,因此我们考虑从 $\pi(i-1)$ 导出 $\pi(i)$。 我们发现,由前缀函数的定义,$\pi(i)\le\pi(i-1)+1$,否则 $\pi(i)-1$ 一定是 $\pi(i-1)$ 的一个更优的解。 同理也可以得到 $\pi(i)-1\in \mathrm{Bd}_{i-1}$,那么我们从大到小枚举 $\mathrm{Bd_{i-1}}$ 中的数 $j$,直到 $S_{j+1}=S_i$(可以扩展一位)或 $j=0$(匹配失败)。 一般在代码中把 $\pi(i)$ 写作 $\texttt{next}$ 数组或 $\texttt{fail}$ 数组,代码如下:(注意要从 $i=2$ 开始枚举) ```c++ 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; } ``` 然后处理如何在 $S$ 中匹配 $T$,我们发现这个过程和上面是类似的。 下面我们来分析它的复杂度。 可以看出,第 3 行的 `while` 循环每执行一次至少会使 $j$ 的值减小 $1$,而 $j$ 总共被增加了不超过 $n$ 次。 因此第 3 行的 `while` 循环至多会执行不超过 $\mathcal O(n)$ 次,总复杂度是 $\mathcal O(n)$ 的。 - 例题:[P3375 【模板】KMP字符串匹配](https://www.luogu.com.cn/problem/P3375) 直接把上面的东西写出来: ```c++ #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 求出 $\pi(i)$ 然后找性质。 ## 例题 感觉有点偏( - [CF1200E Compress Words](https://www.luogu.com.cn/problem/CF1200E) 维护一个答案字符串,每次都对需要加入的字符串算出 $\pi(i)$,然后在答案字符串上进行正序的匹配。 具体做法可参考代码。[Code](https://www.luogu.com.cn/record/59231022) - [P3193 [HNOI2008]GT考试](https://www.luogu.com.cn/problem/P3193) 看到 $m\le20$ 而 $n\le 10^9$ 十分诡异,考虑矩乘。 我们考虑一个 DP,记 $dp_{i,j}$ 为当前到第 $i$ 位,还没有不合法子串但当前有 $j$ 位匹配上了。 然后跑一遍 KMP 处理矩阵的系数,就可以愉快地矩阵快速幂了。 [Code](https://www.luogu.com.cn/record/59320064) ## 失配树 给出**失配树(Border Tree)**的定义: 对于每个 $i$,我们连边 $i\to\pi(i)$,便可得到一棵**以 $0$ 为根的树**,不难发现这棵树满足小根堆性质。 那么,一个前缀 $S[1\cdots i]$ 的所有 Border 就是 $i$ 在树上的所有祖先。 失配树有什么用呢?它可以将一个字符串问题转化为我们熟悉的树论问题。 - **性质:失配树中的每个点的编号都比其父结点大。** 这是显然的。这样我们可以给出这棵树的一个拓扑序为 $\{n,n-1,\cdots,1\}$。 这样我们在失配树上 DP 时就不用写 DFS 了![](https://啧.tk/cy) ### 例题 - 模板:[P5829 【模板】失配树](https://www.luogu.com.cn/problem/P5829) > 给出一个字符串 $S$,求 $S$ 的两个长度分别为 $n$ 和 $m$ 的前缀的最长公共 Border 长度。 不难发现,只要建出失配树,$\operatorname{LCA}(n,m)$ 就是答案。 但是,真的是 $\operatorname{LCA}(n,m)$ 吗?我们发现,若 $n,m$ 中有一个是另一个的祖先,则答案为它的父亲。 特判即可。[Code](https://www.luogu.com.cn/problem/P5829) - 例题:[P2375 [NOI2014] 动物园](https://www.luogu.com.cn/problem/P2375) 这题大致是让我们对每个 $i$ 求出 $\mathrm{Bd}_i$ 中 $\le \dfrac{i}{2}$ 的元素个数。 暴力肯定要超时,貌似可以倍增,但算算复杂度发现它没有前途。 我们考虑从树上入手。 一种做法是 DFS 一遍树然后移指针,但是移指针很费时间,能卡到 $\mathcal \Theta(n^2)$,参见 ZCETHAN 被 hack 的[记录](https://uoj.ac/submission/513633)。 我的做法是先正着算出每个点的深度,再从大到小枚举 $i$,然后像并查集那样合并结点。由于每个点只会被合并一次,所以复杂度是 $\mathcal O(n)$ 的。 [Code](https://www.luogu.com.cn/record/59279497) 题解区有一种做法是算出深度后模拟一遍 KMP 过程: ```c++ 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](https://www.luogu.com.cn/problem/P3426) 非常神仙的一道题。 我们考虑一个 DP,设 $dp_i$ 表示要印出 $S[1\cdots i]$ 至少需要多长的印章,不难发现 $dp_1=1$,且: $$ \forall i\in [1,|S|],\ dp_i\in \mathrm{Bd}_i\cup \{i\} $$ 也就是说,$dp_i$ 在失配树中 $i$ 和 $\mathrm{anc}(i)$ 中取值,若 $\pi(i)=0$,则 $dp_i=i$。 若 $dp_i<i$,则 $dp_i\le \pi(i)$,$S[1\cdots dp_i]$ 能覆盖 $S[1\cdots\pi(i)]$。 因此有 $dp_i\geq dp_{\pi(i)}$,$dp$ 数组在失配树的一条链上呈一个单调性。 然后就不会了,看了[这篇题解](https://www.luogu.com.cn/blog/wtgrz/solution-p3426),然后就发现自己是 sb。 因为如果 $dp_i$ 取到 $(dp_{\pi(i)},\pi(i)]$ 之间的数,那么因为它的某个 Border 一定是 $dp_{\pi(i)}$,且由 $dp_{\pi(i)}$ 的性质,$dp_{\pi(i)}$ 一定能生成这个字符串。 因此它能生成的所有串一定是 $dp_{\pi(i)}$ 所生成的子集。换句话说,取答案为 $(dp_{\pi(i)},\pi(i)]$ 之间的数一定不比 $dp_{\pi(i)}$ 优。 考虑什么时候我们被迫取 $dp_i=i$,我们发现当且仅当 $S[1\cdots \pi(i)]$ 无法覆盖 $S[1\cdots i]$ 时才取 $dp_i=i$。 这可以用一个桶维护,代码见下。 ```c++ #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; } ```