字符串杂记

字符串杂记

Hash

定义哈希函数:

\[H[1, i] = (H[1, i - 1] \times B + S[i]) \bmod M \]

则:

\[H[l, r] = (H[1, r] - H[1, l - 1] \times B^{r - l + 1}) \bmod M \]

若两个串的哈希值相同,基本可以认为两个串相同。

P4324 [JSOI2016] 扭动的回文串

给出两个长度为 \(n\) 的字符串 \(A, B\) ,求最长的扭动回文串长度。扭动回文串被定义为如下情况中的一种:

  • \(A\) 的一个回文串。
  • \(B\) 的一个回文串。
  • 一个回文串 \(S(i, j, k) = A[i, j] + B[j, k]\)

\(n \le 10^5\)

前两种情况直接枚举回文中心,二分回文串半径,哈希判断是否回文即可。

对于第三种情况,一个发现是一定是从一个 \(A\) 的中心扩展到尽量长,然后右边接上 \(B\)\(B\) 的中心也是类似的,仍然可以用哈希判断相等。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int base = 233, Mod = 1e9 + 7;
const int N = 1e5 + 7;

int pa1[N], pa2[N], pb1[N], pb2[N];
int prea[N], preb[N], sufa[N], sufb[N], pw[N];
char a[N], b[N];

int n;

inline int querypre(int *f, int l, int r) {
    return (f[r] - 1ll * f[l - 1] * pw[r - l + 1] % Mod + Mod) % Mod;
}

inline int querysuf(int *f, int l, int r) {
    return (f[l] - 1ll * f[r + 1] * pw[r - l + 1] % Mod + Mod) % Mod;
}

inline int getlen(int *pre, int *suf, int L, int R) {
    int l = 0, r = min(L, n - R + 1), len = 0;

    while (l <= r) {
        int mid = (l + r) >> 1;

        if (querypre(pre, L - mid + 1, L) == querysuf(suf, R, R + mid - 1))
            len = mid, l = mid + 1;
        else
            r = mid - 1;
    }

    return len;
}

signed main() {
    scanf("%d%s%s", &n, a + 1, b + 1);
    pw[0] = 1;

    for (int i = 1; i <= n; ++i)
        pw[i] = 1ll * pw[i - 1] * base % Mod;

    for (int i = 1; i <= n; ++i) {
        prea[i] = (1ll * prea[i - 1] * base + a[i]) % Mod;
        preb[i] = (1ll * preb[i - 1] * base + b[i]) % Mod;
    }

    for (int i = n; i; --i) {
        sufa[i] = (1ll * sufa[i + 1] * base + a[i]) % Mod;
        sufb[i] = (1ll * sufb[i + 1] * base + b[i]) % Mod;
    }

    int ans = 0;

    for (int i = 1; i <= n; ++i) { // 中心在 i
        int lena = getlen(prea, sufa, i, i), lenb = getlen(preb, sufb, i, i);
        ans = max(ans, (lena + getlen(prea, sufb, i - lena, i + lena - 1)) * 2 - 1);
        ans = max(ans, (lenb + getlen(prea, sufb, i - lenb + 1, i + lenb)) * 2 - 1);
    }

    for (int i = 1; i < n; ++i) { // 中心在 (i, i + 1)
        int lena = getlen(prea, sufa, i, i + 1), lenb = getlen(preb, sufb, i, i + 1);
        ans = max(ans, (lena + getlen(prea, sufb, i - lena, i + lena)) * 2);
        ans = max(ans, (lenb + getlen(prea, sufb, i - lenb + 1, i + lenb + 1)) * 2);
    }

    printf("%d", ans);
    return 0;
}

KMP

P3375 【模板】KMP字符串匹配

\(nxt_i\) 表示 \(S[1, i]\) 的最长公共前后缀长度,每次失配的时候就跳 \(nxt\) 指针直到匹配为止。

对于求 \(nxt\) ,考虑将 \(S\) 与自己做一个类似匹配的过程即可。

时间复杂度 \(O(n + m)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

int nxt[N];
char s[N], t[N];

int n, m;

signed main() {
    scanf("%s%s", s + 1, t + 1);
    n = strlen(s + 1), m = strlen(t + 1);

    for (int i = 2, j = 0; i <= m; ++i) {
        while (j && t[i] != t[j + 1])
            j = nxt[j];

        if (t[i] == t[j + 1])
            ++j;

        nxt[i] = j;
    }

    for (int i = 1, j = 0; i <= n; ++i) {
        while (j && s[i] != t[j + 1])
            j = nxt[j];

        if (s[i] == t[j + 1])
            ++j;

        if (j == m)
            printf("%d\n", i - m + 1), j = nxt[j];
    }

    for (int i = 1; i <= m; ++i)
        printf("%d ", nxt[i]);

    return 0;
}

HDU3336 Count the string

给定字符串 \(S\) ,求每个前缀在 \(S\) 中的出现次数和 \(\bmod (10^4 + 7)\)

\(n \le 2 \times 10^5\)

\(ans_i\) 表示 \(S[1, i]\) 的出现次数,先将所有 \(ans_i\) 置为 \(1\) 表示原本的位置。然后考虑其多次出现的情况,此时一定是作为一个前缀的后缀出现的,因此倒序处理,每次令 \(ans_{nxt_i} \gets ans_{nxt_i} + ans_i\) 即可。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e4 + 7;
const int N = 2e5 + 7;

int nxt[N], ans[N];
char str[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%s", &n, str + 1);

        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && str[j + 1] != str[i])
                j = nxt[j];

            if (str[j + 1] == str[i])
                ++j;

            nxt[i] = j;
        }

        fill(ans + 1, ans + n + 1, 1);
        int sum = 0;

        for (int i = n; i; --i)
            sum = (sum + ans[i]) % Mod, ans[nxt[i]] += ans[i];

        printf("%d\n", sum);
    }

    return 0;
}

P3435 [POI2006] OKR-Periods of Words

求给定字符串所有前缀的最大周期长度之和。

\(n \le 10^6\)

发现最大周期就是中间部分的循环节,为了让其最小,循环节就要最小,所以考虑进行跳 \(nxt\) 数组直到其后一个为 \(0\)

