字符串
周期与 Border 的结构
我们定义正整数 \(p\) 是串 \(S\) 的周期,当且仅当 \(p\le |S|\) 且 \(\forall i\in [1,|S|-p],S_i=S_{i+p}\)。
我们定义串 \(T\) 是串 \(S\) 的 border,当且仅当 \(T\) 是 \(S\) 的前缀且 \(T\) 是 \(S\) 的后缀,且 \(T\neq S\)。
周期与 border 的关系
正整数 \(p\) 是串 \(S\) 的周期,当且仅当 \(p\le |S|\) 且 \(S_{1\dots (|S|-p)}\) 是 \(S\) 的 border。
弱周期引理
若 \(p,q\) 是串 \(S\) 的周期,且 \(p+q\le |S|\),那么 \(\gcd(p,q)\) 也是 \(S\) 的周期。
不妨设 \(p<q\)。
对于任意 \(q<i\le |S|\),有 \(S_i=S_{i-q}=S_{i-q+p}\)。对于任意 \(q-p<i\le q\),有 \(S_i=S_{i+p}=S_{i-q+p}\)。所以 \(q-p\) 也是 \(S\) 的周期,而 \((q-p)+p\le |S|\),归纳可知 \(\gcd(p,q)\) 是 \(S\) 的周期。
周期引理
若 \(p,q\) 是串 \(S\) 的周期,且 \(p+q-\gcd(p,q)\le |S|\),那么 \(\gcd(p,q)\) 也是 \(S\) 的周期。
证明略。
短周期结构
串 \(S\) 的所有长度不超过 \(\frac{|S|}{2}\) 的周期,都是其最短周期的倍数。
这是弱周期引理的直接推论。
Border 的等差数列结构
串 \(S\) 的所有 border(或周期)的长度形成了 \(O(\log |S|)\) 个值域不交的等差数列。
首先有一个引理:若 \(S_{1\dots q}\) 是 \(S\) 的 border,\(S_{1\dots p}\) 是 \(S_{1\dots q}\) 的 border,则 \(S_{1\dots p}\) 也是 \(S\) 的 border。
考虑 \(S\) 的所有长度 \(\ge \frac{|S|}{2}\) 的 border,根据“短周期结构”,它们形成一个等差数列。设长度 \(<\frac{|S|}{2}\) 的 border 中,最长的一个的长度为 \(l\),那么长度在 \([\frac{l}{2},l]\) 中的所有 border 构成一个等差数列。以此类推,总共会形成 \(O(\log |S|)\) 个等差数列。
P3426 [POI2005] SZA-Template
给定一个字符串 \(S\),找到一个长度最小的字符串 \(T\) 使得 \(T\) 在 \(S\) 中的所有匹配区间覆盖了整个 \([1,|S|]\cap \mathbb{Z}\)。
首先,\(T\) 一定是 \(S\) 的 border。从短到长考虑 \(S\) 的所有 border \(W\),并维护 \(W\) 在 \(S\) 中的所有匹配位置 \(p_1<p_2<\dots<p_k\)。假如 \(\forall 1\le i<k,p_{i+1}-p_i\le |W|\),那么 \(W\) 就是答案。
\(S_{i\dots (i+|W|-1)}=W\) 当且仅当 \(\mathrm{border}_{i+|W|-1}\ge |W|\)。所以匹配位置就是 \(\mathrm{border}\ge |W|\) 的位置,用链表维护即可。时间复杂度 \(O(n)\)。
P4156 [WC2016] 论战捆竹竿
给定一个长度为 \(n\) 的字符串 \(S\) 和一个正整数 \(w\)(此处的 \(w\) 相较于题面中的减去了 \(n\)),设 \(S\) 的所有周期为 \(a_1,a_2,\dots,a_k\),求有多少个整数 \(x\in [1,w]\) 能被表示成 \(b_1a_1+b_2a_2+\dots+b_ka_k\) 的形式,其中 \(b_i\ge 0\)。
\(1\le n\le 5\times 10^5,1\le w\le 10^{18}\)。
首先,\(S\) 的所有周期可以被分为 \(O(\log n)\) 个值域不交的等差数列。考虑同余最短路,选定首项最小的等差数列的首项作为模数 \(m\),对于一个首项为 \(p\),公差为 \(d\),项数为 \(l\) 的等差数列,对于所有 \(x\in [0,m)\) 和 \(y\in [0,l)\),连边 \(x\xrightarrow{p+dy} ((x+p+dy)\bmod m)\)。最终,设 \(0\to x\) 的最短路是 \(dis_x\),答案就是 \(\sum_{0\le i<m} \max(\lfloor (w-dis_i)/m\rfloor+1,0)\)。
上面的做法时间复杂度显然过高。先考虑一个等差数列 \((p,d,l)\) 怎么做。选定 \(m=p\),连边就被简化成了:对于所有 \(x\in [0,m)\) 和 \(y\in [0,l)\),连边 \(x\xrightarrow{p+dy} ((x+dy)\bmod m)\)。可以发现这些 \(x\) 能被分成 \(\gcd(p,d)\) 个连通块,连通块内的连边结构很简单,可以用单调队列维护。
再考虑,假如当前我们得到了前 \(i\) 个等差数列的答案 \(dis_x\),现在要将模数 \(m\) 从 \(p\) 更改成 \(p'\)(\(p'>p\))。原来所有能被表示出来的数字 \(z\) 满足:
在满足这两个条件的情况下,可以令 \(dis'_{z\bmod p'}\gets \min(dis'_{z\bmod p'},z)\),而初值是 \(dis'_{dis_x \bmod p'}=dis_x\)。
我们仍然将图拆成若干个环,然后对于每个环分别更新一遍即可。总时间复杂度 \(O(n\log n)\)。代码。
CF1286E Fedya the Potter Strikes Back
给定长度为 \(n\) 的字符串 \(S\) 和非负整数数列 \(\{W_n\}\),对于 \(1\le l\le r\le n\),定义
\[f(l,r)=\begin{cases} \min_{l\le i\le r} \{W_i\} & S_{l\dots r}=S_{1\dots (r-l+1)}\\ 0 & \text{otherwise} \end{cases}. \]对于每个 \(1\le i\le n\),求 \(\sum_{1\le l\le r\le i} f(l,r)\) 的值,强制在线。
\(1\le n\le 6\times 10^5,0\le W_i<2^{30}\)。
假如我们已经知道了所有 \(1\le l\le r\le i-1\) 的 \(f(l,r)\) 之和,要计算出 \(r=i\) 时的 \(\sum_{l} f(l,r)\)。对于每个 \(j\),设 \(B_j\) 表示 \(S_{1\dots j}\) 的 border 的长度集合。考虑如何从 \(B_{i-1}\) 推出 \(B_i\),对于每个 \(x\in B_{i-1}\),若 \(S_{x+1}=S_i\),则 \(B_i\) 中会包含 \(x+1\)。还有长度为 \(1\) 的 border,若 \(S_1=S_i\),则 \(B_i\) 中会包含 \(1\)。
用 std::map
维护每种 \(f\) 对应的 border 个数。我们需要支持:
- 将所有 border 的权值与 \(W_i\) 取 \(\min\):暴力做即可,复杂度均摊 \(O(n\log n)\),因为合并 \(x\) 个权值不同的 border 集合的代价是 \(\Theta(x)\),而合并后一定不会再分裂开。
- 删除一个 border:在 \(W_{1\dots (i-1)}\) 对应的单调栈中二分,算出这个 border 的 \(f\),然后直接删除即可。
总时间复杂度 \(O(n\log n)\),常数略大。代码。
P5287 [HNOI2019] JOJO
这道题可以分为两部分:支持添加多个相同字符的 KMP,以及可撤销 KMP。
添加多个相同字符
将 (长度,字符) 二元组看作字符集,设原来的字符串是 \(S=((x_1,c_1),(x_2,c_2),\dots,(x_{n-1},c_{n-1}))\),当前要添加一个 \((x_n,c_n)\)。考虑一个能延伸到 \((x_n,c_n)\) 的 border,设它是 \(((x'_1,c'_1),(x'_2,c'_2),\dots,(x'_k,c'_k))\),那么它必须满足:
- \(c_1=c'_1\land x'_1\le x_1\);
- \(\forall i\in [2,k-1],c_i=c'_i\land x_i=x'_i\);
- \(c_k=c'_k\)。
也就是说,除开头和结尾以外的位置,不仅字符要匹配,个数也要匹配。因此,我们维护在新字符集上的 fail 指针,在往前跳的过程中更新答案即可。
可撤销 KMP
有很多种写法都是错的,这里只给一个正确写法(感谢 142857cs 和 jiangly 的帮助):
for (int i = 2, j = 0;i <= n;++i) {
for (int pre = 0;j && s[j + 1] != s[i];) {
if (j - kmp[j] == pre) j = j % pre + pre;
pre = j - kmp[j], j = kmp[j];
}
if (s[j + 1] == s[i]) ++j;
kmp[i] = j;
}
将这两个东西合起来即可通过本题,时间复杂度 \(O(n\log n)\)。代码。
后缀自动机
定义与性质
对于一个字符串 \(S\),设其字符集为 \(\Sigma\),则 \(S\) 的 SAM 就是一个最小的接受所有 \(S\) 的后缀的自动机。设 SAM 的源点 \(t_0=0\),那么从 \(t_0\) 开始的路径与 \(S\) 的子串一一对应。
对于 \(S\) 的非空子串 \(T\),设 \(\operatorname{endpos}(T)\) 为 \(T\) 在 \(S\) 中的所有出现位置的右端点的集合。若干个不同的 \(T\) 的 \(\operatorname{endpos}\) 可能是相同的,我们根据 \(\operatorname{endpos}\) 将 \(S\) 的所有子串分成若干个 \(\operatorname{endpos}\) 等价类,SAM 中的一个不是 \(t_0\) 的节点就代表了一个 \(\operatorname{endpos}\) 等价类,而 \(t_0\) 代表的是 \(\varnothing\)。
性质 \(1\):若 \(T,W(|W|<|T|)\) 是 \(S\) 的两个非空子串,那么 \(\operatorname{endpos}(T)=\operatorname{endpos}(W)\) 当且仅当 \(W\) 在 \(S\) 中的所有出现都是以 \(T\) 的后缀的形式出现的。
性质 \(2\):若 \(T,W(|W|<|T|)\) 是 \(S\) 的两个非空子串,那么它们 \(\operatorname{endpos}\) 的关系只有两种:
\[\begin{cases} \operatorname{endpos}(T)\subseteq \operatorname{endpos}(W) & W \text{is a suffix of } T\\ \operatorname{endpos}(T)\cap \operatorname{endpos}(W)=\varnothing & \text{otherwise} \end{cases} \]
性质 \(3\):对于一个 \(\operatorname{endpos}\) 等价类,将其中的字符串按照长度从小到大排列后,对于任意两个相邻的字符串 \(T,W\),有:
- \(T\) 是 \(W\) 的后缀;
- \(|T|=|W|-1\)。
对于 SAM 上的一个节点 \(i\),我们定义 \(\operatorname{len}(i)\) 为 \(i\) 代表的等价类中字符串的最长长度,而 \(\operatorname{minlen}(i)\) 为最短长度。定义后缀链接 \(\operatorname{link}(i)\) 为一个节点 \(j\) 使得 \(\operatorname{len}(j)\) 最大且 \(j\) 代表的等价类的 \(\operatorname{endpos}\) 是 \(i\) 代表的等价类的 \(\operatorname{endpos}\) 的超集,特别地 \(\operatorname{link}(t_0)=-1\)。据此,我们可以得出 \(\operatorname{len}(\operatorname{link}(u))=\operatorname{minlen}(u)-1\)。
性质 \(4\):对于所有 \(i>0\),将 \(i\to \operatorname{link}(i)\) 间连边,则会形成一棵以 \(t_0\) 为根的内向树,这棵树描述的包含、不交关系与 \(\operatorname{endpos}\) 描述的相同。
我们考虑 SAM 中转移边的意义。设 \(\delta(u,c)\) 为点 \(u\) 添加字符 \(c\) 得到的状态,\(\operatorname{substr}(u)\) 为点 \(u\) 表示的所有字符串的集合。若两个点 \(u,v\) 之间存在转移边 \(u\xrightarrow{c} v\),则 \(\forall T\in \operatorname{substr}(u),T+c\in \operatorname{substr}(v)\)。
性质 \(5\):对于节点 \(v\),设 \(\operatorname{mintrans}(v),\operatorname{maxtrans}(v)\) 分别是满足 \(\operatorname{len}(u)+1=\operatorname{minlen}(v),\operatorname{len}(u)+1=\operatorname{len}(v)\) 且存在转移边 \(u\to v\) 的 \(u\),易证这样的 \(u\) 存在且唯一。那么对于所有存在转移边 \(u\to v\) 的 \(u\),它们在 link 树上形成了一条 \(\operatorname{maxtrans}(v)\leadsto \operatorname{mintrans}(v)\) 的直上直下的路径。
构建
SAM 的构建是增量构造的过程,且是在线的。设我们已完成了 \(S_{1\dots (i-1)}\) 的构建,现在要加入 \(S_i\) 这个字符,\(S_{1\dots (i-1)}\) 对应的状态是 \(last\)。
新建一个节点 \(cur\),设 \(p=last\),我们不断地让 \(p\gets \operatorname{link}(p)\),并令 \(\delta(p,S_i)=cur\),直到 \(p=-1\) 或存在 \(\delta(p,S_i)\)。分类讨论:
- 若 \(p=-1\):令 \(\operatorname{link}(cur)\gets t_0\);
- 否则,设 \(\delta(p,S_i)=q\),若 \(\operatorname{len}(p)+1=\operatorname{len}(q)\),直接令 \(\operatorname{link}(cur)\gets q\) 即可。
- 否则,\(\operatorname{len}(p)+1<\operatorname{len}(q)\)。我们将 \(q\) 拆成两个状态 \(cl,q\),并令 \(\operatorname{len}(cl)=\operatorname{len}(p)+1\)。对于任意字符 \(c\),都令 \(\delta(cl,c)\gets \delta(q,c)\)。令 \(\operatorname{link}(cl)\gets \operatorname{link}(q),\operatorname{len}(q)\gets\operatorname{len}(cl)+1,\operatorname{link}(q)\gets cl,\operatorname{link}(cur)\gets cl\),并将所有位于 \(p\leadsto t_0\) 的路径上的点 \(p'\) 的 \(\delta(p',S_i)\gets cl\)。
不会复杂度证明。
struct SAM{
struct Node{int len,link,ed;array<int,26> nxt;}st[N*2];
int size,last,pres[N];
int New(){
st[size].len=st[size].link=st[size].ed=0;
st[size].nxt.fill(-1);
return size++;
}
SAM():size(0),last(0){New();st[0].link=-1;}
void Extend(char _c){
int cur=New(),c=_c-'a';
st[cur].len=st[last].len+1,st[cur].ed=1;
int p=last;pres[st[cur].len]=last=cur;
while(~p&&!~st[p].nxt[c]){
st[p].nxt[c]=cur,p=st[p].link;
}
if(!~p){st[cur].link=0;return;}
int q=st[p].nxt[c];
if(st[p].len+1==st[q].len) st[cur].link=q;
else{
int cl=size++;
st[cl]={st[p].len+1,st[q].link,0,st[q].nxt};
while(~p&&st[p].nxt[c]==q){
st[p].nxt[c]=cl,p=st[p].link;
}
st[q].link=st[cur].link=cl;
}
}
int anc[N*2][LogN];
void Build(){
For(i,1,size-1) anc[i][0]=st[i].link;
For(j,1,LogN-1) For(i,1,size-1) anc[i][j]=anc[anc[i][j-1]][j-1];
}
int Substr(int l,int r){
int len=r-l+1,u=pres[st[last].len-l+1];
Dec(j,LogN-1,0) if(st[anc[u][j]].len>=len) u=anc[u][j];
return u;
}
}sam;
回文结构
Manacher 算法
对于字符串 \(S\),设 \(p_i\) 为以 \(i\) 为回文中心的最长延伸长度,也就是说 \(S_{(i-p_i+1)\dots (i+p_i-1)}\) 是极长的回文串。Manacher 可以在 \(O(|S|)\) 时间内求出所有 \(p_i\)。
构造新的字符串 \(T\) 使得 \(T_{2i}=S_i,T_{2i-1}=\texttt{/},T_{2n+1}=\texttt{/}\),这样我们可以同时处理回文中心落在某个字符上、以及落在空隙中的情况。从小到大枚举 \(i\) 并维护当前向右延伸得最远的回文子串 \(S_{d\dots r}\)。考虑利用 \(d,r\) 加速 \(i\) 这个位置的回文串的枚举:若 \(i\le r\) 则令 \(p_i\gets \min(r-i+1,p_{2d-i})\)。随后,不断增大 \(p_i\) 直到 \(S_{i-p_i}\neq S_{i+p_i}\)。
时间复杂度:每次 \(p_i\) 增加 \(1\) 都会使 \(r\) 增加 \(1\),因此时间复杂度为 \(O(|S|)\)。