The Runs Theorem 和 Lyndon Tree 学习笔记
定义
\(\text{Runs}\):
一个 \(\text{run}\) 是一个三元组 \(\text{r}=(l,r,p)\),表示 \(s[l,r]\) 的最小周期为 \(p\),且 \([l,r]\) 区间是极大的,要求 \(2p\leq r-l+1\) 。实数 \(\frac{r-l+1}{p}\) 称为 \(r\) 的指数。\(Runs(w)\) 表示 \(w\) 的 \(runs\) 集合,\(\rho(n)\) 表示长度为 \(n\) 的字符串含有 \(\text{runs}\) 的最大个数,\(\sigma(n)\) 表示长 \(n\) 字符串 \(\text{runs}\) 指数之和的最大值。
\(\text{Lyndon Word}\):
若一个字符串 \(s\) 满足对其任意一个严格后缀 \(t\) 都有 \(s<t\),则称其为关于 \(<\) 的一个 \(\text{Lyndon Word}\)。
在字符集 \(\Sigma\) 上定义相反的全序关系 \(<_0,<_1\) (可以理解为正反字典序),对于字符串 \(w\) 记 \(\hat{w}=w\$\),\(\$\) 满足 \(\forall a\in \Sigma,\$<_0 a,a<_1\$\) 。记 \(l_l(i)\) 表示在 \(\hat{w}\) 上从 \(i\) 开始的最长关于 \(<_l\) 的 \(\text{Lyndon Word}\) 。
\(\text{Lyndon Root}\):
令 \(\text{r}=(l,r,p)\) 是一个 \(\text{run}\),长度为 \(p\) 的一个区间 \(\lambda=[l_{\lambda},r_{\lambda}]\) 被称为 \(\text{r}\) 的一个 \(\text{Lyndon Root}\) 当且仅当 \({l\leq l_{\lambda}\leq r_{\lambda}\leq r}\),且 \(\lambda\) 是一个 \(\text{Lyndon Word}\) 。即 \(\lambda\) 是 \(\text{r}\) 长度 \(=p\) 的周期的最小循环同构。
性质
证明篇幅太长了,可以见 WC2019课件 或 command_block 的博客 或 The "Runs" Theorem 。
这里只整理一下偏实用价值的最终结论。
\(\text{Lyndon Word}\) 的性质:
- 令 \(w=u^ku'a\),\(u\) 是 \(\text{Lyndon Word}\),\(u'\) 是 \(u\) 可为空的严格前缀,\(k\geq 1\),\(a\neq w[|u'|+1]\)
- \(w[|u'|+1]<a\): \(w\) 是一个 \(\text{Lyndon Word}\) 。
- \(w[|u'|+1]>a\): \(u\) 是任何一个以 \(w\) 为前缀的字符串的最长 \(\text{Lyndon Word}\) 前缀。
- 任何 \(\text{Lyndon Word}\) 均不存在 \(> 0\) 的 \(\text{border}\) 。
- \(s\) 是 \(\text{Lyndon Word}\) 当且仅当 \(s=ab\),\(a<b\) 且 \(a\) 和 \(b\) 均为 \(\text{Lyndon Word}\) 。
- 串的 \(\text{Lyndon}\) 分解:将字符串 \(s\) 分解成 \(s=s_1s_2...s_m\),满足 \(s_1..s_m\) 均为 \(\text{Lyndon Word}\) 且 \(s_1\geq s_2\geq...\geq s_m\) 。
- 串的 \(\text{Lyndon}\) 分解必然存在且唯一。
- 对于任意 \(\text{Lyndon Word}\) \(u\) 和 \(\text{Lyndon}\) 分解 \(f_1,..f_m\),\(u<f_1\iff uf_1..f_m<f_1..f_m\) 。
- 对于任意字符串 \(s\) 和一个位置 \(i\),有且仅有一个 \(l\in\{0,1\}\) 满足 \(l_l(i)=[i,i]\),且 \(l_{1-l}(i)=[i,j],j>i\) 。
\(\text{Runs}\) 的性质:
-
\(\text{The Runs Theorem}\):\(\rho(n)<n,\sigma(n)\leq 3n-3\) 。
-
对于 \(w\) 的一个 \(\text{run}\) \(\text{r}=(l,r,p)\),有且仅有一个 \(l\in\{0,1\}\) 满足 \(\hat{w}[r+1]<_l \hat{w}[r+1-p]\),所有 \(\text{r}\) 关于 \(<_l\) 的 \(\text{Lyndon Root}\) \(\lambda=[l_{\lambda},r_{\lambda}]\) 都与 \(l_{l}(l_{\lambda})\) 相等。(为 \(\text{Lyndon Word}\) 性质一的体现)
-
定义 \(B_{\text{r}}\) 为 \(\text{r}\) 的所有关于 \(<_l\) 的 \(\text{Lyndon Root}\) 的集合除去开头 \(l\) 开始的部分,\(\hat{w}[r+1]<_l \hat{w}[r+1-p]\)。对于任意两个 \(\text{runs}\;r,r'\),\(B_r\) 和 \(B_{r'}\) 的串的起点集合不交。
大体是反证考虑相交的部分,\(\lambda\) 应不同则 \(<_0,<_1\) 同时出现,则会有长度为 \(1\) 的周期,利用开头的空位和周期性可以导出一系列相等关系,破坏的 \(\text{Lyndon Word}\) 的成立,导致矛盾。
\(\text{Lyndon Tree}\) 的定义和性质:
- 一个 \(\text{Lyndon}\) 串 \(w(|w|\geq 2)\) 的标准划分是有序对 \((u,v)\),满足 \(w=uv\) 且 \(v\) 是最小的严格后缀。
- 定义:\(\text{Lyndon Tree}\) 是一棵树,每个节点都是一个 \(\text{Lyndon Word}\),根节点为 \(w\) 且 \(w\) 也是 \(\text{Lyndon}\) 串。节点的左右儿子分别为标准划分中的 \((u,v)\),叶子节点长度为 \(1\) 。
- \(\text{Lyndon Tree}\) 本质为 \(SA\) 中 \(rank\) 数组的笛卡尔树。
- 若子串 \(w[l,r]\) 是 \(\text{Lyndon Word}\),记 \(\alpha\) 为 \(\text{Lyndon Tree}\) 上 \(l\) 到 \(r\) 位置节点的 \(LCA\),\(\alpha\) 对应子串为 \(w[l_{\alpha},r_{\alpha}]\),则 \(l=l_{\alpha}\leq r\leq r_{\alpha}\)。若 \(w[l,r]=l_l(l)\),则对应的节点一定是一个右儿子节点。
- \(w\) 的任意一个 \(\text{run}\) 的所有 \(\text{Lyndon Root}\) 都会在 \(w\) 关于 \(<_0\) 或 \(<_1\) 的 \(\text{Lyndon Tree}\) 的右儿子节点出现,\(0/1\) 取决于满足 \(\hat{w}[r+1]<_l \hat{w}[r+1-p]\) 的 \(l\) 。
\(\text{Weak Periodicity Lemma}\) (WPL):
- 若一个串 \(S\) 有 \(p,q\) 两个周期,且 \(p+q\leq |S|\),则 \(\gcd(p,q)\) 也是 \(S\) 的周期。
- \(\text{Periodicity Lemma}\):满足 \(p+q-\gcd(p,q)\leq |S|\) 时 \(\gcd(p,q)\) 便是 \(S\) 的周期。
计算 Runs
由于两个周期为 \(p\) 的 \(\text{runs}\) 的交长度必然 \(<p\),因此同一区域的 \(\text{Lyndon Root}\) 与 \(\text{Runs}\) 存在对应关系,根据 \(\text{Runs}\) 的第二条性质,可以通过计算 \(l_l(i)\) 来找到所有 \(\text{Lyndon Root}\),每个 \(\text{Root}\) 向两侧拓展即可找到包含自己的 \(\text{run}\) 。
考虑从右往左对于字符串 \(w\) 的每个后缀维护 \(\text{Lyndon}\) 分解 \(f_1..f_m\),显然 \(w[i,n]\) 处分解的 \(f_1\) 即 \(l_l(i)\)。每次向左移动时将字符 \(c\) 作为独立的一个 \(\text{Lyndon Word}\) 加入到 \(f\) 序列开头,如果序列中存在相邻的 \(u,v\) 满足 \(u<v\),则将 \(u\) 和 \(v\) 合并为 \(uv\) 并不断检查重复此过程,最后得到的就是 \(w\) 的 \(\text{Lyndon}\) 分解。由 \(\text{Lyndon Word}\) 的第六条性质:\(u<f_1\iff uf_1..f_m<f_1..f_m\),实际上 \(u<v\) 只需要比较 \(SA\) 数组即可。
枚举 \(i\),设 \(l_l(i)=[l,r]\),求出最大的 \(l_1,l_2\) 满足 \(w[l,l+l_1-1]=w[r+1,r+l_1]\),\(w[l-l_2,l-1]=w[r-l_2+1,r]\),若 \(l_1+l_2\geq r-l+1\),那么就找到了一个 \(\text{run}\):\((l-l_2,r+l_1,r-l+1)\) 。这里本质是一些 \(LCP\) 问题。
由于每个 \(\text{runs}\) 的 “正序” \(<_l\) 会有不同,因此要先枚举 \(l\) 按照两类字典序分别进行以上过程。
通过前面结论可知该算法可以找出 \(w\) 的所有 \(\text{runs}\),用 \(SA\)-\(IS\) 算法加 \(O(n)-O(1)\) \(RMQ\) 可以做到严格线性,简易实现可以轻易做到 \(O(n\log n)\) 。
子串半周期查询(Two-Period Queries)
问题:需要一个数据结构支持快速查询 \(S\) 的一个子串是否有不超过长度一半的周期,如果有则求出最小周期。
定义 \(exrun(i,j)\) 为满足 \(i'\leq i,j'\geq j,p\leq (j-i+1)/2\) 的一个 \(\text{run}\) \((i',j',p)\) 。若 \(exrun(i,j)\) 存在则一定唯一,否则重叠的 \(exrun\) 可以通过 WLP 导出矛盾。
算法:根据定义,找出 \(exrun(i,j)\) 即可回答对于子串 \(w[i,j]\) 的询问。对 \(<_0,<_1\) 分别建出 \(\text{Lyndon Tree}\),令 \(a_0=lca_0([i,\lceil(i+j)/2\rceil]),a_1=lca_1([i,\lceil(i+j)/2\rceil])\),并判断它们的右儿子是否满足条件。
正确性:假设 \(exrun(i,j) = r = (i',j',p)\),那么由于 \(p ≤ (j−i+1)/2\),一定有 一个 \(\text{Lyndon root}\) \(λ = S[i_λ,j_λ]\) 包含 \(⌈(j − i + 1)/2⌉\) 这个位置。根据 \(\text{Lyndon Tree}\) 的性质,这个 \(\text{Lyndon root}\) 会在关于 \(<_l\) 的树中作为某个节点的右儿子出现。 这时我们有 \(a_l\) 的长度 \(>p\),且它同样包含 \(⌈(j − i + 1)/2⌉\) 这个位 置,因此 \(a_l\) 是 \(λ\) 的祖先。若它的右儿子 \(β = S[i_β,j_β] \neq λ\),则 \(β\) 也是 \(λ\) 的祖先。因为 \(λ\) 和 \(β\) 都是右儿子,可以得到 \(i ≤ i_β < i_λ\)。 若 \(j_β ≤ j\) 则 \(S[i_β,j_β]\) 有周期 \(p\),与它是 \(\text{Lyndon Word}\) 矛盾。若 \(j_β > j\) 可以发现 \(S[i_λ,j_β] <_ℓ S[i_β,j_β]\),同样与它是 \(\text{Lyndon Word}\) 矛盾。 上述矛盾表明我们的算法是正确的。
该问题似乎默认 \(S\) 为 \(\text{Lyndon Word}\) 。此处存疑,几份资料中仅 command_block 的博客提到了非 \(\text{Lyndon}\) 串的 \(\text{Lyndon Tree}\) 建法:\(\text{Lyndon}\) 分解后各自建树,组成的森林即对应数据结构。然而此方法似乎并不能解决此问题。
应用
给定长为 \(n\) 的字符串 \(S\),要求将其划分为 \(s_1s_2...s_k(k>1)\),满足每个字串 \(s_i\) 是不循环的,且 \(s_i\neq s_{i+1}\)。求划分方案数,\(|S|\leq 2\times 10^5\) 。
本原平方串:形如 \(w=ss\) 的串,满足 \(w\) 的最小周期为 \(\frac{|w|}2\),被称为 \(\text{primitive square}\) 。
- 若 \(SS\) 是 \(TT\) 的前缀,且 \(2|S|>|T|\),则 \(|T|-|S|\) 是 \(S\) 的周期。
- 若 \(u,v,w\) 满足 \(uu\) 是 \(vv\) 的前缀,\(vv\) 是 \(ww\) 的前缀,且 \(uu\) 是本原平方串,则 \(|u|+|v|\leq |w|\) 。
- 不同位置处的本原平方串个数总和为 \(O(n\log n)\) 级别。
- 本质不同本原平方串个数为 \(O(n)\) 级别。
(证明均可见于前文给出的链接)
注意到题目要求相邻串不同,考虑容斥,在 \(s_1=s_2\) 时,\(s_1+s_2\) 与 \(s_1s_2\) 两种划分都是不合法的,可以想办法抵消。于是设划分 \(s_1s_2...s_k\) 的系数为 \(f(s_1)f(s_2)\cdot...\cdot f(s_k)\),\(f(s)=(-1)^{C(s)+1}\),\(C(s)\) 表示 \(s\) 的最小循环节的出现次数。
设 \(f(i)\) 表示前 \(i\) 位的所有划分的权值和,有转移
对于 \(C(s)=1\) 的情况是平凡的,维护前缀和转移即可。而 \(C(s)>1\) 的串 \(s\) 必然存在于一个 \(\text{run}\) \(\text{r}=(l,r,p)\) 中,且 \(s\) 必然包含一个本原平方串作为后缀。可以发现位置 \(i\) 利用 \(\text{r}\) 可以转移到的位置是一个公差为 \(p\) 的等差数列,对于一个 \(\text{r}\),在 \(DP\) 的过程中同时维护 \(p\) 个等差数列各自的和即可。
注意到不同位置的本原平方串个数是 \(O(n\log n)\) 级别的,一个本原平方串可以使其右端点进行一次上面的等差数列转移,所以转移数量也是 \(O(n\log n)\) 级别的。
于是可以先字符串哈希 \(O(n\log n)\) 求出所有 \(\text{runs}\),然后 \(O(n\log n)\) 计算 \(DP\) 即可。
#include<iostream>
#include<stack>
#include<algorithm>
#include<vector>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 200022
#define ll long long
#define mod 998244353
#define Base 29
#define PII pair<int, int>
#define fr first
#define sc second
using namespace std;
struct StrHash{
ll p[N], pow[N];
void build(string s){
int n = s.size();
pow[0] = 1;
rep(i,1,n){
pow[i] = pow[i-1] * Base % mod;
p[i] = (p[i-1] * Base + s[i-1] - 'a') % mod;
}
}
bool equal(int l1, int r1, int l2, int r2){
return (p[r1] + mod - p[l1-1] * pow[r1-l1+1] % mod) % mod ==
(p[r2] + mod - p[l2-1] * pow[r2-l2+1] % mod) % mod;
}
} Hash;
struct Run{ int l, r, p; } runs[N*2];
string s;
int n, tot, l[2][N];
ll f[N];
vector<ll> sum[N*2][2];
vector<int> ex[N];
int extend(int i, int j, int dir){
if(s[i-1] != s[j-1]) return 0;
int l = 1, r = (dir ? n-max(i,j)+1 : min(i, j)), mid;
while(l < r){
mid = (l+r+1)>>1;
if(dir ? Hash.equal(i, i+mid-1, j, j+mid-1) : Hash.equal(i-mid+1, i, j-mid+1, j)) l = mid;
else r = mid-1;
}
return l;
}
int cmp(int l1, int r1, int l2, int r2){
int len = extend(l1, l2, 1), len1 = r1-l1+1, len2 = r2-l2+1;
if(len == min(len1, len2)) return len1 < len2 ? -1 : (len1 == len2 ? 0 : 1);
else return s[l1+len-1] < s[l2+len-1] ? -1 : (s[l1+len-1] == s[l2+len-1] ? 0 : 1);
}
void lyndon(int id){
stack<int> stk;
per(i,n,1){
while(!stk.empty() && cmp(i, n, stk.top(), n) == (id ? 1 : -1)) stk.pop();
l[id][i] = stk.empty() ? n : stk.top()-1;
stk.push(i);
}
}
void check(int l, int r){
int l1 = extend(l, r+1, 1), l2 = extend(l-1, r, 0);
if(l1+l2 >= r-l+1) runs[++tot] = {l-l2, r+l1, r-l+1};
}
int main(){
ios::sync_with_stdio(false);
cin>>s, n = s.size();
Hash.build(s);
rep(id,0,1){
lyndon(id);
rep(i,1,n) check(i, l[id][i]);
}
sort(runs+1, runs+tot+1, [&](Run a, Run b){
return a.l != b.l ? a.l < b.l : (a.r != b.r ? a.r < b.r : a.p < b.p); });
tot = unique(runs+1, runs+tot+1, [&](Run a, Run b){ return a.l == b.l && a.r == b.r; }) - runs-1;
rep(i,1,tot){
Run r = runs[i];
rep(j,r.l+2*r.p-1,r.r) ex[j].push_back(i);
int siz = r.p;
if(r.l + 3*r.p-1 > r.r) siz = (r.r-r.l+1) % r.p + 1;
rep(_,0,1) sum[i][_].resize(siz);
}
f[0] = 1; ll pref = 1;
rep(i,1,n){
f[i] = pref;
for(int k : ex[i]){
Run r = runs[k];
int id = (i - r.l + 1) % r.p;
(sum[k][0][id] += f[i-2*r.p]) %= mod;
(sum[k][1][id] *= mod-1) %= mod, (sum[k][1][id] += mod - f[i-2*r.p]) %= mod;
(f[i] += sum[k][1][id] + mod - sum[k][0][id]) %= mod;
}
(pref += f[i]) %= mod;
}
cout<< f[n] <<endl;
return 0;
}