直接跳是 \(O(n^2)\) 的,做一个路径压缩即可做到 \(O(n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;

int nxt[N];
char str[N];

int n;

signed main() {
    scanf("%d%s", &n, str + 1);

    for (int i = 2, j = 0; i <= n; ++i) {
        while (j && str[j + 1] != str[i])
            j = nxt[j];

        if (str[j + 1] == str[i])
            ++j;

        nxt[i] = j;
    }

    ll ans = 0;

    for (int i = 2; i <= n; ++i) {
        while (nxt[nxt[i]])
            nxt[i] = nxt[nxt[i]];

        if (nxt[i])
            ans += i - nxt[i];
    }

    printf("%lld", ans);
    return 0;
}

P3193 [HNOI2008] GT考试

求有多少长度为 \(n\) 的字符串 \(S\) 不包含给定的长度为 \(m\) 的数字串 \(T\) ,答案对 \(k\) 取模。

\(n \le 10^9\)\(m \le 20\)\(k \le 1000\)

\(f_{i, j}\) 表示 \(S\) 匹配了 \(i\) 位且 \(T\) 匹配了 \(j\) 位的方案数,转移时不要 \(j = |T|\) 的状态即可。

考虑设 \(g_{i, j}\) 表示 \(T\) 已经匹配了 \(i\) 位,再加一个字符匹配长度变为 \(j\) 的方案数。则有:

\[f_{i + 1, j} = \sum f_{i, j} \times g_{j, k} \]

于是可以用矩阵快速幂优化,接下来考虑求 \(g\) ,枚举添加的字符不断跳 \(nxt\) 指针即可。

#include <bits/stdc++.h>
using namespace std;
const int M = 2e1 + 7;

int nxt[M];
char str[M];

int n, m, Mod;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

struct Matrix {
    int a[M][M];

    inline Matrix() {
        memset(a, 0, sizeof(a));
    }

    inline Matrix operator * (const Matrix &rhs) const {
        Matrix res;

        for (int i = 0; i < m; ++i)
            for (int j = 0; j < m; ++j)
                for (int k = 0; k < m; ++k)
                    res.a[i][k] = add(res.a[i][k], 1ll * a[i][j] * rhs.a[j][k] % Mod);

        return res;
    }

    inline Matrix operator ^ (int n) const {
        Matrix res;

        for (int i = 0; i < m; ++i)
            res.a[i][i] = 1;

        for (Matrix base = *this; n; base = base * base, n >>= 1)
            if (n & 1)
                res = res * base;

        return res;
    }
} g;

signed main() {
    scanf("%d%d%d%s", &n, &m, &Mod, str + 1);

    for (int i = 2, j = 0; i <= m; ++i) {
        while (j && str[j + 1] != str[i])
            j = nxt[j];

        if (str[j + 1] == str[i])
            ++j;

        nxt[i] = j;
    }

    for (int i = 0; i < m; ++i) {
        for (char c = '0'; c <= '9'; ++c) {
            int j = i;

            while (j && str[j + 1] != c)
                j = nxt[j];

            if (str[j + 1] == c)
                ++j;

            ++g.a[i][j];
        }
    }

    g = g ^ n;
    int ans = 0;

    for (int i = 0; i < m; ++i)
        ans = add(ans, g.a[0][i]);

    printf("%d", ans);
    return 0;
}

CF1286E Fedya the Potter Strikes Back

对于一个小写字符串 \(S\) ,若子串 \(S[l, r]\) 满足 \(S[l, r] = S[1, r - l + 1]\) ,则称其为好串。

对于长度为 \(n\) 的小写字符串 \(S[1, n]\) 与权值 \(w_{1 \sim n}\) ,定义子串 \(S[l, r]\) 的权值为 \(\min_{i = l}^r w_i\)

强制在线给出序列(每次在末尾加字符与权值),每次添加后求所有好串的权值和。

\(n \le 6 \times 10^5\)

先处理掉这个取 \(\min\) 的操作,不难使用 ST 表做到在线末端插入。

不难发现以 \(r\) 结尾的好串就是 \(S[1, r]\) 的 border 集合,因此考虑动态维护答案的增量,即每次统计当前串所有 border 的贡献。

先考虑动态维护整个串的 border 集合,从 \(i - 1 \to i\) 的时候实际上就是先去掉下一位不为 \(S_i\) 的 border,再将每个 border 右端点扩展一位,若 \(S_i = S_1\) 则还需加入长度为 \(1\) 的新 border。

考虑删除不合法的 border,对于每个 border,跳到最近的满足 \(S_{x + 1} = S_i\) 的祖先,并删去路径上的 border。由于每次只会新增一个 border,因此均摊复杂度是 \(O(n |\sum|)\) 的,这个过程可以路径压缩。

对于合法的 border,由于扩展,需要将原来的权值对 \(w_i\)\(\min\) 。直接用 map 存答案,每次将 \(> w_i\) 的数删除并加入等量 \(w_i\) 即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll B = 1e18;
const int N = 6e5 + 7, LOGN = 21;

struct bignum {
    ll x, y, z1, z2;

    inline bignum operator += (const ll &k) {
        x += (y += k) / B, y %= B, z1 = (z1 + k) % 26, z2 = (z2 + k) & ((1 << 30) - 1);
        return *this;
    }

    inline void writeln() {
        if (x)
            printf("%lld%018lld\n", x, y);
        else
            printf("%lld\n", y);
    }
} ans;

map<int, int> mp;

int anc[N][26], val[N], nxt[N];
char str[N];

ll res;
int n;

namespace ST {
int f[LOGN][N];

int n;

inline void emplace_back(int k) {
    f[0][++n] = k;

    for (int j = 1; j <= __lg(n); ++j)
        f[j][n] = min(f[j - 1][n], f[j - 1][n - (1 << (j - 1))]);
}

inline int query(int l, int r) {
    int k = __lg(r - l + 1);
    return min(f[k][r], f[k][l + (1 << k) - 1]);
}
} // namespace ST

inline void update(int x, int k) {
    mp[x] += k, res += 1ll * x * k;
}

inline void insert(int x) {
    int now = 0;

    for (auto it = mp.upper_bound(x); it != mp.end(); it = mp.erase(it))
        now += it->second, res -= 1ll * it->first * it->second;
    
    update(x, now);
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n;

    for (int i = 1, j = 0; i <= n; ++i) {
        cin >> str[i] >> val[i];
        str[i] = 'a' + (str[i] - 'a' + ans.z1) % 26, val[i] ^= ans.z2;
        ST::emplace_back(val[i]), ans += ST::query(1, i);

        if (i == 1) {
            ans.writeln();
            continue;
        }

        while (j && str[i] != str[j + 1])
            j = nxt[j];

        if (str[i] == str[j + 1])
            ++j;

        nxt[i] = j;

        if (str[1] == str[i])
            update(val[i], 1);

        for (int k = 0; k < 26; ++k)
            anc[i][k] = anc[nxt[i]][k];

        anc[i][str[nxt[i] + 1] - 'a'] = nxt[i];

        for (int k = 0; k < 26; ++k)
            if (str[i] != 'a' + k)
                for (int cur = anc[i - 1][k]; cur; cur = anc[cur][k])
                    update(ST::query(i - cur, i - 1), -1);

        insert(val[i]), (ans += res).writeln();
    }

    return 0;
}

P3502 [POI 2010] CHO-Hamsters

给定 \(n\) 个互不包含的字符串,求一个最短的字符串 \(S\) 使得这 \(n\) 个串在 \(S\) 中的出现次数总和 \(\ge m\) ,输出其长度。

\(n \le 200\)\(\sum |S_i| \le 10^5\)\(m \le 10^9\)

先求出 \(g_{i, j}\) 表示末尾为 \(S_i\) 时最少需要加的字符数量满足末尾为 \(S_j\) ,不难将 \(S_i\)\(S_j\) 拼在一起求 border ,该部分复杂度为 \(O(\sum_{i = 1}^n \sum_{j = 1}^n |S_i| + |S_j|) = O(n \sum |S_i|)\)

\(f_{i, j}\) 表示匹配了 \(i\) 次、末尾为 \(S_j\) 的最短长度,转移只要枚举下一个匹配即可,时间复杂度 \(O(n^2 m)\)

不难发现 \(n\) 很小而 \(m\) 很大,自然想到矩阵快速幂优化,时间复杂度 \(O(n^3 \log m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e2 + 7;

string str[N];

int n, m;

struct Matrix {
    ll a[N][N];

    inline Matrix() {
        memset(a, inf, sizeof(a));
    }

    inline Matrix operator * (const Matrix &rhs) {
        Matrix res;

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                for (int k = 1; k <= n; ++k)
                    res.a[i][k] = min(res.a[i][k], a[i][j] + rhs.a[j][k]);

        return res;
    }

    inline Matrix operator ^ (ll b) {
        Matrix res, base = *this;

        for (int i = 1; i <= n; ++i)
            res.a[i][i] = 0;

        for (; b; base = base * base, b >>= 1)
            if (b & 1)
                res = res * base;

        return res;
    }
} f;

inline int solve(int x, int y) {
    string s = " " + (x == y ? str[x] : str[y] + str[x]);
    vector<int> nxt(s.length());
    int n = s.length() - 1;

    for (int i = 2, j = 0; i <= n; ++i) {
        while (j && s[j + 1] != s[i])
            j = nxt[j];

        if (s[j + 1] == s[i])
            ++j;

        nxt[i] = j;
    }

    return str[y].length() - nxt[n];
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n >> m;

    for (int i = 1; i <= n; ++i)
        cin >> str[i];

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            f.a[i][j] = solve(i, j);

    f = f ^ (m - 1);

    ll ans = inf;

    for (int i = 1; i <= n; ++i)
        ans = min(ans, (int)str[i].length() + *min_element(f.a[i] + 1, f.a[i] + n + 1));

    printf("%lld", ans);
    return 0;
}

Manacher

P3805 【模板】manacher

\(p_i\) 表示以 \(i\) 为中心的最长回文子串的半径。

考虑维护目前 \(r\) 表示已经触及到的最右边的字符, \(mid\) 表示触及到 \(r\) 的回文串其中心。

  • \(i \le r\) ,则有 \(p_i \ge p_{mid \times 2 - i}\) ,因此令 \(p_i \gets \min(p[mid \times 2 - i], r - i + 1)\)
  • \(i > r\) ,令 \(p_i \gets 1\)

之后暴力扩展直到无法扩展为止,时间复杂度线性。

注意该算法只能求出长度为奇数的回文串,长度为偶数的回文串可以考虑在每个字符之间插入一个无关字符统计。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e7 + 7;

int p[N];
char s[N], str[N];

int n;

signed main() {
    scanf("%s", s + 1);

    for (int i = 1, len = strlen(s + 1); i <= len; ++i)
        str[++n] = '#', str[++n] = s[i];

    str[++n] = '#';

    for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
        p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);

        while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
            ++p[i];

        if (i + p[i] - 1 > r)
            mid = i, r = i + p[i] - 1;
    }

    printf("%d", *max_element(p + 1, p + n + 1) - 1);
    return 0;
}

P4555 [国家集训队] 最长双回文串

\(S\) 中形如 \(AB\) 的最长子串长度,其中 \(A, B\) 为两个回文串。

\(n \le 10^5\)

考虑求出每个位置开头和结尾的最长回文串长度 \(R_i, L_i\) 。对于每个位置为中心的最长回文串,先更新回文串两端的 \(L, R\) 。对于剩下的 \(L, R\) ,一定可以从相隔两个位置的 \(L, R\) 推过来。于是可以做到线性。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;

int p[N], L[N], R[N];
char s[N], str[N];

int n;

signed main() {
    scanf("%s", s + 1);

    for (int i = 1, len = strlen(s + 1); i <= len; ++i)
        str[++n] = '#', str[++n] = s[i];

    str[++n] = '#';

    for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
        p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);

        while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
            ++p[i];

        if (i + p[i] - 1 > r)
            mid = i, r = i + p[i] - 1;

        L[i + p[i] - 1] = max(L[i + p[i] - 1], p[i] - 1);
        R[i - p[i] + 1] = max(R[i - p[i] + 1], p[i] - 1);
    }

    for (int i = n - 2; i; --i)
        L[i] = max(L[i], L[i + 2] - 2);

    for (int i = 3; i <= n; ++i)
        R[i] = max(R[i], R[i - 2] - 2);

    int ans = 0;

    for (int i = 1; i <= n; i += 2)
        if (L[i] && R[i])
            ans = max(ans, L[i] + R[i]);

    printf("%d", ans);
    return 0;
}

