其他分享
首页 > 其他分享> > The Runs Theorem 和 Lyndon Tree 学习笔记

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}\) 的性质:

\(\text{Runs}\) 的性质:

\(\text{Lyndon Tree}\) 的定义和性质:

\(\text{Weak Periodicity Lemma}\) (WPL):

计算 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}\) 。

(证明均可见于前文给出的链接)

注意到题目要求相邻串不同,考虑容斥,在 \(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;
}

标签:Runs,Word,int,text,Tree,runs,Lyndon,mod
来源: https://www.cnblogs.com/Neal-lee/p/16353936.html