【闲话 No.6】 Lyndon 串与 runs 相关

约定

有些约定可能并没有被广泛认可,但是为了行文需要还是写在这里,请您理解。
在下文中,如无特殊说明,我们 \(n\) 表示问题规模大小,比如任何字符串的大小。所有数组下标从 \(1\) 开始
我们定义对于一个字符串 \(s\)\(s_i\) 表示 \(s\) 的第 \(i\) 个字符,\(s_{i,j}\) 表示这个串在 \([i,j]\) 区间内的子串,\(\operatorname{pre}_s(i)\) 表示这个串以 \(i\) 结尾的前缀,即 \(s_{1,i}\)\(\operatorname{suf}_s(i)\) 表示这个串以 \(i\) 为开头的后缀,即 \(s_{i,n}\)
如无特殊说明,文中所有对字符或字符串定义的“大于”“小于均基于大家所熟知的字典序。我们定义空字符是字典序最小的字符,且所有字符串的两边都有无数个空字符,所有串内都没有空字符。在进行字符串拼接等边界情况并不重要的操作时,我们默认忽略这些空字符。
我们定义两个字符串的乘法 \(st\) 表示将 \(s\)\(t\) 首尾相接得到的字符串,注意不同于数的乘法,字符串乘法不满足交换律。定义字符串的幂 \(s^k\) 表示将 \(k\)\(s\) 依次首尾相接得到的字符串。
我们定义关键点为两个字符串在比较时对结果贡献的位置,比如判定 \(ab < ac\),关键点为第二个位置。若关键点为空字符,则称这两个字符串的比较产生了一次关键溢出,可以发现,如果在一次比较之中长串较小,短串较大,则不可能发生关键溢出,同时也说明了 Lyndon 串与它的所有后缀比较都不会溢出。由于下文可能经常使用这个结论,我们称之为结论零。由于关键点产生导致接下来的位置无需比较,我们成这种现象为短路

Lyndon 串 与 Lyndon 分解

我们定义一个字符串为 Lyndon 串,当且仅当对于该字符串的所有后缀,该字符串本身是最小的一个。由于字符串的后缀长度互不相同,故不存在相等的后缀,因此 Lyndon 串本身在该串后缀集合中的最小性是严格的。接下来我们探究一些 Lyndon 串的性质。

Lyndon 串的结论一:Lyndon 串不含有 Border。
因为一个串的 Border 既是该串的严格后缀也是该串的严格前缀,前者必须大于原串,后者一定小于原串,二者矛盾。
Lyndon 串的结论二:对于 Lyndon 串 \(t\) 和任意串 \(s\),有 \(s<t \Leftrightarrow st<t\)
对于 \(s<t \Rightarrow st<t\),若在 \(s\)\(t\) 的比较中没有关键溢出,那么比较 \(st\)\(t\) 时必然在比较到 \(st\) 中的 \(t\) 前发生短路,因此无需考虑。根据结论零,只需考虑 \(s\) 考虑 \(|s|<|t|\) 且发生关键溢出的情况,此时如下图所示可得 \(st<t\)

图 1.1