P4199 万径人踪灭

求字符集为 \(\{ \text{a}, \text{b} \}\) 的串 \(S\) 中有多少子序列满足:

  • 位置和字符都关于某条对称轴对称。
  • 不能是连续的一段。

\(n\le 10^5\)

答案等于“位置对称的回文子序列数”减去“回文子串数”,后者不难用 Manacher 解决。

\(i\) 为位置对称的回文子序列的中心,共有 \(cnt\)\(j \ge 0\) 满足 \(S[i - j] = S[i + j]\) ,则中心 \(i\) 的贡献为 \(2^j - 1\)

考虑统计 \(S[i - j] = S[i + j] = \text{a}\) 的数量,设多项式 \(A_i = [S[i] = \text{a}]\) ,将 \(A\) 与自己卷积后,\(A_{2i}\) 的值就是中心为 \(i\)\(S[i - j] = S[i + j] = \text{a}\) 的数量。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e5 + 7;

int p[N], a[N], b[N], pw[N];
char s[N], str[N];

int len, n;

namespace Poly {
#define cpy(a, b, n) memcpy(a, b, sizeof(int) * n)
#define clr(a, n) memset(a, 0, sizeof(int) * n)
const double Pi = acos(-1);
const int S = 2e6 + 7;

int rev[S];

inline void calrev(int n) {
    for (int i = 0; i < n; ++i)
        rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? n >> 1 : 0);
}

inline int calc(int n) {
    int len = 1;

    while (len <= n)
        len <<= 1;

    return calrev(len), len;
}

inline void FFT(complex<double> *f, int n, int op) {
    for (int i = 0; i < n; ++i)
        if (i < rev[i])
            swap(f[i], f[rev[i]]);

    for (int k = 1; k < n; k <<= 1) {
        complex<double> tG(cos(Pi / k), sin(Pi / k) * op);

        for (int i = 0; i < n; i += k << 1) {
            complex<double> buf = 1;

            for (int j = 0; j < k; ++j) {
                complex<double> fl = f[i + j], fr = buf * f[i + j + k];
                f[i + j] = fl + fr, f[i + j + k] = fl - fr;
                buf *= tG;
            }
        }
    }

    if (op == -1) {
        for (int i = 0; i < n; ++i)
            f[i] /= n;
    }
}

inline void Mul(int *f, int n) {
    static complex<double> a[S];
    int len = calc(n * 2 - 1);

    for (int i = 0; i < n; ++i)
        a[i] = f[i];

    for (int i = n; i < len; ++i)
        a[i] = 0;

    FFT(a, len, 1);

    for (int i = 0; i < len; ++i)
        a[i] *= a[i];

    FFT(a, len, -1);

    for (int i = 0; i < n * 2 - 1; ++i)
        f[i] = round(a[i].real());
}

#undef cpy
#undef clr
} // namespace Poly

signed main() {
    scanf("%s", s + 1);
    len = strlen(s + 1);

    for (int i = 1; i <= len; ++i)
        str[++n] = '#', str[++n] = s[i];

    str[++n] = '#';

    for (int i = 1, mid = 0, r = 0; i <= n; ++i) {
        p[i] = (i <= r ? min(p[mid * 2 - i], r - i + 1) : 1);

        while (i - p[i] && i + p[i] <= n && str[i - p[i]] == str[i + p[i]])
            ++p[i];

        if (i + p[i] - 1 > r)
            mid = i, r = i + p[i] - 1;
    }

    for (int i = 1; i <= len; ++i)
        a[i] = (s[i] == 'a'), b[i] = (s[i] == 'b');

    Poly::Mul(a, len + 1), Poly::Mul(b, len + 1);

    pw[0] = 1;

    for (int i = 1; i <= len; ++i)
        pw[i] = 2ll * pw[i - 1] % Mod;

    int ans = 0;

    for (int i = 1; i <= len * 2; ++i)
        ans = (ans + pw[(a[i] + b[i] + (~i & 1)) / 2] - 1) % Mod;

    for (int i = 1; i <= n; ++i)
        ans = (ans - p[i] / 2) % Mod;
    
    printf("%d", (ans + Mod) % Mod);
    return 0;
}

