【日常训练】简单的字符串

半城伤御伤魂 提交于 2019-12-05 17:47:49

Problem

求长度为 \(n\) 的、字符集大小 \(5000\) 的串有多少个偶数长的子串前一半和后一半循环同构。

\(n \leq 5000\)

Solution

分析

问题即为求有多少个子区间可以表示成 \(uvvu\)\(|u|\neq 0\)\(|v|\) 可以为 \(0\))的形式。

我们发现,如果以两个 \(v\) 作为中心,从中心分别向两边扩展,从这个过程可以得到两个字符串 \(a=v^Ru^R,b=vu\)\(u^R\) 表示 \(u\) 的反转),那么 \(s=a_1b_1a_2b_2\dots a_nb_n\) 这个串相当于是 \(v\)\(v^R\) 交错拼起来,\(u\)\(u^R\) 交错拼起来,然后再把这两个得到的字符串连起来。那么 \(s\) 这个串相当于两个偶回文串拼接而成。

考虑枚举中心,然后得到向两边扩展的字符串 \(a,b\),两个字符串都只保留前缀 \(1\dots \min\{|a|,|b|\}\),然后根据上面的方式得到字符串 \(s\)。那么我们就是需要统计,\(s\) 有多少个前缀,能够表示成两个偶回文串拼接而成的结果(注意特判只有 \(|v|=0\))。

Lemma 1: 对于一个双回文串 \(s\)(能够表示成两个非空回文串拼接的结果),若 \(s=x_1x_2=y_1y_2=z_1z_2(|x_1|<|y_1|<|z_1|)\)\(x_2,y_1,y_2,z_1\) 是回文串,则 \(x_1,z_2\) 也是回文串。

证明: 下面只证 \(x_1\) 是回文串,\(z_2\) 同理。

如上图,设 \(z_1=y_1v\),则 \(v\)\(y_2\) 的前缀,\(v^R\)\(x_2,y_2\) 的后缀,\(v\)\(x_2\) 的前缀,于是 \(x_1v\)\(z_1\) 的前缀。

\(y_1\)\(z_1\) 的 border,所以 \(|v|\)\(z_1\) 的 period,于是 \(|v|\) 也是 \(x_1v\) 的 period。

所以 \(x_1\)\(v^{\infty}\) 的后缀。

\(v^R\)\(x_1,z_1\) 的前缀,而 \(|v|\)\(x_1\) 的 period,所以 \(x_1\)\(\left(v^R\right)^{\infty}\) 的前缀。

\(x_1\) 是回文串。\(\square\)

Lemma 2: 对于一个双回文串 \(s\),存在一种回文划分 \(s=ab\)\(a,b\) 均为回文串且非空),使得 \(a\)\(s\) 的最长回文前缀,或 \(b\)\(s\) 的最长回文后缀。

可以根据 Lemma 1 加上一些分类得到,这里不详细证明。

Lemma 3: 对于一个双偶回文串 \(s\)(能表示成两个偶回文串拼接的结果),存在一种回文划分 \(s=ab\)\(a,b\) 均为偶回文串且非空),使得 \(a\)\(s\) 的最长回文前缀,或 \(b\)\(s\) 的最长回文后缀。

将 Lemma 1 和 Lemma 2 的「回文串」改成「偶回文串」结论仍然成立。

Manacher 做法

因此我们只需要求出所有前缀的最长偶回文前缀和最长偶回文后缀,并分别判断剩下的部分是否回文。

求所有前缀的最长偶回文后缀,可以从左往右扫,维护一个当前的回文串能延伸右边界 \(rit\)。每次枚举到新的回文中心 \(i\),只需要更新左端点在区间 \((rit,i+r_i)\) 内的信息即可(\(r_i\) 表示 \(i\) 的回文半径)。正确性显然。(注意因为我们只考虑偶回文串,所以我们只枚举以特殊字符 #(Manacher 时插入的特殊字符)为回文中心的贡献)

判断剩下的部分是否回文就直接用中心的回文半径判即可。最长偶回文前缀就枚举的时候顺便维护一下即可。

#include <bits/stdc++.h>

template <class T>
inline void read(T &x)
{
    static char ch; 
    while (!isdigit(ch = getchar())); 
    x = ch - '0'; 
    while (isdigit(ch = getchar()))
        x = x * 10 + ch - '0'; 
}

template <class T>
inline void relax(T &x, const T &y)
{
    if (x < y)
        x = y; 
}

template <class T>
inline void tense(T &x, const T &y)
{
    if (x > y)
        x = y; 
}

template <class T>
inline T get_abs(const T &x)
{
    return x < 0 ? ~x + 1 : x; 
}

const int MaxN = 1e4 + 5; 

int n, m, ans; 
int a[MaxN], s[MaxN]; 

inline void solve(int *s, int n)
{
    static int t[MaxN], r[MaxN], m; 
    static int max_suf[MaxN]; 
    m = 0;

    for (int i = 1; i <= n; ++i)
    {
        t[(i << 1) - 1] = 0; 
        t[i << 1] = s[i]; 
    }

    m = n << 1 | 1, t[m] = 0; 
    t[0] = -1, t[m + 1] = -2; 

    int rit = 0, p = 0; 
    for (int i = 1; i <= m; ++i)
    {
        if (rit > i)
        {
            int j = (p << 1) - i; 
            r[i] = std::min(r[j], rit - i); 
        }
        else
            r[i] = 1; 
        while (t[i - r[i]] == t[i + r[i]])
            ++r[i]; 
        if (i + r[i] > rit)
        {
            rit = i + r[i]; 
            p = i; 
        }
        max_suf[i] = 0; 
    }

    rit = 1; 
    for (int i = 1; i <= m; i += 2)
    {
        for (int j = i + r[i] - 1; j >= rit && j > i; --j)
            relax(max_suf[j], get_abs(i - j) << 1 | 1); 
        relax(rit, i + r[i]); 
    }

    int cur_pre = 0; 
    for (int i = 2; i <= n; i += 2)
    {
        bool flg = false; 

        if (r[i + 1] > i)
        {
            flg = true; 
            cur_pre = i + 1;
        } 
        if (max_suf[i << 1])
        {
            int cur = i - (max_suf[i << 1] >> 1) - 1; 
            if (r[cur + 1] > cur)
                flg = true; 
        }
        if (cur_pre && r[cur_pre + i] > i - cur_pre)
            flg = true; 
        ans += flg; 
    }
}

int main()
{
    freopen("naive.in", "r", stdin); 
    freopen("naive.out", "w", stdout); 

    read(n);
    for (int i = 1; i <= n; ++i)
        read(a[i]); 
    for (int i = 1; i < n; ++i)
    {
        m = 0; 
        int l = i, r = i + 1; 
        while (l >= 1 && r <= n)
        {
            s[++m] = a[l]; 
            s[++m] = a[r]; 
            --l, ++r; 
        }

        solve(s, m); 
    }
    std::cout << ans << std::endl; 

    return 0; 
}

回文树做法

用 Hash 判回文。求每个前缀的最长偶回文前缀只要枚举的时候维护一下即可。

然后求最长偶回文后缀只要在回文树上找到对应点的 fail 链上深度最大的偶回文串即可,这个也可以在构造回文树的时候顺带维护一下。

(代码懒得写)

参考文献

  1. WC2017 字符串算法选讲 —— 金策

  2. 《简单的字符串》题解 —— 钟子谦

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!