而对于 \(s<t \Leftarrow st<t\),可以发现去掉 \(st\) 最后的 \(t\) 只会使它更小,显然 \(s<st<t\)
Lyndon 串的结论三:如果一个字符串 \(s\) 与一个字符 \(c\) 的积 \(sc\) 是一个 Lyndon 串的前缀,则对于任意一个字符 \(d>c\)\(sd\) 是 Lyndon 串。
考虑取出 \(s\) 的任意一个严格后缀 \(b\),如果 \(bc\)\(s\) 的前缀,那么 \(bd\) 必定在 \(d\) 处短路,使得 \(sd<bd\)。反之,假设 \(sc\) 所在的 Lyndon 串为 \(sct\),则有 \(sct<bct\) 且一定在 \(b\) 内短路,那么我们有 \(s<b\),也就有 \(sd<bd\)。总上,\(sd\) 一定为 Lyndon 串。
Lyndon 串的结论四\(s\) 是 Lyndon 串等价于它一定可以被表示成 \(s=ab\) 的形式,满足 \(a<b\)\(a,b\) 均为 Lyndon 串。
先来考虑为什么能有如上表示的串一定是 Lyndon 串。根据结论二 \(a<b \Rightarrow ab<b\),也就小于 \(b\) 的任意后缀。而 \(a\) 本身是 Lyndon 串,它与任意自己的后缀比较都不溢出,因此同时加上 \(b\) 对大小性没有影响,综上,得证。
对于 Lyndon 串 \(s\) 一定有如上表示的结论,考虑通过构造一种方案来证明。考虑取出 \(s\) 的严格最小后缀 \(\operatorname{suf}_s(i)\)(“严格”保证了不是整串),记作 \(b\)\(\operatorname{pre}_s(i-1)\) 记作 \(a\)。由于 \(b\) 的最小性,可得 \(b\) 是 Lyndon 串,而根据结论二 \(ab=s<b \Rightarrow a<b\),下面只需证明 \(a\) 是 Lyndon 串。考虑对于 \(a\) 的后缀 \(c\),由于已知 \(ab<cb\),只要 \(c\) 不是 \(a\) 的前缀,就会在 \(c\) 上短路,从而可以直接删去 \(b\) 得到 \(a<c\)。现在考虑证明 \(c\) 不是 \(a\) 的前缀,也即 \(a\) 没有 Border。假设 \(a\) 有长为 \(j\) 的 Border,那么回归 \(s\) 串,由于 \(b\) 的最小性可得 \(\operatorname{suf}_s(j+1)>\operatorname{suf}_s(i)\),这里的 \(i\) 就是上文定义 \(b\) 时使用的那个。考虑在两个字符串前同时接上一个相同的串不会影响他们的大小,那么由下图所示,可以证明 \(a\) 没有 Border。

图 1.2

综上,结论四得证。