Z-function

P5410 【模板】扩展 KMP/exKMP(Z 函数)

\(z_i\) 表示 \(S\)\(S[i, n]\) 的 LCP 的长度。

定义 \(z_0 = 0\)\(z_1 = |S|\) 。称区间 \([i, i + z[i] - 1]\)\(i\) 的匹配段(Z-box)。

考虑维护右端点最靠右的匹配段,记作 \([l, r]\)

  • \(i \le r\) ,则有 \(s[i, r] = s[i - l, r - l]\) ,因此令 \(z_i = \min(z_{i - l + 1}, r - i + 1)\)
  • \(i > r\) ,令 \(z_i = 0\)

之后暴力枚举下一位匹配到无法拓展为止,时间复杂度线性。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e7 + 7;

int z[N];
char a[N], b[N], str[N];

int lena, lenb, n;

signed main() {
    scanf("%s%s", a + 1, b + 1);
    lena = strlen(a + 1), lenb = strlen(b + 1);
    memcpy(str + 1, b + 1, sizeof(char) * lenb);
    str[lenb + 1] = '#';
    memcpy(str + lenb + 2, a + 1, sizeof(char) * lena);
    z[1] = n = lena + lenb + 1;

    for (int i = 2, l = 0, r = 0; i <= n; ++i) {
        z[i] = (i <= r ? min(z[i - l + 1], r - i + 1) : 0);
        
        while (i + z[i] <= n && str[i + z[i]] == str[1 + z[i]])
            ++z[i];

        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }

    ll ans = 0;

    for (int i = 1; i <= lenb; ++i)
        ans ^= 1ll * i * (min(z[i], lenb) + 1);

    printf("%lld\n", ans), ans = 0;

    for (int i = 1; i <= lena; ++i)
        ans ^= 1ll * i * (z[lenb + 1 + i] + 1);

    printf("%lld", ans);
    return 0;
}

P7114 [NOIP2020] 字符串匹配

\(S = (AB)^i C\) 的拆分方案数,其中 \(A, B, C\) 均非空,且 \(F(A) \le F(C)\) ,其中 \(F(S)\) 表示 \(S\) 中出现奇数次的字符的数量。

\(|S| \le 2^{20}\)

先不考虑奇偶的限制,枚举 \(AB\) 的长度 \(i\) ,并计算有多少前缀可以被表示成 \((AB)^k\) 的形式,明显此前缀的长度为 \(ik\)

根据循环节经典理论,有 \(S[1, i(k - 1)] = S[i + 1, ik]\) 。求出 \(S\) 的 Z 函数,则 \(i(k - 1) \le z_{i + 1}\) 。又因为 \(C\) 非空,于是 \(k \le t = \min(\frac{z_{i + 1}}{i} + 1, \frac{n - 1}{i})\)

再考虑奇偶的限制,定义权值为某个串中出现奇数次的字符数量。

  • \(k\) 是奇数时, \(C\) 的权值等价于 \(s[i + 1 : n]\) 的权值。
  • \(k\) 是偶数时, \(C\) 的权值等价于 \(s[1 : n]\) 的权值。

对于合法 \(A\) 的数量,发现 \(A\)\(S\) 的一个前缀,用桶进行维护所有前缀中各权值即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1 << 20 | 1;

int z[N];
char str[N];

int n;

inline void exKMP() {
    z[1] = n;

    for (int i = 2, l = 0, r = 0; i <= n; ++i) {
        z[i] = (i <= r ? min(z[i - l + 1], r - i + 1) : 0);

        while (i + z[i] <= n && str[i + z[i]] == str[1 + z[i]])
            ++z[i];

        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%s", str + 1);
        n = strlen(str + 1);
        exKMP();
        vector<int> pre(26), suf(26), sum(27);

        for (int i = 1; i <= n; ++i)
            suf[str[i] - 'a'] ^= 1;

        ll ans = 0;
        int valn = count(suf.begin(), suf.end(), 1), sufval = valn, preval = 0, res1 = 0, res2 = 0;

        for (int i = 1; i < n; ++i) {
            res1 += (suf[str[i] - 'a'] ? -sum[sufval--] : sum[++sufval]), suf[str[i] - 'a'] ^= 1;
            int t = min(z[i + 1] / i + 1, (n - 1) / i);
            ans += 1ll * (t + 1) / 2 * res1;
            preval += (pre[str[i] - 'a'] ? -1 : 1), pre[str[i] - 'a'] ^= 1;
            ++sum[preval], res1 += (preval <= sufval);
            ans += 1ll * t / 2 * res2, res2 += (preval <= valn);
        }

        printf("%lld\n", ans);
    }

    return 0;
}

CF1909G Pumping Lemma

给定长度为 \(n\) 的字符串 \(S\) 和长度为 \(m\) 的字符串 \(T\) ,求满足以下条件的字符串三元组 \((x, y, z)\) 的数量:

  • \(S = x + y + z\)
  • \(T = x + y^k + z\)

\(n, m \le 10^7\)

先求出 \(l = \mathrm{LCS}(S, T)\) ,由于后面的 \(y + z\) 部分一定属于 \(\mathrm{LCS}(S, T)\) ,因此 \(S[1, n - l]\)\(T[1, n - l]\) 一定是 \(x\) 的一段前缀,可以删去,若 \(S[1, n - l] \ne T[1, n - l]\) 则无解。

此时 \(S\)\(T\) 的一段后缀,对比 \(S\)\(T\) 剩下的结构可以发现 \(x + y^k\) 具有周期 \(|y|\)

由于 \(n = |x| + |y| + |z|, m = |x| + k |y| + |z|\) ,因此 \(|y|\)\(m - n\) 的因子,考虑枚举 \(|y|\) 统计有多少合法的 \((x, y, z)\)

\(l = \mathrm{LCP}(T, T[|y| + 1, m])\) ,那么根据 border 理论 \(T[1, l + |y|]\) 具有周期 \(|y|\) ,且 \(x + y^k\) 是其前缀。

考虑 \(|x|\) 的取值范围,需要满足 \(|x| + |y^k| \le l + |y|\) ,由于 \(|y^k| = |y| + m - n\) ,因此合法 \(x\) 的数量不难算出。

