Lyndon 理论学习笔记

upd:修了一些不严谨的表述。

字符串,太深刻了 /kk

引理、结论的证明建议不要跳过,很多问题的思考过程和引理、结论的证明过程是相通的。

定义

下标从 1 开始。

\(+\) 是字符串拼接。

\(|s|\) 表示 \(s\) 的长度。

\(s_i\) 表示 \(s\) 的第 \(i\) 个字符。

\(s^k\) 表示 \(k\)\(s\) 拼接的结果。

字符串间的大小关系用字典序比较,空串小于任何非空串。

Lyndon 分解

Duval 算法

字符串 \(s\) 是 Lyndon 串当且仅当 \(s\) 小于其每个非空真后缀。

有的文章里的“简单串”就是 Lyndon 串。

若字符串 \(s=w_1+\dots+w_k\),且 \(w_1\dots w_k\) 都是 Lyndon 串,且 \(w\) 字典序单调不增(\(w_i\ge w_{i+1}\)),

\(w_1\dots w_k\)\(s\) 的 Lyndon 分解。

引理 1:\(s\) 字典序最小的非空后缀为 \(w_k\)

证明:任意 \(w_i\) 都小于其每个非空真后缀,所以从 \(w_i\) 起头一定比从 \(w_i\) 中间起头(也就是以 \(w_i\) 的某个非空真后缀起头)优,

\(w_k\) 小于等于 \(w_i(i\ne k)\),所以从 \(w_k\) 起头一定比从 \(w_i\) 起头优,则肯定从 \(w_k\) 起头,这个后缀就是 \(w_k\)

引理 2:Lyndon 分解唯一存在。

证明:由引理 1 可以唯一确定 \(w_k\),而 \(w_1\dots w_{k-1}\) 一定是 \(s\) 去掉 \(w_k\) 后的串的 Lyndon 分解,于是可以确定 \(w_{k-1}\)

以此类推,可以唯一确定 \(w_1\dots w_k\)

由引理 2 可以轻松得到一个 \(O(n^2)\) 的 Lyndon 分解算法,但是这显然不是我们想要的。

Duval 算法可以 \(O(n)\) 求出 Lyndon 分解。

定义字符串 \(s\) 是近似 Lyndon 串,当且仅当 \(s=w^k+w'\),其中 \(w\) 是任意 Lyndon 串,\(k\ge 1\)\(w'\)\(w\) 的前缀(可以空)。

划分 \(s=s_1+s_2+s_3\),其中 \(s_1\) 的 Lyndon 分解已经得出,\(s_2\) 是一个近似 Lyndon 串,\(s_3\) 无要求,

初始 \(s_1\) 为空,\(s_2\)\(s\) 的第一个字符(单字符肯定是近似 Lyndon 串),\(s_3\)\(s\) 去掉第一个字符后的串,

现在需要调整划分方案,使得 \(s_1=s\)\(s_2=s_3=\varnothing\)