对于一个任意字符串 \(s\),我们定义它的 Lyndon 分解 \(s'_1,s'_2,s'_3,s'_4,\cdots,s'_k\) 为满足 \(s'_1 \geq s'_2 \geq s'_3 \geq s'_4 \geq \cdots \geq s'_k\)\(s'_1,s'_2,s'_3,s'_4,\cdots,s'_k\) 均为 Lyndon 串。我们称 \(s'_1,s'_2\) 等为 \(s\)Lyndon 分解部分。考虑它的一些性质:

Lyndon 分解的存在性:考虑构造,初始这个字符串为一堆字符,每个字符均为 Lyndon 串,每次合并相邻的两个满足 \(a<b\) 的串,根据 Lyndon 串的性质四,最终剩下的串仍为一堆 Lyndon 串。按照这样直到无法进行,得到的即为 \(s\) 的 Lyndon 分解。
Lyndon 分解的唯一性:假设存在两组不同的 Lyndon 分解 \(s_1,s_2,\cdots,s_a\)\(t_1,t_2,\cdots,t_a\),其中 \(s_i\)\(t_i\) 首次出现不同且 \(|s_i|>|t_i|\),假设 \(s_i=t_i ab\cdots c\),其中 \(a,b\)\(t_i\) 后面的一些 Lyndon 分解部分,\(c\)\(t_i\) 后的某个 Lyndon 分解部分 \(d\) 的前缀,那么 \(s_i<c\),由于 \(d\)\(c\) 向后追加了可能为空的一部分,所以 \(c\leq d\)。而由 Lyndon 分解部分的大小关系可得 \(d\leq b \leq a \leq t_i\),且 \(s_i\) 的前缀 \(t_i\) 满足 \(t_i < s_i\),有 \(s_i < c < s_i\),矛盾,故不存在不同的 Lyndon 分解。

一般情况下,我们求 Lyndon 分解所用的算法是 Duval 算法。

Duval 算法核心结论:对于 \(s=t^k abc\),其中 \(t,a,c\) 为字符串且 \(t\) 为 Lyndon 串,\(a\)\(t\) 的前缀,\(b\) 是一个字母,且 \(|a|=i,0\leq i <|t|,b\neq t_{i+1}\)\(d=t^k ab\),那么有:

  1. 如果 \(b>t_{i+1}\),那么 \(d\) 是 Lyndon 串。考虑取出 \(d\) 的后缀 \(lb\) 使得 \(|l|>|a|\)\(l\) 的取出点不是 \(t^k a\) 中某个部分的开始点,也就是说 \(l\) 的一个前缀是 \(t\) 的严格后缀,那么这个 \(t\) 的后缀在 \(lb\)\(d\) 比较时会与 \(d\) 开头的 \(t\) 短路使得 \(lb>d\),而当 \(l=t^i a,0 \leq i <k\)\(lb\)\(d\) 比较时前面的部分会与 \(d\) 中的 \(t\) 们对齐导致 \(b\) 与某个 \(t_{i+1}\) 对齐发生短路,从而 \(lb>d\)。而由 Lyndon 串的结论三,\(at_{i+1}\) 是 Lyndon 串的前缀,所以 \(ab\) 是 Lyndon 串,且由上文 \(l=t^i a\) 的情况可得 \(d<ab\),所以 \(d\) 也小于 \(ab\) 的所有后缀。综上,得证。
  2. 如果 \(b<t_{i+1}\),那么可以确定 \(t\)\(s\) 的一个 Lyndon 分解部分。考虑 \(d\)\(ab<t\) 所以 \(d\) 不是 Lyndon 串,所有 \(t^k a\) 的长度大于 \(t\) 的前缀也因为 \(a<t\)(此处的 \(a\) 可以长度为 \(t\))使得 \(a<s\) 从而这样的串不是 Lyndon 串,而任意一个长度大于等于 \(d\) 的前缀都会在与 \(s\) 的比较中被 \(b\) 短路,也不是 Lyndon 串,所以由 Lyndon 分解的存在性可以固定 \(t\) 为一个 Lyndon 分解部分。

Duval 算法的做法就是维护这样一个 \(d\),初始钦定第一个字符为 \(t\),每次从 \(c\) 中取出一个字符作 \(b\),如果 \(b=t_{i+1}\) 就将 \(b\) 归入 \(a\)(当然也可能将 \(a\) 归入 \(t^k\)),否则如果 \(b>t_{i+1}\) 就钦定 \(d\) 为新的 \(t\)\(b<t_{i+1}\) 则把所有 \(t\) 取出当作 Lyndon 分解部分并继续对 \(abc\) 求解。可以发现由于最后空字符的存在,这个算法不会无止境地执行。为了方便下文称呼,我们将以上三种操作分别称为融合、扩展和终了。
下面给出一份伪代码以供参考。

\[\begin{array}{l} \textbf{Input. } \text{字符串 } s \text{ 表示要求 Lyndon 分解的字符串,值 } n \text{ 表示 } s \text{ 的长度。}\\ \textbf{Output. } \text{数集 } ans \text{ 包含 } s \text{ 的每个 Lyndon 分解部分的最后一个位置。}\\ \textbf{Method.}\\ \begin{array}{ll} 1 & lastpos \gets 0 \\ 2 & len \gets 1 \\ 3 & ans \gets \varnothing \\ 4 & cur \gets 0 \\ 5 & i \gets 2\\ 6 & \textbf{while } lastpos \neq n \\ 7 & \qquad \textbf{if } s_i > s_{lastpos+cur} \\ 8 & \qquad\qquad len \gets i-lastpos \\ 9 & \qquad\qquad cur \gets 0 \\ 10 & \qquad \textbf{else } \textbf{if } s_i < s_{lastpos+cur} \\ 11 & \qquad\qquad \textbf{while } i-lastpos > len\\ 12 & \qquad\qquad\qquad \text{push } lastpos+len \text{ into } ans \\ 13 & \qquad\qquad\qquad lastpos \gets lastpos+len \\ 14 & \qquad\qquad len \gets 1 \\ 15 & \qquad\qquad cur \gets 0 \\ 16 & \qquad\qquad i \gets lastpos+1 \\ 17 & \qquad i \gets i+1 \\ 18 & \qquad cur \gets cur+1 \\ 19 & \textbf{return } ans \\ \end{array} \end{array} \]

可以发现 \(i\) 每次左移的量一定小于等于上文 \(t\) 的长度,也就是 \(lastpos\) 至少左移的长度。可得 \(lastpos\) 左移至多带给 \(i\)\(O(n)\) 的右移势能,\(i\) 初始有 \(O(n)\) 的右移势能,所以 \(i\) 至多右移 \(O(n)\) 次,该算法的复杂度为 \(O(n)\)
附上一份可以通过P6114 【模板】Lyndon 分解的代码以供对拍。

Code
#include<bits/stdc++.h>
using namespace std;
char ch[5000100];
int n,ans;
int main(){
	scanf("%s",ch+1);
	n=strlen(ch+1);
	int lst=0,len=1;
	for(int i=2,cur=1;lst^n;++i,++cur){
		if(ch[i]>ch[lst+cur]){
			len=i-lst,cur=0;
		}else if(ch[i]<ch[lst+cur]){
			for(;i-lst>len;) lst+=len,ans^=lst;
			len=1,cur=0,i=lst+1;
		}
	}
	printf("%d\n",ans);
	return 0;
}

Runs 相关理论

我们定义字符串 \(s\) 的一个 run \(r=(l,r,x)\) 为一个三元组,满足 \(x\)\(s_{l,r}\) 的最小循环节长度,且长为 \(x\) 的循环节在 \(s_{l,r}\) 中至少出现两次并且不能延伸,即 \(2x\leq r-l+1\) 以及 \(s_{l+x-1} \neq s_{l-1},s_{r-x+1} \neq s_{r+1}\)。有时我们也会用这个 run 来代指 \(s_{l,r}\),由 \(x\) 的最小性可得 \(s_{l,r}\) 对应的 run 一旦存在便是唯一。类似字符串的幂,我们还定义 run 的对数 \(\log r=\frac{r-l+1}{x}\)
我们还定义一个 run 的 Lyndon Root 为这个 run 的长度为 \(x\) 的 Lyndon 子串。

Lyndon Root 的性质:每个 run 都存在至少一个 Lyndon Root 且同一 run 的每个 Lyndon Root 代表的子串相同,均为该 run 的长度为 \(x\) 的循环节的最小表示。
考虑如果我们寻找的 Lyndon Root 的开头不是某个最小表示的开头,那么由于 Lyndon Root 长度为 \(x\),所以这个 Lyndon Root 至少有某个最小表示的前缀作为它的后缀。由于最小表示小于该串,所以这个前缀更小于该串,这样的串不是 Lyndon 前缀。反之,如下图所示即可证明所有最小表示均为 Lyndon 串。

图 2.1

接下来为了证明剩下的一些结论,我们来进行重载字典序操作。我们定义 \(<_0,<_1\) 为两种截然不同的字典序,其中如果 \(a <_0 b\),那么 \(b <_1 a\)。注意空字符也加入字典序重载,所以在一种字典序下一个串的前缀小于它本身,另一种下则相反,这一点很重要,希望读者在阅读下文时不要惯性地认为一个串的前缀一定小于它本身。显然,两种字典序下的后缀数组互为对方完全颠倒的结果。为了方便查找与某种字典序完全相反的字典序,我们定义与 \(<_f\) 完全相反的字典序为 \(<_{\neg f}\)
我们还要定义最大 Lyndon 前缀函数 \(\operatorname{maxLy}_{s,f}(i)\) 表示最大的 \(j\) 使得 \(s_{i,j}\)\(<_f\) 意义下是 Lyndon 串,即 \(s_{i,j}<_f s_{k,j},i<k \leq j\)

最大 Lyndon 前缀函数结论:对于串 \(s\) 的一个位置 \(s_i\),有且仅有一个 \(f\) 使得 \(\operatorname{maxLy}_{s,f}(i)=i\)\(\operatorname{maxLy}_{s,\neg f}(i)>i\)
考虑找到 \(i\) 之后第一个满足 \(s_i\neq s_j\) 的位置 \(j\),假设 \(s_j <_f s_i\)。根据 Duval 算法核心结论,\(s_j\) 会使前面的一串 \(s_i\) 成为 \(<_f\) 意义下的 Lyndon 串,从而 \(\operatorname{maxLy}_{s,f}(i)=i\),而对于 \(\neg f\) 至少有 \(\operatorname{maxLy}_{s,\neg f}(i)\geq j\),得证。

接下来,我们要证明一些 runs 的性质。

runs 的结论一:对于 \(s\) 的一个 run \(r=(l,r,x)\),如果 \(s_{r+1}<_f s_{r+1-x}\),那么这个 run 的以 \(i\) 开头的 Lyndon Root 的最后一个位置是 \(\operatorname{maxLy}_{s,f}(i)\)
我们从 \(i\) 开头跑 Duval 算法,经过 Lyndon Root 前的部分一定会被划入一个 Lyndon 分解部分,而由于 run 的周期性,\(s_{i+1}\) 为第一个打破周期的位置,也就是终了的位置,所以根据 Duval 算法核心结论对终了部分的证明,\(\operatorname{maxLy}_{s,f}(i)\) 即为该 run Lyndon Root 的右端点。
runs 的结论二:对于 \(s\) 的任意两个 run \(r_1=(l,r,x),r_2=(l',r',x')\),有它们的所有 Lyndon Root 的起始点(若果 \(l,l'\) 是起始点,则除去它们)两两不同。这里显然如果 \(l,l'\) 是起始点,那么由于 run 的循环节出现至少两次所以肯定还有别的 Lyndon Root,故可以保证起始点集合非空。
假设有相同的起始点 \(i\),相对应的终止点分别为 \(j,j'\),显然当 \(j=j'\) 时根据 runs 的不可扩展性有 Lyndon Root \(s_{i,j}\) 可以扩展到的边界唯一,所以 \(r=r'\),矛盾。由最大 Lyndon 前缀函数结论可得这两个 Lyndon Root 的右端点分别为 \(\operatorname{maxLy}_{s,0}(i)\)\(\operatorname{maxLy}_{s,1}(i)\),其中必定有一个为 \(i\),它所属的 run 的循环节长度为 1。假设这个 run 为 \(r'\)。由于 \(s_{i,j}\) 是 Lyndon 串可得 \(s_i \neq s_j\),又由 \(i\) 不是 \(r'\) 的开端可得 \(s_i=s_{i-1}\) 且由 \(i\) 不是 \(r\) 的开端以及 runs 的周期性可得 \(s_{i-1}=s_{i-1+j-i+1}=s_j\),矛盾,故不成立。
The Runs Theorem:一个字符串的 runs 个数小于这个字符串的长度,且这些 runs 的对数之和 \(sum<3n-2\)
由 runs 的结论二可得每个 run 都要占有至少一个不同的“除开头外的 Lyndon Root 起始点”,所以 runs 一定少于 \(n\) 个(第一个位置不能作为上面的”非开头起始点“)。而由于每个 run 的“非开头起始点”个数 \(cnt \geq \lfloor \log-1 \rfloor \geq \log - 2\),所以 \(\sum(\log-2) < n\),由于至多 \(n-1\) 个 run 所以 \(\sum\log<3n-2\)

考虑如何求出一个字符串的 runs。考虑钦定一个 Lyndon Root 的左端点,找出它在两种字典序意义下的最大 Lyndon 前缀函数,假设、其中一段区间是 \([i,j]\),那么求出 \(\operatorname{lcs}(i-1,j),\operatorname{lcp}(i,j+1)\) 分别作为向左、向右扩展的长度,如果可以覆盖整个区间 \([i,j]\) 那就将它加入 runs 集合。关键在于如何快速求出一个点的最大 Lyndon 前缀函数。考虑从后往前扫描,维护后缀在 \(< _f\) 意义下的 Lyndon 分解 \(s_1,s_2,\cdots,s_k\),加入新字符 \(a\) 时如果 \(a <_f s_1\) 就进行合并,如果可以合并且 \(as_1 <_f s_2\) 就继续合并直到不能合并为止。由于 \(a\)\(s_1\) 均为 Lyndon 的,所以根据 Lyndon 串的结论二 \(a < s_1\) 等价与 \(as_1 < s_1\),根据结论零可得加上 \(s_1\) 后面的部分没有影响,从而上面的问题等价于求第一个小于以 \(a\) 开始的后缀的后缀,在做完后缀数组后单调栈查询 \(rank\) 即可。
这里给出一份简单的伪代码以供参考。

\[\begin{array}{l} \textbf{Input. }\text{字符串 } s \text{ 表示要求 runs 的字符串,值 } n \text{ 表示 } s \text{ 的长度。}\\ \textbf{Output. }\text{三元组集合 } ans \text{ 表示每个 run 的左端点、右端点以及最小循环节长度。}\\ \textbf{Method. }\\ \begin{array}{ll} 1 & sa \gets \text{ The suffix array of } s \\ 2 & ans \gets \varnothing \\ 3 & \textbf{for } < \text{ in } \{<_0,<_1\} \\ 4 & \qquad \textbf{for } i \text{ in } [1,n] \\ 5 & \qquad\qquad j \gets \text{ The first place that } rank_j < rank_i \\ 6 & \qquad\qquad j \gets j-1 \\ 7 & \qquad\qquad leftlen \gets \operatorname{lcs}(i-1,j) \\ 8 & \qquad\qquad rightlen \gets \operatorname{lcp}(i,j+1) \\ 9 & \qquad\qquad \textbf{if } leftlen+rightlen \geq j-i+1 \\ 10 & \qquad\qquad\qquad \text{push } (i-leftlen,j+rightlen,j-i+1) \text{ into } ans \\ 11 & \text{erase the same elements in } ans \text{ leaving only one of each kind} \\ 12 & \textbf{return } ans \\ \end{array} \end{array} \]

以上使用 SAIS 求后缀数组和 \(O(n) \sim O(1)\) RMQ(这里使用 st 表底层分块)即可做到 \(O(n)\)。以下给出一份可以通过P6656 【模板】Runs的代码。由于该题要求排序,所以复杂度为 \(O(n\log n)\),其他部分均为 \(O(n)\)

Code
#include<bits/stdc++.h>
using namespace std;
bool opbf[2000100];
int cnt[500100],pcnt[500100],lmsbf[1000100];
inline void sortsx(int n,int m,int lmscnt,bool* op,int* a,int* sa,int* lms){
	memset(cnt,0,(m<<2)+128),memset(sa,0,(n<<2)+128);
	for(int i=1;i<=n;++i) ++cnt[a[i]];
	for(int i=1;i<=m;++i) cnt[i]+=cnt[i-1];
	memcpy(pcnt,cnt,(m<<2)+128);
	for(int i=lmscnt;i;--i) sa[pcnt[a[lms[i]]]]=lms[i],--pcnt[a[lms[i]]];
	for(int i=1;i<=m;++i) pcnt[i]=cnt[i-1]+1;
	for(int i=1;i<=n;++i) if(sa[i]&&op[sa[i]-1]) sa[pcnt[a[sa[i]-1]]]=sa[i]-1,++pcnt[a[sa[i]-1]];
	memcpy(pcnt,cnt,(m<<2)+128);
	for(int i=n;i;--i) if(sa[i]>1&&(!op[sa[i]-1])) sa[pcnt[a[sa[i]-1]]]=sa[i]-1,--pcnt[a[sa[i]-1]];
}
void sais(int n,int m,bool* op,int* a,int* sa,int* rk,int* lms){
	op[n]=0; for(int i=n-1;i;--i) if(a[i]^a[i+1]) op[i]=a[i]>a[i+1]; else op[i]=op[i+1];
	int cnt=0;
	for(int i=2;i<=n;++i) if(op[i-1]&&(!op[i])) ++cnt,lms[cnt]=i,rk[i]=cnt; else rk[i]=0;
	sortsx(n,m,cnt,op,a,sa,lms);
	int tem=0,lst=0;
	for(int i=1;i<=n;++i){
		if(rk[sa[i]]){
			if((!tem)||((lms[rk[sa[i]]+1]-sa[i])^(lms[rk[lst]+1]-lst))){++tem; goto placea;}
			for(int j=sa[i],k=lst;j^lms[rk[sa[i]]+1];++j,++k) if(a[j]^a[k]){++tem; break;}
			placea: a[n+rk[sa[i]]]=tem,lst=sa[i];
		}
	}
	if(tem^cnt) sais(cnt,tem,op+n,a+n,sa,rk,lms+cnt); else for(int i=1;i<=cnt;++i) sa[a[n+i]]=i;
	for(int i=1;i<=cnt;++i) a[n+i]=lms[sa[i]]; sortsx(n,m,cnt,op,a,sa,a+n);
}
int n,a[2000100];
int slen,scnt,in[1000100],stpt[80100],edpt[80100],lg[1000100],gt[40];
inline void pre(){
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	slen=lg[n];
	for(int i=1;i<=n;i+=slen){
		++scnt,stpt[scnt]=i,edpt[scnt]=min(n,i+slen-1);
		for(int j=i;j<=edpt[scnt];++j)in[j]=scnt;
	}
	gt[0]=(1<<28)-1; for(int i=1;i<=24;++i) gt[i]=gt[i-1]^(1<<(i-1));
}
struct sttb{
	int sa[1000100],rk[1000100],ht[1000100],fr[1000100],bk[1000100],st[20][80100],data[1000100];
	inline void build(bool opt=0){
		for(int i=1;i<=n;++i) sa[i]=sa[i+1];
		for(int i=1;i<=n;++i) rk[sa[i]]=i;
		for(int i=1,j=0;i<=n;++i){if(j) --j; for(;a[i+j]==a[sa[rk[i]-1]+j];++j); ht[rk[i]]=j;}
		if(opt) reverse(rk+1,rk+n+1); for(int i=1;i<=n;++i) sa[i]=n-sa[i]+1;
		ht[0]=0x3f3f3f3f;
		for(int i=1,sc=1;i<=n;i+=slen,++sc){
			fr[i]=ht[i]; for(int j=i+1;j<=edpt[sc];++j) fr[j]=min(fr[j-1],ht[j]);
			bk[edpt[sc]]=ht[edpt[sc]]; for(int j=edpt[sc]-1;j>=i;--j) bk[j]=min(bk[j+1],ht[j]);
			int qu[40],tp=0,tv=0;
			for(int j=i;j<=edpt[sc];++j){
				for(;tp&&ht[i+qu[tp]]>ht[j];--tp) tv^=(1<<qu[tp]);
				++tp,qu[tp]=j-i,tv|=1<<qu[tp],data[j]=tv;
			}
		}
		for(int i=1;i<=scnt;++i){
			st[0][i]=bk[stpt[i]];
			for(int j=1;(1<<j)<=i;++j) st[j][i]=min(st[j-1][i-(1<<(j-1))],st[j-1][i]);
		}
	}
	inline int query(int x,int y){
		x=rk[x],y=rk[y]; if(x>y) swap(x,y); ++x;
		if(in[x]^in[y]){
			int rtr=min(bk[x],fr[y]);
			if(in[x]+1<in[y]){
				int t1=in[x]+1,t2=in[y]-1;
				int tem=lg[t2-t1+1];
				rtr=min({rtr,st[tem][t1+(1<<tem)-1],st[tem][t2]});
			}
			return rtr;
		}else{return ht[stpt[in[x]]+__builtin_ctz(gt[x-stpt[in[x]]]&data[y])];}
	}
}dsp,dss;
struct node{
	int lft,rgt,len;
	friend bool operator < (node x,node y){
		if(x.lft^y.lft) return x.lft<y.lft; return x.rgt<y.rgt;
	}
};
char ch[1000100];
vector<node> v,ansv;
inline void insert(int x,int y){
	int t1=0,t2=dss.query(x-1,y);
	if(y^n) t1=dsp.query(x,y+1); if(t1+t2>=y-x+1) v.emplace_back(node{x-t2,y+t1,y-x+1});
}
int main(){
	scanf("%s",ch+1),n=strlen(ch+1),pre();
	for(int i=1;i<=n;++i) a[i]=ch[n-i+1]; a[n+1]=1;
	sais(n+1,200,opbf,a,dss.sa,dss.rk,lmsbf),dss.build(1);
	reverse(a+1,a+n+1),sais(n+1,200,opbf,a,dsp.sa,dsp.rk,lmsbf),dsp.build();
	int st[1000100],tp=0;
	st[0]=n+1; for(int i=n;i;--i){for(;tp&&dsp.rk[st[tp]]<dsp.rk[i];--tp); insert(i,st[tp]-1),++tp,st[tp]=i;}
	tp=0; for(int i=n;i;--i){for(;tp&&dsp.rk[st[tp]]>dsp.rk[i];--tp); insert(i,st[tp]-1),++tp,st[tp]=i;}
	sort(v.begin(),v.end());
	node lst=node{0,0,0};
	for(auto i:v){if((i.lft^lst.lft)||(i.rgt^lst.rgt)) ansv.emplace_back(i); lst=i;}
	printf("%ld\n",ansv.size()); for(auto i:ansv) printf("%d %d %d\n",i.lft,i.rgt,i.len);
	return 0;
}

Acknowledgement

感谢 xrlong、Abnormal123 和 jijidawang 在进行部分推导时提供帮助。

Reference

RMQ - OI Wiki
Lyndon 分解 - OI Wiki
浅谈Lyndon Word - 洛谷专栏
Lyndon分解求最小表示法(Python)_lyhdon分解法-CSDN博客
lyndon 分解学习笔记 - Pitiless0514 - 博客园
Lyndon & Runs - 洛谷专栏
题解 P6656 【【模板】Runs】 - 洛谷专栏

posted @ 2025-06-29 16:08  Pursuing_OIer  阅读(33)  评论(0)    收藏  举报