固定了 \(x, y\) 后合法的 \(z\) 是一定的,用 Z-function 求出 \(T\) 每个后缀与 \(T\) 的 LCP,时间复杂度 \(O(n + m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e7 + 7;

int z[N];
char s[N], t[N];

int n, m;

signed main() {
    scanf("%d%d%s%s", &n, &m, s + 1, t + 1);
    int lcs = 0;

    while (lcs < n && s[n - lcs] == t[m - lcs])
        ++lcs;

    for (int i = 1; i <= n - lcs; ++i)
        if (s[i] != t[i])
            return puts("0"), 0;

    for (int i = n - lcs + 1; i <= n; ++i)
        s[i - (n - lcs)] = s[i];

    for (int i = n - lcs + 1; i <= m; ++i)
        t[i - (n - lcs)] = t[i];
    
    m -= n - lcs, n = lcs;
    z[1] = m;
    
    for (int i = 2, l = 0, r = 0; i <= m; ++i) {
        z[i] = (i <= r ? min(z[i - l + 1], r - i + 1) : 0);
        
        while (i + z[i] <= m && t[i + z[i]] == t[1 + z[i]])
            ++z[i];
        
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }

    ll ans = 0;
    
    for (int i = 1; i <= m; ++i)
        if (!((m - n) % i))
            ans += max(0, z[i + 1] - (m - n) + 1);
    
    printf("%lld", ans);
    return 0;
}

周期与 Border 理论

定义:

  • \(p\)\(S\) 的周期当且仅当 \(p \le |S|\)\(S[i] = S[i + p] (i \in [1, |S| - p])\)
  • \(p\)\(S\) 的 Border 当且仅当 \(p < |S|\)\(S[1, p] = S[|S| - p + 1, |S|]\) ,也称 \(S[1, p]\)\(S\) 的 border。

基本结论:\(p\)\(S\) 的周期当且仅当 \(|S| - p\)\(S\) 的 border。

周期相关

  • 弱周期引理(Weak Periodicity Lemma):若 \(p, q\)\(S\) 的周期,且 \(p + q \le |S|\) ,则 \(\gcd(p, q)\) 也是 \(S\) 的周期。

    证明:\(p = q\) 显然,不妨设 \(p < q\)

    • 对于 \(i > q\) ,有 \(S[i] = S[i - q] = S[i - q + p]\)

    • 对于 \(q - p < i \le q\)\(i + p \le |S|\) ,因此 \(S[i] = S[i + p] = S[i - q + p]\)

    \(S[i] = S[i - q + p] (i \in [q - p + 1, |S|])\) ,即 \(q - p\)\(S\) 的周期。而 \((q - p) + p \le |S|\) ,模拟欧几里得算法过程即可得证。

  • 周期引理(Periodicity Lemma):若 \(p, q\)\(S\) 的周期,且 \(p + q - \gcd(p, q) \le |S|\) ,则 \(\gcd(p, q)\) 也是 \(S\) 的周期。

  • 短周期结构:\(S\) 的所有不超过 \(\frac{|S|}{2}\) 的周期都是其最短周期的倍数。

  • 字符串周期结构: \(S\) 的所有周期可以形成 \(O(\log n)\) 个值域不交的等差数列。

Border 相关

  • 长 border 结构:\(S\) 所有 \(\ge \frac{|S|}{2}\) 的 border 构成等差数列,且如果排序后延申这个数列,下一项就是 \(|S|\)

    证明:设 \(|S| - p\)\(S\) 最长的 Border,另一个 border 长度为 \(|S| - q\) ,且 \(p, q \le \frac{|S|}{2}\)

    由弱周期引理得到 \(\gcd(p, q)\) 也是 \(S\) 的周期,因此存在长度为 \(|S| - \gcd(p, q)\) 的 border 。又因为 \(|S| - p\) 是最长的 border,因此 \(\gcd(p, q) \ge p\) ,即 \(p \mid q\) ,故 \(S\) 所有 \(\ge \frac{|S|}{2}\) 的 border 构成公差为 \(p\) 的等差数列。

  • 字符串 border 结构:\(S\) 的所有 border 形成至多 \(\lceil \log_2 S \rceil\) 个值域不交的等差数列。

    证明:下面不妨钦定 border 可以为 \(|S|\) ,显然加入该 border 后等差数列数量不小于原问题。

    \(S\) 最长的 border 为 \(b_0 = |S|\) ,则长度 \(\ge \frac{b_0}{2}\) 的 border 与 \(b_0\) 构成等差数列。

    再设最长的 \(< \frac{b_0}{2}\) 的 border 为 \(b_1\) ,同理长度在 \([\frac{b_1}{2}, b_1]\) 间的 border 构成一个等差数列。

    由于 \(b_i < \frac{b_{i - 1}}{2}\) ,因此得证。

  • 推论:\(S\) 公差 \(\ge d\) 的 Border 的等差数列总大小是 \(O(\frac{n}{d})\) 的。

应用

P4156 [WC2016] 论战捆竹竿

给定字符串 \(S\) ,现有一空串 \(T\) ,每次可以将 \(S\) 接在后面,可以将一段相等的前后缀重叠,求 \(T\) 的长度可以是 \([n, w]\) 中的多少个数。

\(n \le 5 \times 10^5\)\(w \le 10^{18}\)

可以重叠转化为将 \(S\) 去掉一个 border 接在后面,即将 \(S\) 的一个周期接在后面。

将题意转化为 \(\sum_{i = 1}^m a_i x_i\)\([0, w - n]\) 中能取到的值的个数,其中 \(m\)\(|S|\) 的周期数量,\(a_i\) 为周期长度。

考虑同余最短路模型,暴力跑最短路是 \(O(n^2)\) 的。

考虑字符串 border 结构,将等差数列放在一起跑最短路。对于首项为 \(a\) ,公差为 \(d\) ,项数为 \(k\) 的等差数列,若当前已经得到之前的等差数列所求出的 \(\bmod a\) 意义下的 \(dis\) ,考虑如何快速用一个等差数列更新该数组。

此时这个等差数列会将 \([0, a - 1]\) 的元素划分为 \(\gcd(a, d)\) 个不交的环。对于一个环,找到 \(dis\) 最小的位置 \(p\) ,将环展开并认为 \(p\) 是第一个元素,对于 \(q \le p + k\) ,则 \(dis_p + (q - p) \times b \to dis_q\) ,显然可以用单调队列实现。

接下来处理求解完当前等差数列后,跳转到下一个等差数列的问题。若原先模数为 \(d_1\) ,现在模数为 \(d_2\) ,首先有 \(dis_x \to dis_{dis_x \bmod d_2}\) 。然后从 \(dis\) 最小的点开始在环上走,每走一步就加上 \(d_1\) ,然后与当前 \(dis\)\(\min\) 即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7;

ll dis[N];
int bd[N];
char str[N];

ll w;
int n, m;

inline void change(int mod) {
    vector<ll> d(dis, dis + m);
    memset(dis, 0x3f, sizeof(ll) * mod);

    for (int i = 0; i < m; ++i)
        dis[d[i] % mod] = min(dis[d[i] % mod], d[i]);

    for (int i = 0, g = __gcd(mod, m); i < g; ++i) {
        vector<int> id = {i};

        for (int x = (i + m) % mod; x != i; x = (x + m) % mod)
            id.emplace_back(x);

        rotate(id.begin(), min_element(id.begin(), id.end(), [](const int &a, const int &b) {
            return dis[a] < dis[b];
        }), id.end());

        for (int j = 1; j < id.size(); ++j)
            dis[id[j]] = min(dis[id[j]], dis[id[j - 1]] + m);
    }

    m = mod;
}

inline void solve(int fir, int d, int k) {
    for (int i = 0, g = __gcd(fir, d); i < g; ++i) {
        vector<int> id = {i};

        for (int x = (i + d) % fir; x != i; x = (x + d) % fir)
            id.emplace_back(x);

        rotate(id.begin(), min_element(id.begin(), id.end(), [](const int &a, const int &b) {
            return dis[a] < dis[b];
        }), id.end());

        deque<pair<int, ll> > q = {make_pair(0, dis[id[0]])};

        for (int j = 1; j < id.size(); ++j) {
            while (!q.empty() && q.front().first < j - k + 1)
                q.pop_front();

            if (!q.empty())
                dis[id[j]] = min(dis[id[j]], q.front().second + 1ll * j * d + fir);

            while (!q.empty() && q.back().second >= dis[id[j]] - 1ll * j * d)
                q.pop_back();

            q.emplace_back(j, dis[id[j]] - 1ll * j * d);
        }
    }
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%lld%s", &n, &w, str + 1);

        if (w < n) {
            puts("0");
            continue;
        }

        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && str[i] != str[j + 1])
                j = bd[j];

            if (str[i] == str[j + 1])
                ++j;

            bd[i] = j;
        }

        vector<int> cyc;

        for (int x = bd[n]; x; x = bd[x])
            cyc.emplace_back(n - x);

        cyc.emplace_back(n), w -= n, m = n;
        memset(dis, 0x3f, sizeof(ll) * m), dis[0] = 0;

        for (int i = 0, j = 0; i + 1 < cyc.size(); i = j) {
            while (cyc[j + 1] - cyc[j] == cyc[i + 1] - cyc[i])
                ++j;

            change(cyc[i]), solve(cyc[i], cyc[i + 1] - cyc[i], j - i);
        }

        ll ans = 0;

        for (int i = 0; i < m; ++i)
            if (dis[i] <= w)
                ans += (w - dis[i]) / m + 1;

        printf("%lld\n", ans);
    }

    return 0;
}

