「manacher」学习笔记

定义与基本求法

  • 定义

    又名 马拉车 ,用于处理子串回文问题。

  • 基本求法

    暴力判断每个子串是否是回文是 \(O(n^3)\) 的,根据其对称性优化为 \(O(n^2)\) 依旧是不优秀的。

    马拉车便是解决这种单一问题的算法,具有局限性,但同时是解决这种问题的不二选择。

    枚举回文串的中点,例如 \(aabaa\) 的中点为 \(b\) ,依次为基础进行下一步判断。

    那么这里发现如果回文串长度为偶数则无法判断,于是将其进行下述优化:

    例如 \(aaaaaa\) ,将其每一个空隙插入一个 \(\#\) (两边也插):\(\#a\#a\#a\#a\#a\#a\#\) ,这样其中点就变成了 \(\#\) ,长度也变为奇数。同时为了方便,对于奇数长度的回文也不放过,如 \(aaa→\#a\#a\#a\#\) ,每一个长度为 \(n\) 的回文长度都变为 \(2\times n+1\) ,这样无一例外的全部变为奇数长度。

    先设 \(p_i\) 为以 \(i\) 为中点的回文的最长半径,例如:\(\#a\#a\#a\#a\#\) ,以中间的 \(\#\) 为中点,则其最长半径为 \(4\) ,即 \(\#a\#a\#\) 的长度。

    再以 \(i\) 为中心,不断向两边扩张。定义 \(mx\) 为之前找到的回文串最靠右的右边界,\(id\) 表示这个最大右边界对称轴的位置。

    那么 $i $ 在 \(mx\) 以内时,显然时存在对称性的,\(p_i=p_{id\times 2-j}\) ,前提是其右端点必须在 \(mx\) 以内,否则其右端点只能到 \(mx\) , \(p_i\) 也只能 \(=mx-i\) 。继续向外扩展

    不然他就只能自力更生了,\(p_j=0\) ,再进一步向外扩展。

    只要 \(s[i+p_i]=s[i-p_i]\) 就说明可以继续向外扩展了,\(p_i++\)

    那么在扩展的过程中,超出 \(mx\) 了,就刷新 \(mx\) 的值,同时 \(i\) 也成了新的 \(id\)

    其实把马拉车理解了会发现这个东西很简单,很好理解,没有那么抽象。

    于是就通过上述方法求出了每一个 \(p_i\)

    参考下面一些图:

    image
    image
    image
    image
    image
    image

    综上所述:

    int init()
    {
        int len=strlen(str);
        s[0]='@',s[1]='#';
        int j=2;
        for(int i=0;i<len;i++) s[j++]=str[i],s[j++]='#';
        s[j]='\0';
        return j;
    }
    void manacher()
    {
        int ans=-1,mx=0,id=0,len=init();
        for(int i=1;i<len;i++)
        {
            if(i<mx) p[i]=min(p[id*2-i],mx-i);
            else p[i]=1;
            while(s[i+p[i]]==s[i-p[i]]) p[i]++;
            if(p[i]+i>mx) mx=p[i]+i,id=i;
        }
    }
    

例题

模板

  • 题目链接
  • 上面的代码找最大的 \(p_i-1\) 即可。(因为 \(\#\)

神奇项链

  • 题目链接

  • 题面:

    多个回文串拼在一起,且相同部分可重叠,如 \(aba,aca\) 可以拼成 \(abaaca\)\(abaca\) ,给定一字符串 \(s\) ,求拼成该字符串最少需要多少步。

  • 解法:

    我们是将每个串插上 \(\#\) 的,所以就算不另其重叠,\(\#\) 也是会重叠的,这就解决了判断重叠的问题。

    那么对于其每次拼定会存在重叠,所以只要求其最少产生多少重叠即可。

    那么用到马拉车,先求出每个 \(p_i\) ,随后就可以求出每一个回文的左右端点,将这些端点以左端点前后顺序排序。

    这样使每两个回文重叠部分尽可能的小。确定一回文 \(s\) 的右端点(当然也是尽可能靠右的,即当前左端点允许的,右端点最靠后的回文右端点),枚举后面回文的左端点,使其右端点端点尽可能的靠后,直至左端点与 \(s\) 右端点重合,在此过程中,刷新最靠后的右端点。

    举个例子,方便理解:

    image
    image

    有次可见上述文字描述的正确性,以及无论拼合时无论是否重叠,加上 \(\#\) 之后都是有重叠的。

  • 代码如下:

    #include<bits/stdc++.h>
    #define int long long 
    #define endl '\n'
    using namespace std;
    const int N=1e6+10,P=1e9+7;
    template<typename Tp> inline void read(Tp&x)
    {
        x=0;register bool z=1;
        register char c=getchar();
        for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
        for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
        x=(z?x:~x+1);
    }
    char str[N],s[N];
    int p[N],len;
    struct aa
    {
        int sta,en;
    }a[N];
    bool cmp(aa a,aa b) {return a.sta<b.sta;}
    int init()
    {
        int len=strlen(str);
        s[0]='#';
        int j=1;
        for(int i=0;i<len;i++) s[j++]=str[i],s[j++]='#';
        s[j]='\0';
        return j;
    }
    void manacher()
    {
        int ans=-1,mx=0,id=0,len=init();
        for(int i=0;i<len;i++)
        {
            if(i<mx) p[i]=min(p[id*2-i],mx-i);
            else p[i]=1;
            while(s[i+p[i]]==s[i-p[i]]) p[i]++;
            if(p[i]+i>mx) mx=p[i]+i,id=i;
        }
    }
    int cover()
    {
        int len=init();
        for(int i=0;i<len;i++)
            a[i].sta=i-p[i]+1,
            a[i].en=i+p[i]-1;
        stable_sort(a,a+len,cmp);
        int far=0,ans=0,i=0;
        for(i=0;a[i].sta<=0;i++)
            if(a[i].en>a[far].en)
                far=i;
        while(i<len)
        {
            ans++;
            int x=far;
            while(a[i].sta<=a[far].en&&i<len)
            {
                i++;
                if(a[i].en>a[x].en) 
                    x=i;
            }
            far=x;
        }
        return ans;
    }
    signed main()
    {
        #ifndef ONLINE_JUDGE
        freopen("in.txt","r",stdin);
        freopen("out.txt","w",stdout);
        #endif
        while(scanf("%s",str)!=EOF)
        {
            memset(p,0,sizeof(p));
            memset(a,0,sizeof(a));
            manacher();
            cout<<cover()-1<<endl;
        }
    }
    
  • 由此可见,马拉车也可以作为求具体问题的辅助算法存在。

$ Antisymmetry$

  • 题目链接

  • 题面:

    反对称字符串,仅由 \(0,1\) 组成,如 \(000111\) ,将 \(0,1\) 取反后,再反过来和原串一样。

    现给定一个字符串,求它有多少个子串是反对称的。

  • 解法:

    回文,但是反的回文。

    不难发现,如果其值反对称的,其长度必定是偶数。

    举个反例:\(011\) 显然无法是反对称的。

    那么转换到 \(manacher\) 中,就是其对称中心必定是 \(\#\) ,那么跑 \(manacher\) 时,只管对称中心是 \(\#\) 的情况就行了。

    下一步思考怎么跑这个回文,有两个方法:

    1. 先建一个与其相反的字符串,如 \(10001→01110\) ,然后跑马拉车时改成 \(s[i+p[i]]==t[i-p[i]]\) 就行了。

    2. 当然也可以不用再建一个,他要是匹配成功一定是一个 \(0\) 一个 \(1\) ,那么判 \(s[i+p[i]]+s[i-p[i]]=='0'+'1'\) 就行了,这样会把 \(\#\) 跳过去,所以让 \(p[i]+=2\) ,还能快一点。

    最后把所有对称中心是 \(\#\)\(p[i]\div 2\) 加起来就行了。至于把他 \(\div 2\) 是因为他是插入了 \(\#\) 的,长度也就 \(\times 2\) 了。

    如果代码中直接把对称中心不是 \(\#\)continue 了,那把所有的 \(p[i]\) 加在一起也可以,因为对称中心不是 \(\#\)\(p[i]\) 肯定是 \(0\)

    打就完事了,挺简单的,但是别手残 \(qwq\) \(→\) 不要手残

  • 代码如下:

#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=1e6+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
string str,s=" #";
int n,p[N],ans,len;
void init()
{
    for(int i=0;i<n;i++) 
        s+=str[i],
        s+='#';
}
void manacher()
{
    int mx=0,id=0;
    len=s.size()-1;
    for(int i=1;i<=len;i++)
    {
        if(s[i]!='#') continue;
        if(i<mx) p[i]=min(p[(id<<1)-i],mx-i);
        else p[i]=1;
        while(s[i+p[i]]+s[i-p[i]]=='0'+'1'&&i+p[i]<=len&&i-p[i]>=1) 
            p[i]+=2;
        if(p[i]+i>mx) mx=p[i]+i,id=i;
    }
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("ant13a.in","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n);
    cin>>str;
    init();
    manacher();
    for(int i=1;i<=len;i++)
        ans+=p[i]>>1;
    cout<<ans;
}

总结

作为一个相对简单且应用范围不广的算法,没有找到别的经典例题了,到这儿就结束了。

打完博客对于其理解还是有很大进步的。

再次吐槽一下网课质量,依旧是网上找资料进行学习的。

例题的图是自己画的,上面基本求法的图是网上找的,感觉不错就扣下来了。

神奇项链这题感觉也是挺不错的,教练得知 \(oj\) 上没有马拉车刚刚加上的,别的网站上没找到这道题(主要洛谷被禁了),感觉应该是道蓝。

最后挺不明白这玩意为啥是 \(NOI\) 知识点,感觉挺简单的 (至少只有 \(manacher\) 不套别的是的),但是里面的题基本都是蓝题起步,其实想明白真挺简单的。

还有,别手残!

posted @ 2024-02-01 22:11  卡布叻_周深  阅读(55)  评论(2)    收藏  举报