天天看点

Luogu 3649 [APIO 2014] 回文串

          • 传送门
          • 思路
          • Manacher 算法
            • 特殊字符
            • 回文半径
            • 算法与实现
            • 本质不同的回文串个数
          • 正解
          • 参考代码
          • 总结
传送门
思路

  唉,我太弱了,什么都不会,这道题各路神仙都说是模板题,但我觉得完全不可做,唉,我太弱啦!神仙们太强啦!

  我不打算再学回文树1了,我打算用神仙流传的 Manacher + SAM 来做。在此之前,我们先回顾一下 Manacher 算法以及回文串的相关知识。

Manacher 算法

特殊字符

  Manacher 算法要在每两个字符间插入一个从未出现过的特殊字符(包括首位),也就是说长度为 n n 的字符串将会插入 n+1n+1 个特殊字符。约定我们的特殊字符为 #。

  插入特殊字符后,所有回文串的长度都变成了奇数,也就是说回文串的对称轴一定在一个字符上。

回文半径

  回文半径表示一个回文串最左或者最右的字符与其对称轴的距离,准确地说是对称轴到边界的字符数(包含对称轴和边界)。

  我们设 RLi R L i 表示以第 i i 个字符为对称轴的最长回文串的回文半径。

  RLiRLi 还表示以 i i 为对称轴的最长回文串长度加一。

算法与实现

  自己想。百度去。

本质不同的回文串个数

  定理:本质不同的回文串个数为 O(n)O(n)。

证明

  考虑 Manacher 算法的过程。当我们扩张触及范围时,说明我们可能找到一个新的回文子串。如果我们没有扩张,那么这个回文子串一定已经被找到过了。

  由于扩张次数是 O(n) O ( n ) 的,因此本质不同的回文子串是 O(n) O ( n ) 的。

  证毕。

  推论:本质不同的回文串的总长度为 O(n2) O ( n 2 ) 。

证明

  由定理,显然。我们也可以考虑构造这样一个字符串: aaaaa⋯ a a a a a ⋯ ,显然本质不同的回文串的总长度是 O(n2) O ( n 2 ) 的。

正解

  我们来整理一下思路。显然我们可以通过 Manacher 算法得到本质不同的所有回文串,个数为 O(n) O ( n ) 个。如果我们知道了各回文串的出现次数,问题就解决了。

  提到求各回文串的出现次数,自然会想到后缀自动机。那么我们再来回忆一下后缀自动机。我们可以求出 right r i g h t 集合的大小,然后把每个字符串放到后缀自动机上跑,停在的结点就是出现次数。

  但是以上操作的时间复杂度与串总长同阶,因此以上算法的时间复杂度是 O(n2) O ( n 2 ) 的。 有没有加速这个过程的方法呢?

  假设某个子串以第 i i 个字符结尾,那它一定是 1∼i1∼i 的后缀,换句话说,它在后缀链接树中一定是 i i 对应结点的祖先。我们倍增向上跳,直到长度满足要求即可。这样,单次查询的时间复杂度为 O(logn)O(log⁡n),算法的整体时间复杂度为 O(nlogn) O ( n log ⁡ n ) 。

参考代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef LL INT_PUT;
INT_PUT readIn()
{
    INT_PUT a = ; bool positive = true;
    char ch = getchar();
    while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
    if (ch == '-') { positive = false; ch = getchar(); }
    while (std::isdigit(ch)) { a = a *  - (ch - '0'); ch = getchar(); }
    return positive ? -a : a;
}
void printOut(INT_PUT x)
{
    char buffer[]; int length = ;
    if (x < ) putchar('-'); else x = -x;
    do buffer[length++] = -(x % ) + '0'; while (x /= );
    do putchar(buffer[--length]); while (length);
}

const int maxn = int() + ;
int n;
char str[maxn];
void readstr()
{
    char ch = getchar();
    while (!std::isalpha(ch)) ch = getchar();
    while (std::isalpha(ch))
    {
        str[++n] = ch;
        ch = getchar();
    }
}