失配树

定义:建立一个 \(n + 1\) 个点的图,编号为 \(0 \sim n\) ,对于 \(i \ge 1\)\(i\)\(fail_i\) 连边,这构成了一棵树,称为失配树或 Fail 树。

\(fail_i\)\(i\) 的失配指针,那么一个前缀的所有 border 即它在失配树上的所有祖先。

P5829 【模板】失配树

给出字符串 \(S\)\(m\) 次询问两个前缀的共同真 border。

\(|S| \le 10^6\)

两个前缀的共同真 border 就是树上的 LCA,若构成祖先关系还要向上跳一次。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, LOGN = 21;

int fa[N][LOGN], dep[N];
char str[N];

int n, m;

signed main() {
    scanf("%s", str + 1);
    n = strlen(str + 1);
    fa[0][0] = 0, dep[0] = 0;
    fa[1][0] = 0, dep[1] = 1;

    for (int i = 2, j = 0; i <= n; ++i) {
        while (j && str[j + 1] != str[i])
            j = fa[j][0];

        if (str[j + 1] == str[i])
            ++j;

        dep[i] = dep[fa[i][0] = j] + 1;
    }

    for (int j = 1; j < LOGN; ++j)
        for (int i = 1; i <= n; ++i)
            fa[i][j] = fa[fa[i][j - 1]][j - 1];

    scanf("%d", &m);

    while (m--) {
        int x, y;
        scanf("%d%d", &x, &y);

        if (dep[x] < dep[y])
            swap(x, y);

        for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
            if (h & 1)
                x = fa[x][i];

        for (int i = LOGN - 1; ~i; --i)
            if (fa[x][i] != fa[y][i])
                x = fa[x][i], y = fa[y][i];

        printf("%d\n", fa[x][0]);
    }

    return 0;
}

P2375 [NOI2014] 动物园

给出串 \(S\) ,对于所有前缀 \(S[1, i]\) 求其长度不超过 \(\frac{i}{2}\) 的 Border 数量。

\(|S| \le 10^6\)

求出 fail 指针后,考虑从后往前遍历前缀,这样 \(\frac{i}{2}\) 就是递减的,因此限制越来越紧。

若一个 Border 的长度大于 \(\frac{i}{2}\) ,则在之后往前跳的过程中一定也不合法,于是只要每次路径压缩即可做到时间复杂度线性。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;

int fail[N], len[N];
char str[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%s", str + 1);
        n = strlen(str + 1);
        len[1] = 1;

        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && str[j + 1] != str[i])
                j = fail[j];

            if (str[j + 1] == str[i])
                ++j;

            fail[i] = j, len[i] = len[fail[i]] + 1;
        }

        int ans = 1;

        for (int i = n; i; --i) {
            vector<int> vec;

            while (fail[i] > i / 2)
                vec.emplace_back(fail[i]), fail[i] = fail[fail[i]];

            ans = 1ll * ans * (len[fail[i]] + 1) % Mod;

            for (int it : vec)
                fail[it] = fail[i];
        }

        printf("%d\n", ans);
    }

    return 0;
}

序列自动机

序列自动机(Subsequence automaton)是接受且仅接受一个字符串的子序列的自动机。

序列自动机一共有 \(|S| + 1\) 个状态,每个状态表示一个子序列 \(T\) 第一次在 \(S\) 出现时的末尾位置。

转移:每次转移到字符 \(c\) 下一次出现的位置,即:

\[\delta(u, c) = \min \{ i | i > u, S[i] = c \} \]

实现

字符集较小

从后往前倒序枚举 \(i\) ,维护 \(to_{i, c}\) 。枚举时维护一个 \(las_c\) 表示 \(c\) 在当前后缀中出现最前的位置,每次更新时令 \(to_i \leftarrow las_c\) ,再令 \(las_{S[i]} = i\) 即可。

namespace SeqAM {
int nxt[N][S];

inline void build(char *str, int len) {
    memset(nxt[len], -1, sizeof(nxt[len]));

    for (int i = len; i; --i) {
        memcpy(nxt[i - 1], nxt[i], sizeof(nxt[i]));
        nxt[i - 1][str[i] - 'a'] = i;
    }
}

inline bool query(char *str, int len) {
    int u = 0;

    for (int i = 1; i <= n; ++i) {
        u = nxt[u][str[i] - 'a'];

        if (u == -1)
            return false;
    }

    return true;
}
} // namespace SeqAM

字符集较大

考虑用主席树维护,\(i\) 代表的线段树就是 \(to_i\) 。每次都是单点修改一个 \(las\) ,所以时间复杂度是 \(O(n \log |\sum|)\) 的。

P5826 【模板】子序列自动机

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

int a[N], b[N];

int testid, n, q, m;

namespace SeqAM {
namespace SMT {
const int SIZE = N << 5;

int lc[SIZE], rc[SIZE], val[SIZE];
int rt[N];

int tot;

int update(int x, int nl, int nr, int pos, int k) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x];

    if (nl == nr)
        return val[y] = k, y;

    int mid = (nl + nr) >> 1;

    if (pos <= mid)
        lc[y] = update(lc[x], nl, mid, pos, k);
    else
        rc[y] = update(rc[x], mid + 1, nr, pos, k);

    return y;
}

int query(int x, int nl, int nr, int pos) {
    if (!x)
        return -1;

    if (nl == nr)
        return val[x];

    int mid = (nl + nr) >> 1;
    return pos <= mid ? query(lc[x], nl, mid, pos) : query(rc[x], mid + 1, nr, pos);
}
} // namespace SMT

inline void build(int *str, int len) {
    for (int i = n; i; --i)
        SMT::rt[i - 1] = SMT::update(SMT::rt[i], 1, m, str[i], i);
}

inline bool query(int *str, int len) {
    int u = 0;

    for (int i = 1; i <= len; ++i) {
        u = SMT::query(SMT::rt[u], 1, m, str[i]);

        if (u == -1)
            return false;
    }

    return true;
}
} // namespace SeqAM

