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]\)
    1. \(w[|u'|+1]<a\)\(w\) 是一个 \(\text{Lyndon Word}\)
    2. \(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\)

#429. 【集训队作业2018】串串划分

本原平方串:形如 \(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\) 位的所有划分的权值和,有转移

\[f(i)=\sum_{j=0}^{i-1}(-1)^{C(S[j+1,i])+1}f(j) \]

对于 \(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;
}
posted @ 2022-06-07 23:52  Neal_lee  阅读(380)  评论(0编辑  收藏  举报