struct SuffixAutomaton
{
    static const int alphabet = ;
    static inline int code(char ch) { return ch - 'a'; }
    struct Node
    {
        int len;
        int suffix;
        int ch[alphabet];
    } nodes[maxn * ];
    int size, last;
    SuffixAutomaton() { nodes[last = size++].suffix = -; }

    int cnt;
    int leaf[maxn];

    void extend(char ch)
    {
        int x = code(ch);
        int p = last;
        int cur = leaf[++cnt] = last = size++;
        nodes[cur].len = nodes[p].len + ;

        for (; ~p && !nodes[p].ch[x]; p = nodes[p].suffix)
            nodes[p].ch[x] = cur;
        if (!~p)
        {
            nodes[cur].suffix = ;
            return;
        }
        int q = nodes[p].ch[x];
        if (nodes[p].len +  == nodes[q].len)
        {
            nodes[cur].suffix = q;
            return;
        }

        int clone = size++;
        nodes[clone] = nodes[q];
        nodes[clone].len = nodes[p].len + ;
        nodes[q].suffix = nodes[cur].suffix = clone;
        for (; ~p && nodes[p].ch[x] == q; p = nodes[p].suffix)
            nodes[p].ch[x] = clone;
    }

    int k;
    int parent[][maxn * ];
    int buf[maxn * ];
    int sort[maxn * ];
    int weight[maxn * ];
    void init()
    {
        while ( << k <= size) k++;
        for (int i = ; i <= size; i++)
            parent[][i] = nodes[i].suffix;
        for (int i = ; i <= k; i++)
            for (int j = ; j <= size; j++)
                parent[i][j] = parent[i - ][parent[i - ][j]];

        for (int i = ; i < size; i++) buf[nodes[i].len]++;
        for (int i = ; i < size; i++) buf[i] += buf[i - ];
        for (int i = size - ; ~i; i--) sort[--buf[nodes[i].len]] = i;

        for (int i = ; i <= n; i++)
            weight[leaf[i]] = ;
        for (int i = size - ; i; i--)
        {
            int node = sort[i];
            weight[parent[][node]] += weight[node];
        }
    }
    LL query(int pos, int len)
    {
        int node = leaf[pos];
        for (int i = k; ~i; i--)
        {
            if (nodes[parent[i][node]].len < len) continue;
            node = parent[i][node];
        }
        return (LL)len * weight[node];
    }
} sam;

struct Manacher
{
    int size;
    char str[maxn * ];
    int f[maxn * ];

    void init()
    {
        str[] = '#';
        for (int i = ; i <= n; i++)
        {
            str[(i << ) - ] = ::str[i];
            str[i << ] = '#';
        }
        size =  * n + ;
    }

    void manacher(const std::function<void(int, int)> func)
    {
        int center = ;
        int maxr = ;
        for (int i = ; i < size; i++)
        {
            if (i <= maxr)
                f[i] = std::min(f[(center << ) - i], maxr - i + );
            else
                f[i] = ;

            while (i - f[i] >=  && i + f[i] < size && str[i - f[i]] == str[i + f[i]])
            {
                f[i]++;
                if (i + f[i] -  > maxr)
                {
                    maxr = i + f[i] - ;
                    center = i;
                    if (str[maxr] != '#')
                        func(((maxr + ) >> ), f[i]);
                }
            }
        }
    }
} manacher;

LL ans;
void callback(int pos, int len)
{
    ans = std::max(ans, sam.query(pos, len));
}

void run()
{
    readstr();

    for (int i = ; i <= n; i++)
        sam.extend(str[i]);
    sam.init();
    manacher.init();

    manacher.manacher(callback);
    printOut(ans);
}

int main()
{
    run();
    return ;
}
           
总结

  后缀自动机第一次跳跃要重定向至

cur

  Manacher 一般是先扩张,再更新

maxr

,这里需要同时更新

maxr

。当然,第一步是要确定答案下界。

  1. 什么是回文树?回文树又称回文自动机(但是后缀树不是后缀自动机),它在处理回文子串时有超强功效。 ↩