signed main() {
    scanf("%d%d%d%d", &testid, &n, &q, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    SeqAM::build(a, n);

    while (q--) {
        int len;
        scanf("%d", &len);

        for (int i = 1; i <= len; ++i)
            scanf("%d", b + i);

        puts(SeqAM::query(b, len) ? "Yes" : "No");
    }

    return 0;
}

扩展

可以用一种更简洁的方法构建自动机。

给每一个字符开一个 vector,存储着这个字符出现的所有下标。每次查询 \(to_{i, c}\) ,就是在 \(c\) 对应的 vector 里面二分出第一个 \(\ge i\) 的下标即可。

应用

P3856 [TJOI2008] 公共子串

求三个串的不同公共子序列数量。

\(n \le 100\)

\(f_{x, y, z}\) 表示在第一个串以 \(x\) 开始、第二个串以 \(y\) 开始、第三个串以 \(z\) 开始的公共子序列数量,不难记忆化搜索实现,转移时枚举序列自动机上的公共边即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e2 + 7, S = 27;

struct SeqAM {
    int nxt[N][S];

    inline void build(char *str, int len) {
        memset(nxt[len], -1, sizeof(nxt[len]));

        for (int i = len; i; --i) {
            memcpy(nxt[i - 1], nxt[i], sizeof(nxt[i]));
            nxt[i - 1][str[i] - 'a'] = i;
        }
    }
} A, B, C;

ll f[N][N][N];
char a[N], b[N], c[N];

ll dfs(int x, int y, int z) {
    if (~f[x][y][z])
        return f[x][y][z];

    f[x][y][z] = (x || y || z);

    for (int i = 0; i < S; ++i)
        if (~A.nxt[x][i] && ~B.nxt[y][i] && ~C.nxt[z][i])
            f[x][y][z] += dfs(A.nxt[x][i], B.nxt[y][i], C.nxt[z][i]);

    return f[x][y][z];
}

signed main() {
    scanf("%s%s%s", a + 1, b + 1, c + 1);
    A.build(a, strlen(a + 1)), B.build(b, strlen(b + 1)), C.build(c, strlen(c + 1));
    memset(f, -1, sizeof(f));
    printf("%lld", dfs(0, 0, 0));
    return 0;
}

Lyndon 分解

定义:

  • Lyndon Word:对于字符串 \(S\) ,若 \(S\) 的字典序是所有后缀中最小的,则称 \(S\) 为 Lyndon Word。
  • Lyndon 分解:将字符串分解为 \(S = w_1 w_2 \cdots w_k\) ,其中每个 \(w_i\) 均为 Lyndon Word,且 \(w_1 \ge w_2 \ge \cdots \ge w_k\)

性质

若两个字符串 \(u,v\) 为 Lyndon Word 并且 \(u<v\),则 \(uv\) 为 Lyndon Word 。

若字符串 \(S\) 与字符 \(c\) 满足 \(Sc\) 是 Lyndon Word,则对于字符 \(d > c\)\(Sd\) 是 Lyndon Word 。

设有两个字符串 \(S_1, S_2\),其中 \(S_1\) 是 Lyndon Word 并且 \(S_1 > S_2\) ,则 \(S_1 > S_2^2\)

\(S_2\) 不为 \(S_1\) 后缀时显然成立,否则有 \(S_1 > S_1[1, |S_2|] > S_2\)

Lyndon Word 不存在 border 。

如果 Lyndon Word 存在border,则存在某个前缀等于后缀,因此这个后缀小于整个串,矛盾。

\(S\) 是 Lyndon Word 等价于 \(S\) 是其所有循环位移中最小的一个。

对于 \(|S| > 2\)\(S\) 是 Lyndon Word 等价于存在分解 \(s = uv\) ,其中 \(|u| > 0, |v| > 0, u < v\)\(u,v\) 都是 Lyndon Word 。

Lyndon 分解存在且唯一。

Duval 算法

P6114 【模板】Lyndon 分解

Duval 算法可以线性求出一个串的 Lyndon 分解。

维护三个指针 \(i,j,k\)

  • \(S[1, i - 1]\) 为已经固定的 Lyndon 分解,记作 \(S[1, i - 1] = s_1 s_2 \cdots s_g\)
  • \(S[i, k - 1]\) ,即一个循环串,满足每个循环节都是 Lyndon Word,且 \(s_g > S[i, k - 1]\) ,记 \(v\)\(S[i, k - 1]\) 一段可为空的残余后缀循环节。
  • \(j = k - p\) ,其中 \(p\)\(S[i, k - 1]\) 的周期。

考虑当前加入字符 \(S_k\) ,分类讨论:

  • \(S_k = S_j\) :直接令 \(k \gets k + 1, j \gets j + 1\) 即可,循环串周期不变。
  • \(S_k > S_j\) :此时 \(v + S_k\) 是一个 Lyndon Word。由于 Lyndon 分解需要满足 \(s_i \ge s_{i + 1}\) ,因此不断向前合并得到 \(S[i, k]\) 作为一个新的 Lyndon Word,并作为待处理的循环节。
  • \(S_k < S_j\) :此时每个循环节都可以被固定为 Lyndon 分解,新的循环节从残余后缀循环节开始计算。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e6 + 7;

char str[N];

int n;

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);
    int ans = 0;

    for (int i = 1, j, k; i <= n;) {
        for (j = i, k = i + 1; k <= n && str[k] >= str[j]; ++k)
            j = (str[k] == str[j] ? j + 1 : i);

        for (; i <= j; i += k - j)
            ans ^= i + k - j - 1;
    }

    printf("%d", ans);
    return 0;
}

应用

P1368 【模板】最小表示法

给出一个长度为 \(n\) 的序列 \(S\) ,求其最小的循环同构序列。

\(n \le 3 \times 10^5\)

\(S + S\) 进行 Lyndon 分解,找到首字符位置 \(\le n\) 且最后的 Lyndon Word ,这个串的首字符即最小表示法的首字符,时间复杂度线性。

如果还要求起始位置最靠前(UVA719 Glass Beads),则只需要特判一下循环串即可,下面给出一个简单的写法。

#include <bits/stdc++.h>
using namespace std;
const int N = 6e5 + 7;

int a[N];
bool be[N];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), a[i + n] = a[i];

    int firpos = 1;

    for (int i = 1, j, k; i <= n * 2;) {
        if (i <= n)
            firpos = i;

        for (j = i, k = i + 1; k <= n * 2 && a[k] >= a[j]; ++k)
            j = (a[k] == a[j] ? j + 1 : i);

        for (; i <= j; i += k - j)
            be[i] = true;
    }

    for (int i = firpos; i <= n; ++i)
        printf("%d ", a[i]);

    for (int i = 1; i < firpos; ++i)
        printf("%d ", a[i]);

    return 0;
}

bitset 匹配

CF914F Substrings in a String

给定字符串 \(S\)\(q\) 次操作,操作有:

  • 单点修改 \(S\) 的出现次数。
  • 查询 \(T\)\(S[l, r]\) 的出现次数。

\(|S|, q, \sum |T| \le 10^5\) ,时限 6s

考虑对每个字符开一个大小为 \(|S|\)bitset 记录其出现位置,单点修改时不难 \(O(1)\) 维护这些 bitset

查询时考虑用 bitset 维护 \(T\) 的出现位置(定义为首字符的出现位置),遍历 \(T\) 的每个字符 \(T_i\) ,将其在 \(S\) 中出现的位置右移 \(i - 1\) 位即可得到 \(T\) 可能的出现位置,将这些结果与起来即可。

再考虑 \(l, r\) 的限制,将其差分为 \(\ge l\) 的出现位置减去 \(\ge r - |T|\) 的出现位置,不难用右移操作处理。

时间复杂度 \(O(\frac{n^2}{\omega})\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, S = 26;

bitset<N> exist[S], ans;

char str[N], t[N];

int n, q;

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> (str + 1);
    n = strlen(str + 1);
    
    for (int i = 1; i <= n; ++i)
        exist[str[i] - 'a'].set(i);
    
    cin >> q;
    
    while (q--) {
        int op;
        cin >> op;
        
        if (op == 1) {
            int x;
            char c;
            cin >> x >> c;
            exist[str[x] - 'a'].reset(x), str[x] = c, exist[str[x] - 'a'].set(x);
        } else {
            int l, r;
            cin >> l >> r >> (t + 1);
            int len = strlen(t + 1);

            if (len > r - l + 1) {
                cout << "0\n";
                continue;
            }

            ans.set();
            
            for (int i = 1; i <= len; ++i)
                ans &= exist[t[i] - 'a'] >> (i - 1);
            
            cout << (ans >> l).count() - (ans >> (r - len + 2)).count() << '\n';
        }
    }
    
    return 0;
}

