Manacher算法学习笔记

算法原理与实现

\(Manacher\) 是一种用来处理回文的算法,首先对于处理回文我们有一种十分暴力的方法,即直接暴力枚举每一个字符,然后暴力的往两边找,但是这个算法复杂度是 \(O(n ^ 2)\) 的,就十分的不优,于是,神奇的马拉车算法就出现了(好耶!)\(Manacher\) 算法的复杂度是 \(O(n)\) 的,是复杂度非常优秀的算法。

\(Manacher\) 算法的核心其实是一个枚举中心位置 \(mid\) ,边界位置 \(r\) 与递推的过程,对于一个奇回文,我们是更好处理回文串问题的,因为他有一个中心点,但是对于一个偶串,我们是不好求解的,所以我们在每两个字符中间插入一个毫不相关的字符,使偶串变成奇串,而奇串还是奇串,然后在最开始的位置与最后的位置加入两个互异的毫不相关的字符,防止算法在枚举时越界,因为在我们枚举边界位置的时候,最后的字符与第一个字符始终无法匹配,这就很好的防止的枚举越界。

对于一个字符串,我们定义一个 \(p\) 数组,来表示在字符串中已 \(i\) 为中心的最大回文串的半径,因为我们最开始在原串中加入了新的字符,所以我们最终的答案就是 \(p[i] - 1\)

首先,我们可以得知一个性质,当一个回文串的左边含有一个回文串的时候,那么在这个回文串的右边也一定会有一个回文串,且这两个回文串的长度一定相等,所以当 \(r > i\) 时,我们可以直接算出 \(p[i]\) 等于 \(p[mid - (r - mid)]\)\(p[2 * mid - r]\)\(i\)\(r\) 其中那个小的。但是当 \(r \le i\) 时,我们无法直接得出 \(p[i]\) 的值,所以我们把 \(p[i]\) 设为 \(0\),再暴力扫一遍,更新 \(r\) 的值。

对于复杂度的证明。

因为对于每个 \(i < r\) 的情况我们可以 \(O(1)\) 算出,而当 \(r \le i\) 时,我们总共能够扩展的长度就是字符串的长度,所以时间复杂度是 \(O(n)\) 的(口胡)

题目

「模板」Manacher算法
马拉车板子

#include <bits/stdc++.h>
using namespace std;
#define R register int
const int MAXN = 11000000 + 10;
char in[MAXN], ch[MAXN << 2];
int p[MAXN << 2];
int main()  {
    scanf("%s", in + 1);
    int len = strlen(in + 1);
    for(R i = 1; i <= len; i ++) {
        ch[(i << 1) - 1] = '%';
        ch[(i << 1)] = in[i];
    }//填充字符
    len <<= 1; ch[0] = '<', ch[++ len] = '%';
    int r = 0, id, Max = 0; ch[++ len] = '>';
    for(R i = 1; i < len; i ++) {
        if(i < r) p[i] = min(r - i, p[(id << 1) - i]);
        else p[i] = 1;
        //如果我们计算过了当前的点对于id的对称点,我们就可以直接计算出结果
        while(ch[i + p[i]] == ch[i - p[i]]) p[i] ++;
        //暴力搜索长度
        if(r < i + p[i]) id = i, r = i + p[i];
        //如果这次扩展比以前的都要长,更新最右边界与中心点
        Max = max(p[i] - 1, Max);
    }
    printf("%d\n", Max);
}

「Luogu P1723 」高手过愚人节
也是板子题,但是不推荐直接复制粘贴,建议自己在写写练下手,因为有多组数据注意清空数组,字符串长度开到 1e7 是能过的。

#include <bits/stdc++.h>
using namespace std;
#define R register int
const int MAXN = 11000000 + 10;
char in[MAXN], ch[MAXN << 1];
int p[MAXN << 1];
int main()  {
    int T; scanf("%d", &T);
    while(T --) {
        memset(p, 0, sizeof(p));
        scanf("%s", in + 1);
        int len = strlen(in + 1);
        for(R i = 1; i <= len; i ++) {
            ch[(i << 1) - 1] = '%';
            ch[(i << 1)] = in[i];
        }
        len <<= 1; ch[0] = '<', ch[++ len] = '%';
        int r = 0, id, Max = 0; ch[++ len] = '>';
        for(R i = 1; i < len; i ++) {
            if(i < r) p[i] = min(r - i, p[(id << 1) - i]);
            else p[i] = 1;
            while(ch[i + p[i]] == ch[i - p[i]]) p[i] ++;
            if(r < i + p[i]) id = i, r = i + p[i];
            Max = max(p[i] - 1, Max);
        }
        printf("%d\n", Max);
    }
}