\(s_2=w^k+w'\)\(x=w_{|w'|+1}\),尝试每次去掉 \(s_3\) 开头的字符 \(y\) 加到 \(s_2\) 末尾,考虑 \(x,y\) 的关系:

  • \(x=y\)\(w^k+w'+x\) 仍是近似 Lyndon 串,无需调整划分方案,更新一下 \(w'\) 即可。
  • \(x<y\)\(w^k+w'+y\) 是 Lyndon 串。

证明:设 \(t=w^k+w'+y\)。需要证明 \(t\) 字典序最小的非空后缀是 \(t\) 本身。
引理 1 已经证过从 \(w\) 起头一定比从 \(w\) 中间起头优,
\(x<y\)\(w'+y>w\),而从第 \(i(i\ne 1)\)\(w\) 起头比从第一个 \(w\) 起头先走到 \(w'+y\)
则从第一个 \(w\) 起头一定比从第 \(i\)\(w\) 起头优,则肯定从第一个 \(w\) 起头,这个后缀就是 \(t\) 本身。

Lyndon 串肯定也是近似 Lyndon 串,所以也无需调整划分方案,

\(w'+y\) 并不是 \(w\) 的前缀,所以需要更新 \(w=t\)\(w'=\varnothing\)

  • \(x>y\)\(w^k+w'+y\) 啥也不是,必须调整划分方案。把 \(k\)\(w\) 划分进 \(s_1\),令 \(s_2\)\(s_1\) 后第一个字符,\(s_3\) 为剩下的串即可。

\(s_3\) 为空时像 \(x>y\) 一样调整划分方案(把 \(k\)\(w\) 划分进 \(s_1\),令 \(s_2\)\(s_1\) 后第一个字符,\(s_3\) 为剩下的串),

以此法不断去掉 \(s_3\) 开头的字符加到 \(s_2\) 末尾,可以发现 \(s_1\) 的长度一直在增加,最终一定能达到 \(s_1=s\)

实现的时候一般用类似最小表示法的三指针,维护 \(s_2\) 开头 \(i\)\(s_3\) 开头 \(j\),以及 \(k=j-|w|\)\(s_k=x\))。

//洛谷 P6114 【模板】Lyndon 分解
#include <cstdio>
#include <cstring>
int n, z;
char a[10000050];
int main()
{
    scanf("%s", a + 1), n = strlen(a + 1);
    int i = 1, j, k;
    while (i <= n)
    {
        j = i + 1, k = i;
        while (j <= n)
        {
            if (a[j] == a[k])
                ++j, ++k;
            else if (a[j] > a[k])
                ++j, k = i;
            else
                break;
        }
        while (i <= k)
            z ^= i + j - k - 1, i += j - k; //[i, i + j - k - 1] 是分解出的一个 Lyndon 串
    }
    printf("%d", z);
    return 0;
}

所以这玩意有啥用?

最小表示法

给定字符串 \(s\),求与 \(s\) 循环同构的字典序最小的串。(假设你不会正常的最小表示法)

想一想,怎么做

\(t=s+s\),则需要找出 \(t\) 的长度为 \(n\) 的最小子串。

\(t\) Lyndon 分解,引理 1 证过从 \(w_i\) 中间起头不优,且越靠后的 \(w_i\) 越优,

所以最优起头位置为最后一个 \(w_i\),使得 \(w_i\) 左端点后有不少于 \(n\) 个字符。

//洛谷 P1368 【模板】最小表示法 
#include <cstdio>
int n, m, z, a[10000050];
int main()
{
    scanf("%d", &m), n = m << 1;
    for (int i = 1; i <= m; ++i)
        scanf("%d", a + i), a[m + i] = a[i];
    int i = 1, j, k;
    while (i <= n)
    {
        j = i + 1, k = i;
        while (j <= n)
        {
            if (a[j] == a[k])
                ++j, ++k;
            else if (a[j] > a[k])
                ++j, k = i;
            else
                break;
        }
        while (i <= k)
        {
            if (i <= m + 1)
                z = i;
            i += j - k;
        }
    }
    for (int i = z; i < z + m; ++i)
        printf("%d ", a[i]);
    return 0;
}

简单字符串 弱化版

给定字符串 \(s\) 和整数 \(k\),把 \(s\) 划分为不超过 \(k\) 个串 \(c_1\dots c_m(m\le k)\),求 \(\max c_i\) 的最小值。

想一想,怎么做

\(s\) Lyndon 分解得到 \(s=w_1^{m_1}+\dots+w_z^{m_z}\),则 \(\max c_i\) 有多大取决于其分到多少个 \(w_1\)

根据鸽巢原理,\(\max c_i\) 至少分到 \(\left\lceil\dfrac {m_1}k\right\rceil\)\(w_1\)

\(k\mid m_1\),则每个 \(c_i\) 都分到 \(m_1/k\)\(w_1\),最后一个 \(c_i\) 还要承包 \(w_2^{m_2}+\dots+w_z^{m_z}\)

于是 \(\max c_i\) 就是最后一个 \(c_i\),即 \(w_1^{m_1/k}+w_2^{m_2}+\dots+w_z^{m_z}\)

否则有些 \(c_i\) 分到了 \(\left\lceil\dfrac {m_1}k\right\rceil\)\(w_1\)(A 类),有些 \(c_i\) 分到了 \(\left\lfloor\dfrac {m_1}k\right\rfloor\)\(w_1\)(B 类),

令最后一个 \(c_i\) 属于 B 类,这样其承包 \(w_2^{m_2}+\dots+w_z^{m_z}\)\(\max c_i\) 还是之前的一个 A 类,即 \(w_1^{\left\lceil\frac {m_1}k\right\rceil}\)

牛客 201916 简单字符串(对每个后缀 Lyndon 分解)

\(f(s,k)\) 表示对 \(s,k\) 做《简单字符串 弱化版》的结果,

给定字符串 \(s\),有 \(q\) 次询问,每次给定 \(x,y\),设 \(t\)\(s\) 长度为 \(x\) 的后缀,求 \(f(t,y)\)

想一想,怎么做

如果能对所有后缀 Lyndon 分解,那这题就做完了。

Sol 1:

实际上只需要对每个后缀求出其分解出的 \(w_1\),因为 \(w_2\dots w_k\) 一定是这个后缀去掉 \(w_1\) 后的串的 Lyndon 分解。

结论:对于 \(s\)\(i\) 开头的后缀 \(S_i\)\(S_{n+1}=\varnothing\))求出最小的 \(j\) 使得 \(j>i,S_j<S_i\),则 \(S_i\) 分解出的 \(w_1\)\(s\)\(i\)\(j-1\) 的子串。

证明:只需要证 \(w_1\) 是 Lyndon 串,且 \(w_1\ge w_2\),剩下的归纳即可。

  • \(w_1\) 是 Lyndon 串:考虑反证法,设 \(w_1\) 有非空后缀 \(t\) 小于 \(w_1\),而 \(S_i\)\(t\) 起头的后缀 \(X\) 又大于 \(S_i=Y\),所以 \(t\) 肯定是 \(w_1\) 的真前缀。对 \(X\)\(Y\) 同时去掉前缀 \(t\),得到 \(S_j=X'\) 大于 \(S_i\)\(w_1\) 的某个非空后缀 \(t'\) 起头的后缀 \(Y'\),则 \(Y'<X'<S_i\),这样 \(w_2\) 就应该以 \(t'\) 开头。但 \(w_2\) 并没有以 \(t'\) 开头,所以 \(w_1\) 是 Lyndon 串。

  • \(w_1\ge w_2\):考虑反证法,设 \(w_1<w_2\)。而 \(S_i>S_j\),所以 \(w_1\) 肯定是 \(w_2\) 的真前缀。对 \(S_i\)\(S_j\) 同时去掉 \(w_1\) 前缀,得到 \(S_j\) 大于 \(S_i\)\(w_2\) 的某个非空后缀 \(t\) 起头的后缀,这样 \(w_3\) 就应该以 \(t\) 开头。但 \(w_3\) 并没有以 \(t\) 开头,所以 \(w_1\ge w_2\)

求出 SA 后扫描线 + set 即可对每个 \(S_i\) 求出最小的 \(j\) 使得 \(j>i,S_j<S_i\),然后就做完了。

Sol 2:

考虑每次往前加入一个字符,用栈维护当前串的 Lyndon 分解,栈顶是 \(w_1\)

考虑每次加入字符 \(c\) 后怎么求出新的 \(w_1\),首先设 \(w_1=c\),每次比较 \(w_1\) 和栈顶的大小,

\(w_1\) 大于等于栈顶,直接把 \(w_1\) 入栈即可;若 \(w_1\) 小于栈顶,则 \(w_1\) 与栈顶拼接后一定还是 Lyndon 串,把 \(w_1\) 拼上栈顶即可。

进一步地,分讨一下可以发现,要比较 \(w_1\) 与栈顶的大小关系,只需要比较以 \(w_1\) 开头的后缀与以栈顶开头的后缀的大小关系,

如果可以 \(O(n)\) 建 SA,复杂度为 \(O(n)\)

HDU 6761 Minimum Index(对每个前缀 Lyndon 分解)

给定字符串 \(s\),求 \(s\) 每个前缀的最小非空后缀。

想一想,怎么做

考虑在 Duval 算法的过程中求出 \(s_1+s_2\) 每个时刻的最小非空后缀。

\(s_2=w\),则最小非空后缀为 \(w\)

\(s_2=w^k+w'\),此时引理 1 的分析不再完全适用:从 \(w\) 中间起头仍然不如从 \(w\) 起头,

但是从 \(w'\) 中间起头可能优于从 \(w'\) 起头,因为 \(w'\) 不是 Lyndon 串。

所以只有可能从 \(w\) 起头或从 \(w'\) 中间起头,可以发现去掉后 \(|w|\) 个字符后答案不变,而去掉后 \(|w|\) 个字符后的答案早已求出。

可以发现过程中 \(s_1+s_2\) 会覆盖每个前缀,所以这就够了。

实际上本题对每个前缀求出了其 Lyndon 分解出的最后一个串 \(w_k\),可以由此得到每个前缀的 Lyndon 分解。

//HDU 6761 Minimum Index
#include <cstdio>
#include <cstring>
#define M 1000000007
int T, n, z[10000050];
char a[10000050];
int main()
{
    scanf("%d", &T);
    while (T--)
    {
        scanf("%s", a + 1), n = strlen(a + 1);
        int i = 1, j, k;
        while (i <= n)
        {
            j = i + 1, k = i;
            while (1)
            {
                if (k == i)
                    z[j - 1] = i;
                else
                    z[j - 1] = z[k - 1] + j - k;
                if (j > n)
                    break;
                if (a[j] == a[k])
                    ++j, ++k;
                else if (a[j] > a[k])
                    ++j, k = i;
                else
                    break;
            }
            while (i <= k)
                i += j - k;
        }
        int Z = 0;
        for (int i = 1, o = 1; i <= n; ++i)
            Z = (Z + 1ll * z[i] * o) % M, o = o * 1112ll % M;
        printf("%d\n", Z);
    }
    return 0;
}

洛谷 P5334 [JSOI2019] 节日庆典

给定字符串 \(s\),求 \(s\) 每个前缀的最小表示法。

想一想,怎么做

求某个串 \(s\) 的最小表示,实际上就是求 \(s\) 的一个非空后缀 \(S\),设 \(s\) 去掉 \(S\) 后得到的前缀为 \(P\),求 \(\min(S+P)\)

直接取 \(s\) 的最小非空后缀作为 \(S\) 肯定不行,比如 \(s=\texttt{baa}\) 时,\(s\) 的最小非空后缀为 \(\texttt{a}\),取 \(S=\texttt{a}\) 得到 \(S+P=\texttt{aba}\)

但是取 \(S=\texttt{aa}\) 可以得到 \(S+P=\texttt{aab}\),比直接取最小非空后缀作为 \(S\) 要优。

然而,我们仍然可以沿用上一题求每个前缀的最小非空后缀的思路,在 Lyndon 分解的过程中求解。

考虑在 Lyndon 分解的过程中求出 \(s_1+s_2\) 每个时刻的最小表示法。

\(s_2=w\),取 \(S=w\)

\(s_2=w^k+w'\),此时去掉后 \(|w|\) 个字符后最优 \(S\) 可能会变,

因为可能取 \(S=w^k+w'\) 最优,但是去掉后 \(|w|\) 个字符后就取不到 \(S=w^k+w'\) 了,

所以需要考虑去掉后 \(|w|\) 个字符后的最优 \(S\)(下文设为 \(T\))和 \(w^k+w'\)

如果 \(|T|>|w^{k-1}+w'|\),那么此时的最优 \(S\) 根本取不到 \(T\),直接取 \(w^k+w'\) 即可,

否则 \(T\) 要么从某个 \(w\) 起头,要么从 \(w'\) 的某个后缀起头,

结论:\(|T|\le|w^{k-1}+w'|\) 时,\(T\) 一定是 \(w^k+w'\) 的前缀。

证明:若 \(T\) 从某个 \(w\) 起头,则 \(T\) 形如 \(w^p+w'(p<k)\),一定是 \(w^k+w'\) 的前缀。

\(T\)\(w'\) 的某个后缀 \(t\),则 \(t\) 一定是 \(w\) 的前缀,

否则 \(t\)\(w\) 的比较在 \(|t|\) 前就结束了,若 \(t<w\)\(w\) 不是 Lyndon 串,若 \(t>w\)\(T\) 不应该从 \(t\) 起头,而应该从 \(w\) 起头。

所以 \(w^k+w'\)\(T\) 的关系形如下图:

现在需要比较从 \(\uparrow_A\)\(\uparrow_B\) 开头的两种答案,

它们的前 \(|T|\) 个字符相同,所以只需要比较从 \(\downarrow_A\)\(\downarrow_B\) 开头的两种答案,

此时只需要求原串与后缀的 LCP,预处理 Z 函数即可。

//洛谷 P5334 [JSOI2019] 节日庆典 
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 1000000007
using namespace std;
int n, z[10000050], Z[10000050];
char a[10000050];
int main()
{
    scanf("%s", a + 1), n = strlen(a + 1);
    z[1] = n;
    for (int i = 2, l = 0, r = 0; i <= n; ++i)
    {
        if (i <= r)
            z[i] = min(z[i - l + 1], r - i + 1);
        while (i + z[i] <= n && a[i + z[i]] == a[z[i] + 1])
            ++z[i];
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
    int i = 1, j, k;
    while (i <= n)
    {
        j = i + 1, k = i;
        while (1)
        {
            if (!Z[j - 1]) // 这题在最小表示法相同时要输出最小开头,所以取每个前缀第一次被 s1+s2 覆盖时的答案
            {
                if (k == i || k - Z[k - 1] > k - i) // s2=w 时直接取 w,|T|>|w^(k-1)+w'| 时直接取 w^k+w'
                    Z[j - 1] = i;
                else //|T|<=|w^(k-1)+w'|,用 Z 函数比较两种答案即可
                {
                    bool F;
                    int S = i, T = Z[k - 1] + j - k;
                    int A = S + j - T, B = 1;
                    if (z[A] >= j - A)
                    {
                        A = 1, B += j - A;
                        if (z[B] >= T - B)
                            F = S < T;
                        else
                            F = a[z[B] + 1] < a[B + z[B]];
                    }
                    else
                        F = a[A + z[A]] < a[z[A] + 1];
                    if (F)
                        Z[j - 1] = S;
                    else
                        Z[j - 1] = T;
                }
            }
            if (j > n)
                break;
            if (a[j] == a[k])
                ++j, ++k;
            else if (a[j] > a[k])
                ++j, k = i;
            else
                break;
        }
        while (i <= k)
            i += j - k;
    }
    for (int i = 1; i <= n; ++i)
        printf("%d ", Z[i]);
    return 0;
}

洛谷 P5108 仰望半月的夜空

给一个字符串 \(s\),对 \(i\in[1,|s|]\) 问长度为 \(i\) 的最小子串的第一次出现。

想一想,怎么做 字典序问题,可以在 Lyndon 分解上考虑,设分解得到 $s=w_1+\dots+w_k$。

考虑长度为 \(i\) 的最小子串在哪里起头。首先根据 Lyndon 串的定义,在某个 \(w_p\) 中间起头肯定不优,

而且分解出的 Lyndon 串字典序是单调不增的,所以应该在尽量靠后的 \(w_p\) 起头,

所以可以得出结论:应该在 \(|w_p+w_{p+1}+\dots+w_k|\ge i\) 的最后一个 \(w_p\) 处起头,

但这个子串不一定只在这里出现,考虑它还在哪里出现过。

分解出的 Lyndon 串字典序是单调不增的,所以这个子串只在 \(w_p\) 及其前的若干个 Lyndon 串中出现。

二分这个子串最早在哪里出现即可。

#include <cstdio>
#include <cstring>
char _[300050];
int n, o, S, l[300050], a[300050];
unsigned long long p[300050], h[300050];
int Q(int l, int r) { return h[r] - h[l - 1] * p[r - l + 1]; }
int main()
{
    scanf("%d%d", &S, &n);
    if (S == 26)
    {
        scanf("%s", _ + 1);
        for (int i = 1; i <= n; ++i)
            a[i] = _[i];
    }
    else
    {
        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);
    }
    for (int i = p[0] = 1; i <= n; ++i)
        p[i] = p[i - 1] * 10000019, h[i] = h[i - 1] * 10000019 + a[i];
    int i = 1, j, k;
    while (i <= n)
    {
        j = i + 1, k = i;
        while (j <= n)
        {
            if (a[j] == a[k])
                ++j, ++k;
            else if (a[j] > a[k])
                ++j, k = i;
            else
                break;
        }
        while (i <= k)
            l[++o] = i, i += j - k;
    }
    for (int i = 1, j = o; i <= n; ++i)
    {
        if (n - l[j] + 1 < i)
            --j;
        int L = 1, R = j;
        while (L <= R)
        {
            int M = L + R >> 1;
            if (Q(l[M], l[M] + i - 1) == Q(l[j], l[j] + i - 1))
                R = M - 1;
            else
                L = M + 1;
        }
        printf("%d ", l[L]);
    }
    return 0;
}

最小后缀族

考虑这样一个问题:给定字符串 \(s\),每次询问一个字符串 \(T\),求 \(s\) 的一个后缀 \(S\)(可空)使得 \(S+T\) 最小。

称可能成为最优 \(S\) 的后缀为 \(s\) 的有效后缀,\(s\) 的最小后缀族 \(SS(s)\) 就是 \(s\) 的所有有效后缀组成的集合。

引理 3:若 \(S\)\(T\) 都是 \(s\) 的有效后缀且 \(|S|<|T|\),则 \(S\)\(T\) 的 border 且 \(2|S|\le|T|\)

证明:首先一定有 \(S<T\),否则 \(S\)\(T\) 的比较在 \(|S|\) 前就结束了,\(S\) 不可能是有效后缀,

而且 \(S\) 一定是 \(T\) 的前缀,否则 \(S\)\(T\) 的比较在 \(|S|\) 前就结束了,\(T\) 不可能是有效后缀,

\(S\) 明显是 \(T\) 的后缀,所以 \(S\)\(T\) 的 border。

考虑反证法,若 \(2|S|>|T|\),设 \(S\) 长度为 \(|T|-|S|\) 的前缀为 \(u\),则 \(T\) 一定可以写成 \(u^2+v\),而 \(S=u+v\)

若存在字符串 \(y\) 使得 \(u+v+y<v+y\),则 \(u^2+v+y<u+v+y\)\(S=u+v\) 要么比 \(T=u^2+v\) 劣,要么比 \(v\) 劣,\(S\) 一定不是有效后缀,

但是 \(S\) 确实是有效后缀,所以 \(2|S|\le|T|\)

由引理 3 可知,\(|SS(s)|=O(\log |S|)\)

引理 4:有效后缀一定形如 \(S_i=w_i^{c_i}+\dots+w_k^{c_k}\)\(S_{k+1}=\varnothing\))。

证明:引理 1 已经证过从 \(w_i\) 中间起头不优,只需要证一定从第一个 \(w_i\) 起头,

类似引理 3 后半部分的证法,\(w_i^x+\dots+w_k^{c_k}(x\ne c_i)\) 要么比 \(w_i^{x+1}+\dots+w_k^{c_k}\) 劣,要么比 \(w_i^{x-1}+\dots+w_k^{c_k}\) 劣,

则不从第一个 \(w_i\) 起头肯定不优,所以一定从第一个 \(w_i\) 起头。

\(SS(s)\) 咋求呢?\(\newcommand{\L}{\lambda}\)

考虑 Duval 算法过程中第一次 \(s_3=\varnothing\) 的时刻,设此时 \(s_2=w_{\L}^{c_{\L}}+w_{\L}'\)

可以发现 \(S_{\L}=w_{\L}^{c_{\L}}+w_{\L}'\)\(S_{\L+1}=w_{\L}'\),所以 \(S_{\L+1}\)\(S_{\L}\) 的前缀,

考虑 Duval 算法的过程,从 \(|s_2|=1\) 开始,每次往 \(s_2\) 后加一个字符后 \(s_2\) 一直是近似 Lyndon 串,

所以 \(s_2\) 的每个前缀都是近似 Lyndon 串,\(S_{\L+1}=w_{\L}'\) 明显是 \(s_2\) 的前缀,于是 \(S_{\L+1}\) 也是近似 Lyndon 串,

下次 \(s_3=\varnothing\)\(s_2=S_{\L+1}=w_{\L+1}^{c_{\L+1}}+w_{\L+1}'\)\(S_{\L+2}=w_{\L+1}'\),所以 \(S_{\L+2}\)\(S_{\L+1}\) 的前缀,

以此类推,\(\forall i\ge\L\)\(S_{i+1}\)\(S_i\) 的前缀,并且每次 \(s_3=\varnothing\) 时的 \(s_2\) 取遍 \(S_i(i\ge\L)\)

\(\forall i<\L,S_i=w_i^{c_i}+w_i'+s_3,S_{i+1}=w_i'+s_3\),而 \(s_3<w_i\),所以 \(S_{i+1}\) 肯定不是 \(S_i\) 的前缀。于是:

引理 5:\(S_{i+1}\)\(S_i\) 的前缀当且仅当 \(i\ge\L\)

所以,结合引理 3 可以确定,\(\forall i<\L,S_i\notin SS(s)\)。实际上 \(\forall i\ge\L,S_i\in SS(s)\),接下来将说明这一点。

回到原问题:给定字符串 \(s\),每次询问一个字符串 \(T\),求 \(s\) 的一个后缀 \(S_i\)(可空)使得 \(S_i+T\) 最小。

首先 \(S_i\in SS(s)\),所以 \(i\ge\L\)。对于 \(i\ge\L\)\(w_i=S_{i+1}+y_i\)\(x_i=y_i+S_{i+1}\),则 \(S_i=S_{i+1}+x_i^{c_i}\)

(笑点解析:论文这里 typo 了,写成了 \(S_i=S_{i+1}+y_i\),模了半天没模明白)

引理 6:\(\forall i\in[\L,k-1],x_i^{\infty}>x_{i+1}^{\infty}\)

证明:首先 \(x_i^{\infty}>y_i\),只需证 \(y_i>x_{i+1}^{\infty}\)

两边同时在前面拼接 \(S_{i+1}\),左边 \(S_{i+1}+y_i=w_i\),右边

\[\begin{aligned} &S_{i+1}+x_{i+1}^{\infty}\\ =&S_{i+1}+(y_{i+1}+S_{i+2})^{\infty}\\ =&S_{i+2}+x_{i+1}^{c_{i+1}}+(y_{i+1}+S_{i+2})^{\infty}\\ =&S_{i+2}+(y_{i+1}+S_{i+2})^{c_{i+1}}+(y_{i+1}+S_{i+2})^{\infty}\\ =&S_{i+2}+(y_{i+1}+S_{i+2})^{\infty}\\ =&(S_{i+2}+y_{i+1})^{\infty}+S_{i+2}\\ =&w_{i+1}^{\infty}+S_{i+2} \end{aligned} \]

字符串推式子见过没(

此时只需证 \(w_i>w_{i+1}^{\infty}\)

根据引理 5 的证明过程,\(S_{i+1}\)\(w_i\) 的前缀,那么 \(w_{i+1}\) 肯定也是 \(w_i\) 的前缀,

\(w_i=w_{i+1}^k+t\),其中 \(w_{i+1}\) 不是 \(t\) 的前缀,而 \(w_i\) 是 Lyndon 串,所以 \(t>w_i\)

而且 \(w_{i+1}\) 不是 \(t\) 的前缀,所以 \(t\)\(w_i\) 的比较在前 \(|w_{i+1}|\) 位就能结束,所以 \(t>w_{i+1}^{\infty}\)

两边同时在前面拼接 \(w_{i+1}^k\),得到 \(w_{i+1}^k+t>w_{i+1}^k+w_{i+1}^{\infty}\),所以 \(w_i>w_{i+1}^{\infty}\)

再次回到原问题:对给定的 \(T\),考虑比较 \(S_i+T\)\(S_{i+1}+T\)

首先 \(S_i+T=S_{i+1}+x_i^{c_i}+T\),只需比较 \(x_i^{c_i}+T\)\(T\)

(笑点解析:这里论文又 typo 了,翻转后缀见过没)

结论:\(x_i^{c_i}+T<T\) 当且仅当 \(x_i^{\infty}<T\)

证明:设 \(T=(x_i^{c_i})^k+t\),其中 \(x_i^{c_i}\) 不是 \(t\) 的前缀,\(k\) 可能等于 \(0\),则需要比较 \((x_i^{c_i})^{k+1}+t\)\((x_i^{c_i})^k+t\)

由于 \(x_i^{c_i}\) 不是 \(t\) 的前缀,前 \(|(x_i^{c_i})^{k+1}|\) 位一定能比出结果,所以用 \(x_i^{\infty}\)\(T=(x_i^{c_i})^k+t\) 一定能比出相同的结果。

所以 \(S_i+T<S_{i+1}+T\iff x_i^{c_i}+T<T\iff x_i^{\infty}<T\)

\(x_i^{\infty}\) 单调减,所以一定存在 \(p\) 使得 \(\forall i<p,x_i^{\infty}>T\iff S_i+T>S_{i+1}+T,\forall i\ge p,x_i^{\infty}<T\iff S_i+T<S_{i+1}+T\)

可以发现答案就是 \(S_p+T\),二分求出 \(p\) 即可。

注意到 \(\forall i\ge\L\)\(S_i\) 都有可能成为 \(S_p\),这样就证明了上面提出的 \(\forall i\ge\L,S_i\in SS(s)\)

下文将求出的 \(S_p\) 称为“后接 \(T\) 的最小后缀”。

所以这玩意有啥用?

最大后缀

给定字符串 \(s\),求 \(s\) 每个前缀的最大后缀。

(笑点解析:论文原题是求 \(s\) 的最大后缀)

想一想,怎么做

先做一下论文原题:

首先反转字母表之后就是求最小后缀了吗?并不是,

比如后缀 \(S\) 是后缀 \(T\) 的前缀时应该取较长的 \(T\),但是反转字母表后求最小后缀会取到较短的 \(S\)

考虑修补这个做法,反转字母表后设一个极大字符 \(\texttt{#}\),然后求出后接 \(\texttt{#}\) 的最小后缀,这样就可以取到较长的 \(T\) 了,

\(\texttt{#}\) 肯定大于所有 \(x_i\),所以后接 \(\texttt{#}\) 的最小后缀就是 \(S_{\L}\)

(但是根本没必要这么做,直接在 \(s\) 后面接一个 \(\texttt{#}\),然后求最小后缀就行了……)

考虑求 \(s\) 每个前缀的最大后缀。

肯定不能把每个前缀后面都接上 \(\texttt{#}\) 做一次,所以考虑对每个前缀求出 \(S_{\L}\)

考虑之前几个题的做法,\(s_1+s_2\) 会覆盖所有前缀,所以某个前缀第一次被 \(s_1+s_2\) 覆盖时,\(s_2\) 就是这个前缀的 \(S_{\L}\)

洛谷 P5334 [JSOI2019] 节日庆典(求每个前缀的 SS)

再 放 送

给定字符串 \(s\),求 \(s\) 每个前缀的最小表示法。

想一想,怎么做

求某个串 \(s\) 的最小表示,实际上就是求 \(s\) 的一个非空后缀 \(S\),设 \(s\) 去掉 \(S\) 后得到的前缀为 \(P\),求 \(\min(S+P)\)

根据引理 5,对于有效后缀 \(S_i\) 和不是有效后缀的后缀 \(T\)(明显有 \(S_i<T\)),\(S_i\) 一定不是 \(T\) 的前缀,

所以 \(S_i\)\(T\) 的比较在 \(|S_i|\) 前就结束了,最优 \(S\) 一定不会取到 \(T\)

所以求最优 \(S\) 只需要考虑有效后缀,现在只需要对每个前缀求出所有有效后缀。

\(P_i\) 表示 \(s\)\(i\) 结尾的前缀,设 \(SS(P_i)\) 表示 \(P_i\) 的所有有效后缀的起头位置集合,

根据有效后缀的定义,可以发现 \(SS(P_i)-SS(P_{i-1})\) 要么为 \(\{i\}\),要么为 \(\varnothing\)

于是考虑从 \(SS(P_{i-1})\) 递推到 \(SS(P_i)\)

考虑从短到长依次加入 \(SS(P_{i-1})\) 中每个起头位置对应的后缀 \(S_j+s_i\)

加入 \(S_j+s_i\) 时,设加入的上一个后缀为 \(S_p+s_i\),首先 \(S_p\) 肯定是 \(S_j\) 的前缀,设 \(S_j\) 的第 \(|S_p|+1\) 个字符为 \(x\)

\(x>s_i\),则前 \(|S_p+s_i|\) 位就能比出 \(S_p+s_i<S_j+s_i\),那肯定就不用加入 \(S_j+s_i\) 了,

\(x=s_i\)\(2|S_p+s_i|\le|S_j+s_i|\),则 \(S_p+s_i\) 有可能是有效后缀(满足引理 3 不一定是有效后缀),

不管 \(S_p+s_i\) 是不是有效后缀,保留 \(S_p+s_i\) 对复杂度和正确性都没影响,直接加入 \(S_j+s_i\) 即可,

否则 \(S_p+s_i\) 不可能是有效后缀(不满足引理 3 一定不是有效后缀),删除 \(S_p+s_i\) 后加入 \(S_j+s_i\) 即可。

再之前加入的后缀同样满足引理 3,所以它们也有可能是有效后缀,保留它们对复杂度和正确性都没影响,所以不用管它们。

上面的“有可能”使得维护出的 \(SS'(P_i)\) 只是满足引理 3 的一个后缀集合,而不是真正的有效后缀集合 \(SS(P_i)\)

\(SS(P_i)\) 肯定是 \(SS'(P_i)\) 的子集,而且 \(SS'(P_i)\) 的大小也是 \(O(\log|s|)\),这就够了。

\(SS'(P_i)\) 中的后缀满足引理 3,所以仍然可以用 Z 函数比较两个后缀对应的答案。总复杂度 \(O(|s|\log|s|)\)

甚至不需要 Lyndon 分解……

#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n, z[3000050];
char a[3000050];
vector<int> S[3000050];
int main()
{
    scanf("%s", a + 1), n = strlen(a + 1);
    z[1] = n;
    for (int i = 2, l = 0, r = 0; i <= n; ++i)
    {
        if (i <= r)
            z[i] = min(z[i - l + 1], r - i + 1);
        while (i + z[i] <= n && a[i + z[i]] == a[z[i] + 1])
            ++z[i];
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
    S[1].push_back(1), printf("1 ");
    for (int i = 2; i <= n; ++i)
    {
        S[i].push_back(i);
        for (auto j : S[i - 1])
        {
            if (a[i] < a[j + i - S[i].back()])
                continue;
            if (a[i] > a[j + i - S[i].back()] || i - j + 1 < i - S[i].back() + 1 << 1)
                S[i].pop_back(); //S_p+s_i 不可能是有效后缀,删除 S_p+s_i
            S[i].push_back(j);
        }
        int Z = S[i][0];
        for (int j = 1; j < S[i].size(); ++j)
        {
            int T = S[i][j];
            int A = T + i - Z + 1, B = 1;
            if (z[A] >= i - A + 1)
            {
                A = 1, B += i - A + 1;
                if (z[B] >= Z - B || a[z[B] + 1] < a[B + z[B]])
                    Z = T;
            }
            else if (a[A + z[A]] < a[z[A] + 1])
                Z = T;
        }
        printf("%d ", Z);
    }
    return 0;
}

P5211 [ZJOI2017] 字符串(求区间 SS)

给一个字符串,支持两种操作:

  1. 区间加
  2. 区间求最小后缀
想一想,怎么做

考虑线段树,要求一个节点的最小后缀的话,需要知道右孩子的最小后缀,和左孩子后接右孩子的最小后缀,

也就是说需要维护每个点的 SS,你发现仍然可以用上一个题的维护方法,然后就做完了,,,

比较子串应该只能用哈希,如果线段树维护哈希的话会多一个 log,可以用 \(O(\sqrt n)\sim O(1)\) 的分块平衡一下。

#include <cstdio>
#include <vector>
#include <algorithm>
#define K 450
#define B 400000009
#define M1 1000000007
#define M2 1000000093
#define int long long
using namespace std;
int n, m, L[500], R[500], a[200050], b[200050];
struct H
{
    const int M;
    int s[500], t[500], a[200050], p[200050], sp[200050], h[200050];
    H(int M) : M(M) {}
    void I()
    {
        for (int i = p[0] = sp[0] = 1; i <= n; ++i)
            a[i] = ::a[i], p[i] = p[i - 1] * 233 % M, sp[i] = (sp[i - 1] + p[i]) % M;
        for (int i = 1; i <= b[n]; ++i)
        {
            h[L[i]] = a[L[i]];
            for (int j = L[i] + 1; j <= R[i]; ++j)
                h[j] = (h[j - 1] * 233 + a[j]) % M;
            s[i] = (s[i - 1] * p[R[i] - L[i] + 1] + h[R[i]]) % M;
        }
    }
    void _C(int l, int r, int x)
    {
        for (int i = l; i <= r; ++i)
            a[i] += x;
        h[L[b[l]]] = a[L[b[l]]];
        for (int j = L[b[l]] + 1; j <= R[b[l]]; ++j)
            h[j] = (h[j - 1] * 233 + a[j]) % M;
    }
    void C(int l, int r, int x)
    {
        if (b[l] == b[r])
        {
            _C(l, r, x);
            for (int i = 1; i <= b[n]; ++i)
                s[i] = (s[i - 1] * p[R[i] - L[i] + 1] + h[R[i]] + t[i] * sp[R[i] - L[i]]) % M;
            return;
        }
        _C(l, R[b[l]], x), _C(L[b[r]], r, x);
        for (int i = b[l] + 1; i < b[r]; ++i)
            t[i] = (t[i] + x + M) % M;
        for (int i = 1; i <= b[n]; ++i)
            s[i] = (s[i - 1] * p[R[i] - L[i] + 1] + h[R[i]] + t[i] * sp[R[i] - L[i]]) % M;
    }
    int Q(int x) { return (a[x] + t[b[x]]) % M; }
    int _Q(int l, int r) { return (h[r] + M - (l == L[b[l]] ? 0ll : h[l - 1] * p[r - l + 1] % M) + t[b[l]] * sp[r - l]) % M; }
    int Q(int l, int r)
    {
        if (b[l] == b[r])
            return _Q(l, r);
        int x = _Q(l, R[b[l]]);
        int y = (s[b[r] - 1] + M - s[b[l]] * p[R[b[r] - 1] - L[b[l] + 1] + 1] % M) % M;
        int z = _Q(L[b[r]], r);
        return (x * p[r - L[b[l] + 1] + 1] + y * p[r - L[b[r]] + 1] + z) % M;
    }
} X(M1), Y(M2);
int Q(int l1, int r1, int l2, int r2)
{
    int L = 1, R = min(r1 - l1 + 1, r2 - l2 + 1);
    while (L <= R)
    {
        int M = L + R >> 1;
        if (X.Q(l1, l1 + M - 1) == X.Q(l2, l2 + M - 1) && Y.Q(l1, l1 + M - 1) == Y.Q(l2, l2 + M - 1))
            L = M + 1;
        else
            R = M - 1;
    }
    if (R == r1 - l1 + 1 || R == r2 - l2 + 1)
    {
        if (r1 - l1 + 1 < r2 - l2 + 1)
            return -1;
        else if (r1 - l1 + 1 > r2 - l2 + 1)
            return 1;
        else
            return 0;
    }
    else
    {
        int x = X.Q(l1 + R), y = Y.Q(l2 + R);
        if (x < y)
            return -1;
        else
            return 1;
    }
}
struct V
{
    int s, t;
    vector<int> v;
    V operator+(V b)
    {
        V c;
        c.s = s, c.t = b.t, c.v = b.v;
        for (auto i : v)
        {
            int l = c.t - c.v.back() + 1, _ = Q(i, i + l - 1, c.v.back(), c.t);
            if (_ == 1)
                continue;
            if (_ == -1 || l << 1 > c.t - i + 1)
                c.v.pop_back();
            c.v.push_back(i);
        }
        return c;
    }
} O[200050 << 2];
void F(int s, int t, int p)
{
    if (s == t)
    {
        O[p].s = O[p].t = s, O[p].v.push_back(s);
        return;
    }
    int m = s + t >> 1;
    F(s, m, p << 1);
    F(m + 1, t, p << 1 | 1);
    O[p] = O[p << 1] + O[p << 1 | 1];
}
void M(int l, int r, int s, int t, int p)
{
    if (l <= s && t <= r)
        return;
    int m = s + t >> 1;
    if (l <= m)
        M(l, r, s, m, p << 1);
    if (r > m)
        M(l, r, m + 1, t, p << 1 | 1);
    O[p] = O[p << 1] + O[p << 1 | 1];
}
V Q(int l, int r, int s, int t, int p)
{
    if (l <= s && t <= r)
        return O[p];
    int m = s + t >> 1;
    if (l <= m && r > m)
        return Q(l, r, s, m, p << 1) + Q(l, r, m + 1, t, p << 1 | 1);
    if (l <= m)
        return Q(l, r, s, m, p << 1);
    else
        return Q(l, r, m + 1, t, p << 1 | 1);
}
signed main()
{
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", a + i), a[i] += 2e8, b[i] = (i - 1) / K + 1;
    for (int i = 1; i <= b[n]; ++i)
        L[i] = (i - 1) * K + 1, R[i] = min(L[i] + K - 1, n);
    X.I(), Y.I(), F(1, n, 1);
    for (int i = 0, o, l, r, x; i < m; ++i)
    {
        scanf("%lld%lld%lld", &o, &l, &r);
        if (o & 1)
            scanf("%lld", &x), X.C(l, r, x), Y.C(l, r, x), M(l, r, 1, n, 1);
        else
        {
            auto z = Q(l, r, 1, n, 1);
            int Z = z.v[0];
            for (auto j : z.v)
                if (Q(j, r, Z, r) == -1)
                    Z = j;
            printf("%lld\n", Z);
        }
    }
    return 0;
}

Lyndon 数组与 Runs

这是一级标题哦!跟上面的 Lyndon 分解就没什么关系了

防止你不知道周期是啥:若对于字符串 \(s\)\(s_i=s_{i+T}\),则 \(T\)\(s\) 的周期,若 \(T\mid|s|\),则 \(T\) 还是 \(s\) 的整周期

字符串 \(s\) 是本原串当且仅当 \(s\) 没有整周期。

字符串 \(s\)\(k(k\ge 2)\) 次方串当且仅当存在 \(w\) 使得 \(s=w^k\)\(2\) 次方串也叫平方串,

\(w\) 是本原串,则 \(w^k\) 是本原 \(k\) 次方串,所有 \(k\) 次方串统称幂串。

考虑找到一种结构,来刻画所有幂串。

首先,若 \(s\) 的最小周期为 \(p\)\(2p\le|s|\),则 \(s\) 的所有长度为 \(kp\) 的子串都是本原 \(k\) 次方串,

当然这刻画不出所有幂串,所以考虑对 \(s\) 的子串建立这种结构:

三元组 \(r=(i,j,p)\)\(s\) 的 run,当且仅当 \(2p\le j-i+1\),且 \(s\)\(i\)\(j\) 的子串是最小周期为 \(p\) 的极长子串,

定义其指数 \(e_r=\dfrac{j-i+1}p\)\(s\) 的所有 run 的集合记为 \(\text{Runs}(s)\)

引理 7:若 \((i,j,p)\) 是 run,则其可以导出 \(j-i+2-2p\) 个本原平方串,即 \([i,j]\) 中所有长度为 \(2p\) 的子串;

同时每个本原平方串都由唯一一个 run 导出。

前半部分可以由 run 的定义得到。

对于后半部分,首先一个本原平方串至少由一个 run 导出,即其向两边扩展得到的 run,

其次一个本原平方串不可能由超过一个 run 导出,因为 run 是极长的。

Runs 定理

这一节都是证这个定理……不过由证明过程可以得到一种求 Runs 的方法,建议别跳

Runs 定理:\(|\text{Runs}(s)|<|s|,\sum\limits_{i\in\text{Runs}(s)}e_i<3|s|\)

Runs 是关于周期的结构,是不是感觉和关于字典序的 Lyndon 一点关系都没有呢,所以证明之前需要先引入一些东西:

定义两种字典序 \(\prec_0,\prec_1\),满足对于字符 \(a,b\)\(a\prec_0 b\) 当且仅当 \(b\prec_1 a\),(不妨认为 \(\prec_0\) 是原来的字典序,\(\prec_1\) 是反过来)

注意对于字符串 \(a,b\),当 \(a\)\(b\) 的真前缀时,有 \(a\prec_0 b\)\(a\prec_1 b\)

\((i,j,p)\) 是 run,其长度为 \(p\) 的子串 \([u,u+p-1]\) 是其关于字典序 \(\prec\) 的 Lyndon 根,

当且仅当 \([u,u+p-1]\)\(\prec\) 意义下是 Lyndon 串。

可以发现 Lyndon 根就是周期的最小表示,所以 Lyndon 根是存在的,且每个 Lyndon 根都相等。

定义 Lyndon 数组 \(l_t(i)(t=0,1)\) 表示 \(\prec_t\) 意义下左端点为 \(i\) 的最长 Lyndon 串的右端点。

为了避免一些 corner case,令 \(\overline{s}=s+\$\)\(\$\)\(\prec_0\) 下小于其他所有字符,可以发现 runs 没有变化。

引理 8:\(\forall i\in[1,|s|]\)\(l_0(i)\)\(l_1(i)\) 中恰有一个为 \(i\)

证明:考虑 \(i\) 后第一个与 \(\overline{s}_i\) 不同的字符 \(\overline{s}_j\),若 \(\overline{s}_j\prec_0\overline{s}_i\),则 \(l_0(i)<j\)

\(l_0(i)\) 也不可能属于 \((i,j)\),否则 \([i,l_0(i)]\) 中字符全都相等,肯定不是 Lyndon 串,所以 \(l_0(i)\) 只能取 \(i\)。反之亦然。

引理 9:若 \((i,j,p)\) 是 run,设 \(\overline{s}_{j+1}\prec_t\overline{s}_{j-p+1}\),则对于其所有 Lyndon 根 \([u,u+p-1]\),有 \(l_t(u)=u+p-1\)

证明:首先根据定义 \(l_t(u)\ge u+p-1\)

设这个 Lyndon 根为 \(w\),则 \(\forall v\in[u+p,j]\)\([u,v]\) 形如 \(w^k+w'\),其中 \(w'\)\(w\) 的前缀,它们肯定不是 Lyndon 串,

又因为 \(\overline{s}_{j+1}\prec_t\overline{s}_{j-p+1}\),所以 \(\forall v>j\)\([u,v]\) 大于其以 \(v+p\) 开头的后缀,它们肯定不是 Lyndon 串,

所以 \(l_t(u)\) 只能取到 \(u+p-1\)

\((i,j,p)\) 是 run,其 Lyndon 根 \([u,u+p-1]\) 是真 Lyndon 根当且仅当 \(u>i\)\(j-i+1\ge 2p\),所以真 Lyndon 根存在。

引理 10:\(\forall u\in[1,|s|]\)\([u,l_0(u)]\)\([u,l_1(u)]\) 不可能都是某个 run 的真 Lyndon 根。

证明:根据引理 8,\(l_0(u)\)\(l_1(u)\) 中恰有一个为 \(u\),不妨设 \(l_0(u)=u,l_1(u)>u\)

考虑反证法,若 \([u,l_0(u)]\)\([u,l_1(u)]\) 都是真 Lyndon 根,则它们对应的 run 的周期分别为 \(1\)\(l_1(u)-u+1\)

\(\overline{s}_u=\overline{s}_{u-1}=\overline{s}_{l_1(u)}\),而 \(\overline{s}_u=\overline{s}_{l_1(u)}\)\([u,l_1(u)]\) 不可能是 Lyndon 串,

\([u,l_1(u)]\) 确实是 Lyndon 串,所以 \([u,l_0(u)]\)\([u,l_1(u)]\) 不可能都是某个 run 的真 Lyndon 根。

引理 11:两个不同 run 的真 Lyndon 根左端点集合不交。

证明:考虑反证法,设 \([u,v_1],[u,v_2]\) 是两个不同 run 的真 Lyndon 根,

由引理 9 得 \(v_1,v_2\) 都是 \(l_0(u),l_1(u)\) 之一,由引理 10 得 \(v_1,v_2\) 不能分别取 \(l_0(u),l_1(u)\)

所以 \(v_1=v_2\),这两个 run 相同,但这两个 run 不同,所以两个不同 run 的真 Lyndon 根左端点集合不交。

\(B(r)\) 表示 \(r\) 的真 Lyndon 根左端点集合,可以发现 \(B(r)\ge\lfloor e_r-1\rfloor>e_r-2\)。我们终于可以证明 Runs 定理了:

Runs 定理:\(|\text{Runs}(s)|<|s|,\sum\limits_{i\in\text{Runs}(s)}e_i<3|s|\)

证明:对于前半部分,\(|\text{Runs}(s)|\le\sum\limits_{i\in\text{Runs}(s)}|B(i)|<n\)

对于后半部分,\(\sum\limits_{i\in\text{Runs}(s)}e_i<\sum\limits_{i\in\text{Runs}(s)}(|B(i)|+2)=2|\text{Runs}(s)|+\sum\limits_{i\in\text{Runs}(s)}|B(i)|<3n\)

求 Runs(s)

法一

注意到 [NOI2016] 优秀的拆分 可以找出每种长度的周期能延伸到的极长区间,直接使用即可。

需要求 SA,常数比较大。

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
int n, z, d[1000050], o[2000050], k[1000050], c[1000050], P[1000050];
char s[1000050];
struct SA
{
    char s[1000050];
    int m = 127, a[1000050], r[2000050], h[20][1000050];
    inline bool B(int x, int y, int w) { return o[x] == o[y] && o[x + w] == o[y + w]; }
    void I()
    {
        memset(c, 0, sizeof c);
        n = strlen(s + 1);
        for (int i = 1; i <= n; ++i)
            ++c[r[i] = s[i]];
        for (int i = 1; i <= m; ++i)
            c[i] += c[i - 1];
        for (int i = n; i >= 1; --i)
            a[c[s[i]]--] = i;
        for (int w = 1, p = 0; p < n; m = p, w <<= 1)
        {
            p = 0;
            for (int i = n - w + 1; i <= n; ++i)
                d[++p] = i;
            for (int i = 1; i <= n; ++i)
                if (a[i] > w)
                    d[++p] = a[i] - w;
            memset(c, 0, sizeof c);
            for (int i = 1; i <= n; ++i)
                ++c[k[i] = r[d[i]]];
            for (int i = 1; i <= m; ++i)
                c[i] += c[i - 1];
            for (int i = n; i >= 1; --i)
                a[c[k[i]]--] = d[i];
            memcpy(o, r, sizeof o);
            p = 0;
            for (int i = 1; i <= n; ++i)
                r[a[i]] = B(a[i], a[i - 1], w) ? p : ++p;
            for (int i = 1, k = 0; i <= n; ++i)
            {
                k -= !!k;
                while (s[i + k] == s[a[r[i] - 1] + k])
                    ++k;
                h[0][r[i]] = k;
            }
            for (int j = 1; 1 << j <= n; ++j)
                for (int i = 1; i + (1 << j) - 1 <= n; ++i)
                    h[j][i] = min(h[j - 1][i], h[j - 1][i + (1 << j - 1)]);
        }
    }
    inline int Q(int x, int y)
    {
        x = r[x], y = r[y];
        if (x > y)
            swap(x, y);
        ++x;
        int k = __lg(y - x + 1);
        return min(h[k][x], h[k][y - (1 << k) + 1]);
    }
} X, Y;
struct S
{
    int a, b, c;
} Z[2000050];
bool B(S x, S y) { return x.a == y.a ? x.b == y.b ? x.c < y.c : x.b < y.b : x.a < y.a; }
int main()
{
    scanf("%s", s + 1), n = strlen(s + 1);
    memcpy(X.s, s, sizeof s), memcpy(Y.s, s, sizeof s), reverse(Y.s + 1, Y.s + n + 1);
    X.I(), Y.I();
    for (int p = 1; p << 1 <= n; ++p)
        for (int l = 1; l <= n; l += p)
        {
            int r = min(l + p - 1, n), l1 = 0, l2 = 0;
            if (r < n)
                l1 = X.Q(l, r + 1);
            if (l > 1)
                l2 = Y.Q(n - r + 1, n - l + 2);
            if (l1 + l2 >= p && P[l - l2] != r + l1)
                Z[++z] = {l - l2, r + l1, p}, P[l - l2] = r + l1;
        }
    sort(Z + 1, Z + z + 1, B);
    printf("%d\n", z);
    for (int i = 1; i <= z; ++i)
        printf("%d %d %d\n", Z[i].a, Z[i].b, Z[i].c);
    return 0;
}

蛙趣,咋过不了了

法二

首先求出所有 Lyndon 根,再像优秀的拆分那样向两边求 LCP 就可以求出所有 runs,

根据引理 9,Lyndon 根只能取 \([u,l_t(u)]\),所以求出 Lyndon 数组就可以求出所有 Lyndon 根,

根据《牛客 201916 简单字符串》Sol 2,\([u,l_t(u)]\) 就是以 \(u\) 开头的后缀 Lyndon 分解出的 \(w_1\),用那个题的做法求出即可。

#include <cstdio>
#include <cstring>
#include <algorithm>
#define int unsigned long long
using namespace std;
char a[1000050];
int n, m, s[1000050], p[1000050], h[1000050];
inline int Q(int l, int r) { return h[r] - h[l - 1] * p[r - l + 1]; }
inline int CP(int x, int y)
{
    int L = 1, R = min(n - x + 1, n - y + 1);
    while (L <= R)
    {
        int M = L + R >> 1;
        if (Q(x, x + M - 1) == Q(y, y + M - 1))
            L = M + 1;
        else
            R = M - 1;
    }
    return R;
}
inline int CS(int x, int y)
{
    int L = 1, R = min(x, y);
    while (L <= R)
    {
        int M = L + R >> 1;
        if (Q(x - M + 1, x) == Q(y - M + 1, y))
            L = M + 1;
        else
            R = M - 1;
    }
    return R;
}
inline int P(int x, int y, int o)
{
    int l = CP(x, y);
    if (l == min(n - x + 1, n - y + 1))
    {
        if (n - x + 1 < n - y + 1)
            return -1;
        else if (n - x + 1 > n - y + 1)
            return 1;
        else
            return 0;
    }
    else if (o == 0)
    {
        if (a[x + l] < a[y + l])
            return -1;
        else
            return 1;
    }
    else
    {
        if (a[x + l] > a[y + l])
            return -1;
        else
            return 1;
    }
}
struct R
{
    int l, r, p;
} z[2000050];
bool X(R x, R y) { return x.l == y.l ? x.r == y.r ? x.p < y.p : x.r < y.r : x.l < y.l; }
bool Y(R x, R y) { return x.l == y.l && x.r == y.r && x.p == y.p; }
signed main()
{
    scanf("%s", a + 1), n = strlen(a + 1);
    for (int i = p[0] = 1; i <= n; ++i)
        p[i] = p[i - 1] * 233, h[i] = h[i - 1] * 233 + a[i];
    for (int o = 0; o < 2; ++o)
        for (int i = n, l = 0; i >= 1; --i)
        {
            while (l && P(i, s[l], o) == -1)
                --l;
            int x = i, y = l ? s[l] - 1 : n, L = x - CS(x - 1, y), R = y + CP(x, y + 1);
            if (y - x + 1 << 1 <= R - L + 1)
                z[++m] = {x - CS(x - 1, y), y + CP(x, y + 1), y - x + 1};
            s[++l] = i;
        }
    sort(z + 1, z + m + 1, X), m = unique(z + 1, z + m + 1, Y) - z - 1;
    printf("%llu\n", m);
    for (int i = 1; i <= m; ++i)
        printf("%llu %llu %llu\n", z[i].l, z[i].r, z[i].p);
    return 0;
}

双模卡不过去,只能自然溢出了,,,

值得一题的是,如果能 \(O(n)\) 求 SA,\(O(n)\sim O(1)\) RMQ,那这个做法可以做到线性,

但优秀的拆分复杂度瓶颈在调和级数,无法线性。

posted @ 2024-10-30 17:04  Jijidawang  阅读(420)  评论(6)    收藏  举报