CF963D Frequency of String

给出字符串 \(S\)\(q\) 次询问 \(S\) 最短的子串长度 \(|T|\) 满足询问串 \(m_i\)\(T\) 中至少出现 \(k\) 次,或告知不存在。

\(|S|, q, \sum |m_i| \le 10^5\)

考虑用 bitset 做字符串匹配,求出 \(m_i\) 的所有出现位置后,若 \(m_i\) 出现不足 \(k\) 次则 \(T\) 不存在,否则取相邻 \(k\) 次出现位置作为 \(T\) 即可,时间复杂度 \(O(\frac{n^2}{\omega})\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, S = 26;

bitset<N> exist[S], res;
map<int, int> mp;

char str[N], t[N];

int n, m;

signed main() {
    scanf("%s%d", str + 1, &m), n = strlen(str + 1);

    for (int i = 1; i <= n; ++i)
        exist[str[i] - 'a'].set(i);

    while (m--) {
        int k;
        scanf("%d%s", &k, t + 1);
        int len = strlen(t + 1);
        bitset<N> res;
        res.set();

        for (int i = 1; i <= len; ++i)
            res &= exist[t[i] - 'a'] >> (i - 1);

        vector<int> vec;

        for (int i = res._Find_first(); i != res.size(); i = res._Find_next(i))
            vec.emplace_back(i);

        int ans = n + 1;

        for (int i = 0; i + k - 1 < vec.size(); ++i)
            ans = min(ans, vec[i + k - 1] + len - vec[i]);

        printf("%d\n", ans == n + 1 ? -1 : ans);
    }

    return 0;
}

P4465 [国家集训队] JZPSTR

初始有一个空串,\(T\) 次操作,操作有:

  • 在位置 \(x_i\) 处插入一个数字串。
  • 删除一段区间的数字串。
  • 查询一个串在一段子串中的出现次数。

任意时刻字符串长度 \(\le 10^6\) ,询问串总长度 \(\le 10^4\)

显然可以开 \(10\)bitset 存储每个字符的出现位置,则三个操作不难处理,然后就做完了。

具体懒得写,相信大家都会。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, S = 10;

bitset<N> p[S];
char str[N];

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        int op;
        scanf("%d", &op);

        if (!op) {
            int x;
            scanf("%d%s", &x, str);
            int len = strlen(str);
            bitset<N> msk;
            msk.set(), msk = msk >> x << x;

            for (int i = 0; i < S; ++i)
                p[i] = (p[i] & ~msk) | ((p[i] & msk) << len);

            for (int i = 0; i < len; ++i)
                p[str[i] & 15].set(x + i);
        } else if (op == 1) {
            int l, r;
            scanf("%d%d", &l, &r);
            bitset<N> mskl, mskr;
            mskl.set(), mskl = ~(mskl >> l << l);
            mskr.set(), mskr = mskr >> r << r;

            for (int i = 0; i < S; ++i)
                p[i] = (p[i] & mskl) | ((p[i] & mskr) >> (r - l));
        } else {
            int l, r;
            scanf("%d%d%s", &l, &r, str);
            bitset<N> msk, ans;
            msk.set(), msk = (msk >> l << l) ^ (msk >> r << r), ans.set();

            for (int i = 0, len = strlen(str); i < len; ++i)
                ans &= (p[str[i] & 15] & msk) >> i;

            printf("%d\n", (int)ans.count());
        }
    }

    return 0;
}

杂题选做

[ABC240Ex] Sequence of Substrings

给出一个长度为 \(n\) 的 01 串,最大化选取不交子串数量,满足这些子串按照原串中的顺序,字典序严格升序。

\(n \le 2.5 \times 10^4\)

首先不难发现,最优情况下,对于相邻两个串,后面的串长不大于前面的串长 \(+1\) (否则可以去掉多余的部分,不劣)。因此选取的串的长度上界为 \(\sqrt{2n}\) (事实上不满)。

考虑将这 \(O(n \sqrt{n})\) 个子串先用 Trie 排序,设 \(f_i\) 表示 \(i\) 结尾的最大选取数量,记当前处理的子串为 \(S[l, r]\) ,则 \(f_r \gets \max_{i = 0}^{l - 1} f_i + 1\) ,不难用树状数组优化到 \(O(n \sqrt{n} \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2.5e4 + 7;

char str[N];

int n;

namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] = max(c[x], k);
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res = max(res, c[x]);

    return res;
}
} // namespace BIT

namespace Trie {
const int M = 3e7 + 7;

vector<int> vec[M];

int ch[M][2], dep[M];

int tot;

inline void insert(int l, int r) {
    int u = 0;

    for (int i = l; i <= r; ++i) {
        int c = str[i] & 15;

        if (!ch[u][c])
            ch[u][c] = ++tot, dep[tot] = dep[u] + 1;

        u = ch[u][c], vec[u].emplace_back(i);
    }
}

void dfs(int u) {
    sort(vec[u].begin(), vec[u].end(), greater<int>());

    for (int it : vec[u])
        BIT::update(it, BIT::query(it - dep[u]) + 1);

    if (ch[u][0])
        dfs(ch[u][0]);

    if (ch[u][1])
        dfs(ch[u][1]);
}
} // namespace Trie

signed main() {
    scanf("%d%s", &n, str + 1);
    int m = sqrt(n * 2);

    for (int i = 1; i <= n; ++i)
        Trie::insert(i, min(i + m - 1, n));

    Trie::dfs(0);
    printf("%d", BIT::query(n));
    return 0;
}

[ABC225F] String Cards

给定 \(n\) 个串,选择 \(k\) 个串任意拼接,最小化拼接串的字典序。

\(n, k, |S_i| \le 50\)

先考虑 \(k = n\) 的情况,此时可以先按任意顺序拼接然后调整。对于两个相邻的字符串 \(a, b\) ,若 \(a + b < b + a\) 则交换二者即可,具体实现就是如此排序后依次拼接。

在考虑 \(k \ne n\) 的情况,此时直接贪心是错的,因为没有固定最终串的组成元素。

将字符串排序后考虑 DP,设 \(f_{i, j}\) 表示 \(i \sim n\) 选了 \(j\) 个串得到的最小串,转移直接枚举当前串是否选取即可。从后往前 DP 是因为从前往后 DP 时是在后面加字符串,而 \(a < b\) 不能推出 \(a + c < b + c\) ,从后往前就可以。

#include <bits/stdc++.h>
using namespace std;
const int N = 51;

string str[N], f[N][N];

int n, k;

signed main() {
    cin >> n >> k;

    for (int i = 1; i <= n; ++i)
        cin >> str[i];

    sort(str + 1, str + n + 1, [](const string &a, const string &b) {
        return a + b < b + a;
    });

    for (int i = n; i; --i)
        for (int j = 1; j <= n - i + 1; ++j)
            f[i][j] = min(j <= n - i ? f[i + 1][j] : "~", str[i] + f[i + 1][j - 1]);

    cout << f[1][k];
    return 0;
}
posted @ 2024-11-04 15:41  wshcl  阅读(47)  评论(0)    收藏  举报