「SHOI2001」双倍回文

通过 \(Manacher\) 算法算出回文,再通过 \(p[i]\) 与关于 \(mid\) 的对称点是否有交,有交即能构成双倍回文串。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500000 + 10;
char ch[MAXN << 2];
int p[MAXN << 2];
int main ()  {
    int n; scanf("%d\n", &n);
    ch[0] = '$'; ch[1] = '%'; n = 2 * n + 1;
    for(register int i = 2; i <= n;) {
        scanf("%c", &ch[i ++]); ch[i ++] = '%';
    }
    ch[n] = '%';
    int id, mx = 0, Max = -1;
    for(register int i = 1; i <= n; i += 2) {
        if(i < mx) p[i] = min(p[2 * id - i], mx - i);
        else p[i] = 1;
        if(i < mx && i - p[i] < id) Max = max(Max, 2 * (i - id));
        while(i + p[i] <= n && i - p[i] > 0 && ch[i - p[i]] == ch[i + p[i]]) p[i] ++;
        if(mx < i + p[i]) id = i, mx = i + p[i];

    }
    printf("%d\n", Max);
    return 0;
}

「国家集训队」最长双回文串
看到这道题的名字的时候我还以为是双倍经验来着。。。
实际上这道题是要比上题要难一些的,首先,对于两个回文串,他们能构成一个双回文串首先是他们两个不相交即两个回文串的左右边界有一个特殊字符隔着,所以我们处理出以 \(i\) 为左边界与右边界的字符串,最后直接输出即可,不过要注意判断是否为 \(0\) 因为如果为 \(0\) 的话我们选择的则是单个字符串

#include <bits/stdc++.h>
using namespace std;
#define R register int
const int MAXN = 100000 + 10;
char in[MAXN], ch[MAXN << 2];
int p[MAXN << 2], l[MAXN << 2], r[MAXN << 2];
int main()  {
    scanf("%s", in + 1);
    int len = strlen(in + 1);
    for(R i = 1; i <= len; i ++) {
        ch[(i << 1) - 1] = '%';
        ch[(i << 1)] = in[i];
    }
    len <<= 1; ch[0] = '<', ch[++ len] = '%';
    int id, mx = 0, Max = -1;
    for(R i = 1; i <= len; i ++) {
        if(i < mx) p[i] = min(p[2 * id - i], mx - i);
        else p[i] = 1;
        while(ch[i - p[i]] == ch[i + p[i]]) p[i] ++;
        if(mx < i + p[i]) id = i, mx = i + p[i];
        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(R i = 3; i <= len; i += 2)
        r[i] = max(r[i], r[i - 2] - 2);
    for(R i = len; i >= 3; i -= 2) 
        l[i] = max(l[i], l[i + 2] - 2);
    for(R i = 3; i <= len; i += 2)
        if(l[i] && r[i]) Max = max(Max, r[i] + l[i]);
    printf("%d\n", Max);
    return 0;
}

「NUMOFPAL」Number of Palindromes
大水题,每个字符为中心的回文串的数量的总数就是 \(p[i] \div 2\) 的和(真·秒紫题)

#include <bits/stdc++.h>
using namespace std;
#define R register int
const int MAXN = 100000 + 10;
char in[MAXN], ch[MAXN << 2];
int p[MAXN << 2], l[MAXN << 2], r[MAXN << 2];
int main()  {
    scanf("%s", in + 1);
    int len = strlen(in + 1);
    for(R i = 1; i <= len; i ++) {
        ch[(i << 1) - 1] = '%';
        ch[(i << 1)] = in[i];
    }
    len <<= 1; ch[0] = '<', ch[++ len] = '%';
    int id, mx = 0, ans = 0;
    for(R i = 1; i <= len; i ++) {
        if(i < mx) p[i] = min(p[2 * id - i], mx - i);
        else p[i] = 1;
        while(ch[i - p[i]] == ch[i + p[i]]) p[i] ++;
        if(mx < i + p[i]) id = i, mx = i + p[i];
    }
    for(register int i = 1; i <= len; i ++) ans += p[i] >> 1;
    printf("%d\n", ans);
    return 0;
}
posted @ 2020-11-06 11:10  Van_樣年华  阅读(116)  评论(0编辑  收藏  举报