• $Manacher$ 算法学习笔记


    (Manacher) 算法

    (1.) 算法概述

    给定字符串 (s),可以在 (O(n)) 时间内处理出以每个位置 (i) 为回文中心所具有的回文串的长度,进而可以在 (O(n)) 时间内求出 (s)最大回文子串长度

    (2.) 算法详解

    (2.1) 引入参数

    (s = abcba) 如下

    |(0)|(1)|(2)|(3)|(4)|(5)|(6)|
    |-|-|-|-|-|-|-|-|-|-|-|
    ||(a)|(b)|(c)|(b)|(a)|(ackslash 0)|

    (2.1.1) (str) 字符串

    (str) 表示 (s) 处理后的串

    为了避免对回文串长度奇偶性的讨论

    考虑在 (s) 的首尾、以及中间每两个字符中插入分隔符 ($)

    并且在 (str) 首部位置插入一个哨兵 ($)

    (str) 具体的构造如下

    |(0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9)|(10)|(11)|(12)|
    |-|-|-|-|-|-|-|-|-|-|-|-|-|-|
    |($)|($)|(a)|($)|(b)|($)|(c)|($)|(b)|($)|(a)|$|(ackslash 0)|

    对应的代码如下

    int L = strlen(s + 1);
    for(int i = 1; i <= L; ++i)
        str[2 * i] = s[i], str[2 * i + 1] = '$';
    L = L * 2 + 1, str[0] = str[1] = '$', str[L + 1] = '';
    

    (2.1.2) (pleft[i ight]) 数组

    (p[i]) 表示每个位置 (i) 可以扩展的最大长度,也等于以位置 (i) 为回文中心的回文串长度

    构造如下

    |(0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9)|(10)|(11)|(12)|
    |-|-|-|-|-|-|-|-|-|-|-|-|-|-|
    |($)|($)|(a)|($)|(b)|($)|(c)|($)|(b)|($)|(a)|($)|(ackslash 0)|
    ||(0)|(1)|(0)|(1)|(0)|(5)|(0)|(1)|(0)|(1)|(0)||

    (2.1.3 mid, maxr)

    (maxr) 表示当前所有回文子串中的最右端点,(mid) 表示这个回文子串的回文中心

    (2.2) 构造 (p[i])

    (manacher) 算法复杂度之所以优秀,关键在于 (p[i]) 的构造

    基本的思想在于利用已知的 (maxr, mid) 来计算 (p[i]),具体可以分成两类

    (2.2.1) (i < maxr)

    图源洛谷,(pos)(mid),示例如下

    (j)(i) 关于 (mid) 的对称点

    由对称性可知,填色的两个回文串一模一样,所以 (p[i] = p[j])

    但若回文长度过长,超出了 (maxl/ maxr),如下所示

    那么 (p[i] = j - maxl = maxr - i)

    所以取二者最小即可

    [p[i] = minleft{p[j], maxr - i ight} ]

    (2.2.2) (igeq maxr)

    只能暴力拓展

    具体代码如下

    for(int i = 1; i <= L; ++i) {
        if(i < maxr) p[i] = min(p[2 * mid - i], maxr - i);
        while(str[i - p[i] - 1] == str[i + p[i] + 1]) ++p[i]; //暴力拓展
        if(i + p[i] > maxr) maxr = i + p[i], mid = i; //更新 maxr, mid
    }
    

    (2.3) 算法复杂度分析

    每次暴力拓展时,更新 (maxr)

    (maxr) 一直在右移,移动次数总计 (O(n))

    加上一个遍历的复杂度 (O(n))

    故总的复杂度仍然为 (O(n))

    (3.) 代码模板

    #include <bits/stdc++.h>
    #define fi first
    #define se second
    #define pii pair<int, int>
    #define pb push_back
    #define arrayDebug(a, l, r) for(int i = l; i <= r; ++i) printf("%d%c", a[i], " 
    "[i == r])
    typedef long long LL;
    typedef unsigned long long ULL;
    const LL INF = 0x3f3f3f3f3f3f3f3f;
    const LL MOD = 11092019;
    const int inf = 0x3f3f3f3f;
    const int DX[] = {0, -1, 0, 1, 0, -1, -1, 1, 1};
    const int DY[] = {0, 0, 1, 0, -1, -1, 1, 1, -1};
    const int N = 4e7 + 7;
    const double PI = acos(-1);
    const double EPS = 1e-6;
    using namespace std;
    inline int read()
    {
        char c = getchar();
        int ans = 0, f = 1;
        while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
        while(isdigit(c)) {ans = ans * 10 + c - '0'; c = getchar();}
        return ans * f;
    }
    
    char s[N], str[N];
    int L, p[N];
    void manacher()
    {
        for(int i = 1; i <= L; ++i) str[2 * i] = s[i], str[2 * i + 1] = '$';
        L = L * 2 + 1, str[0] = str[1] = '$', str[L + 1] = '';
        int mid = 0, maxr = 0;
        for(int i = 1; i <= L; ++i) {
            if(i < maxr) p[i] = min(p[2 * mid - i], maxr - i);
            while(str[i - p[i] - 1] == str[i + p[i] + 1]) ++p[i];
            if(i + p[i] > maxr) maxr = i + p[i], mid = i;
        }
    }
    int main()
    {
        scanf("%s", s + 1);
        L = strlen(s + 1);
        manacher();
        int ans = 0;
        for(int i = 1; i <= L; ++i) ans = max(ans, p[i]);
        cout<<ans<<endl;
        return 0;
    }
    

    (4.) 应用

    (4.1) 求最长回文前缀

    (manacher) 预处理 (p[i]) 数组

    对于新字符串 (str) 的每一个位置 (i),判断其回文半径是否触及左端点 (1),即 (i - p[i] = 1),若可以触及,说明是一个回文前缀,更新长度即可

    代码如下

    #include <bits/stdc++.h>
    #define fi first
    #define se second
    #define pii pair<int, int>
    #define pb push_back
    #define arrayDebug(a, l, r) for(int i = l; i <= r; ++i) printf("%d%c", a[i], " 
    "[i == r])
    typedef long long LL;
    typedef unsigned long long ULL;
    const LL INF = 0x3f3f3f3f3f3f3f3f;
    const LL MOD = 11092019;
    const int inf = 0x3f3f3f3f;
    const int DX[] = {0, -1, 0, 1, 0, -1, -1, 1, 1};
    const int DY[] = {0, 0, 1, 0, -1, -1, 1, 1, -1};
    const int N = 4e7 + 7;
    const double PI = acos(-1);
    const double EPS = 1e-6;
    using namespace std;
    inline int read()
    {
        char c = getchar();
        int ans = 0, f = 1;
        while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
        while(isdigit(c)) {ans = ans * 10 + c - '0'; c = getchar();}
        return ans * f;
    }
    
    char s[N], str[N];
    int L, p[N];
    void manacher()
    {
        for(int i = 1; i <= L; ++i) str[2 * i] = s[i], str[2 * i + 1] = '$';
        L = L * 2 + 1, str[0] = str[1] = '$', str[L + 1] = '';
        int mid = 0, maxr = 0;
        for(int i = 1; i <= L; ++i) {
            if(i < maxr) p[i] = min(p[2 * mid - i], maxr - i);
            while(str[i - p[i] - 1] == str[i + p[i] + 1]) ++p[i];
            if(i + p[i] > maxr) maxr = i + p[i], mid = i;
        }
    }
    int main()
    {
        scanf("%s", s + 1);
        L = strlen(s + 1);
        manacher();
        int ans = 0;
        for(int i = 1; i <= L; ++i) 
            if(i - p[i] == 1) ans = max(ans, p[i]);
        cout<<ans<<endl;
        return 0;
    }
    
  • 相关阅读:
    PHP通用函数
    Discuz 取各排行榜数据
    htaccess 增加静态文件缓存和压缩
    一个域名解析不同访问方法
    TP5:隐藏inde.php文件
    vscode:解决操作git总让输入用户名及密码问题
    vscode:配置git
    cmd:相关命令和笔记
    PHP:通过MVC,实现第三方登录(百度)
    Linux:301重定向 —— 将不带www的重定向到带www的
  • 原文地址:https://www.cnblogs.com/ChenyangXu/p/12907443.html
Copyright © 2020-2023  润新知