1. 程式人生 > >Manacher(馬拉車)學習筆記

Manacher(馬拉車)學習筆記

Manacher可以有效的在\(O(n)\)時間內解決一個字串的迴文子串的題目

目錄

  1. 簡介
  2. 講解
  3. 推介
  4. 簡單的練習
  5. 恐怖的練習QAQ
  6. 小結

簡介

開頭都說了,Manacher是目前解決迴文子串的最有效的方法之一,可以在\(O(n)\)時間內處理出以這個點為中心的最大回文子串長度。

講解

例題

1. 將偶數的迴文子串處理成奇數迴文子串

在暴力處理迴文子串的過程中,我們會把偶數與奇數的分成兩個判斷

相信大家一定會有這個疑問,那麼,我們在開頭、結尾與兩兩字元中間插入一個沒有出現過的字元,如:'#',舉個栗子:aaaa做完處理為:#a#a#a#a#。

那麼偶數的迴文子串就是以'#'為中心的字串。

為什麼在開頭與結尾插入一個'#',首先,仔細思考:
在#a#a#a#a#不管以哪個字元為中心,列舉到的迴文子串的開頭與結尾都肯定是'#'號,如:a#a,我們可以在兩個a再向外擴充套件一個'#'。

如果不在開頭與結尾加的話,就會出現開頭結尾不是'#'的異類迴文子串,如a#a。

那麼,我們就可以愉快直接算長度為奇數的迴文子串了。

2. 統計答案

由於後面的內容會十分的血腥,所以我還是先把簡單的搬到前面。

首先,我們定義一個數組ma陣列,\(ma_{i}\)代表以\(i\)到以\(i\)為中心的最大回文子串的開頭的長度,如:#a#b#b#c#b#,中以\(c\)為中心,組成的最大回文子串為#b#c#b#,c到最右邊的#的串長度為4,所以他的\(ma\)

就為4!

那麼,如何統計答案?

如圖:
在這裡插入圖片描述

迴歸正題,我們發現,b的ma值是4,同時真正的迴文子串是3(去掉#號),難道就是ma值減1?

沒錯,就是,為什麼?
我們繼續思考:把右邊的字母與左邊的'#'號調轉一下

如圖:
在這裡插入圖片描述

首先,我們知道,目前我們是以字母作為中心而不是'#', 那麼,在迴文子串中中心左邊的子串就呈現這種情況:#?#?#?#...#(問號是字母),而這個子串的長度是中心的ma值減1,'#'的個數是?的個數加1。

而右邊也是#?#?#?#...#(問號是字元),長度也一樣,'#'與?的個數也一樣,那麼把右邊的字母與左邊的'#'號調轉一下,我們會發現右邊的子串全是'#',而左邊只剩下一個'#'號。

那麼,根據ma陣列的定義,以字母為中心的迴文子串的長度為ma值減1。

那以'#'為中心的呢?
在這裡插入圖片描述

貌似也是ma值減1喲。

繼續,如果以#為中心,那麼迴文子串中心的左邊與右邊的子串也都是#?#?#?,長度為ma值減1,同時#的個數與?的個數相同,把左邊的'#'與右邊的'#'交換,那麼左邊全都是?,而中心是個'#'號,所以也是ma值減1。

處理ma陣列

沒錯,這也是最重要的!

學過EXKMP的話,這個應該是可以自己手推的。

在這裡插入圖片描述

現在處理以a為中心的迴文子串,難道還要從1開始?

細心的同學發現了,由於第三個位置('#')的ma值為3,我們可以發現,\(st_{2}=st_{4},st_{1}=st_{5}\),我們可以大膽的猜想一下我們可以將\(ma_{2}\)作為一個參考,\(ma_{2}\)值為2,仔細一看,\(ma_{4}\)的值至少為2!

繼續參考EXKMP。

我們得到一下步驟:

  1. 統計目前回文子串能到的最遠位置(p)與是哪個中心(b)。
  2. \(L=ma_{b-(i-b)}\)
  3. \(i+L-1<p\)時,ma的值直接等於L
  4. \(i+L-1≥p\)時,ma值為\(p-i+1\),然後開始直接暴力匹配,更新b與p。

其實在\(i+L-1>p 並且 i≤p\)時,ma值其實直接等於\(p-i+1\)就可以了,不需要暴力匹配,跟EXKMP差不多。

至於更改了b與p為什麼答案還一樣,我就不一一贅述了,都跟EXKMP差不多。

現在都還不懂的話,看程式碼自行理解吧

#include<cstdio>
#include<cstring>
#define  N  23000000
using  namespace  std;
char  st[N],sst[11000000];
int  ma[N],n,ans;//定義
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;//情況1
        else
        {
            int  pp=p-i+1<1?1:p-i+1;//至少為1
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;//(i-pp+1)-1>=1,i+pp-1+1<=n
            ma[i]=pp;b=i;p=ma[b]+b-1;ans=mymax(ans,ma[i]-1);//更新
        }
    }
}
int  main()
{
    scanf("%s",sst+1);n=strlen(sst+1);
    for(int  i=1;i<=n;i++)st[i*2]=sst[i];
    st[n*2+1]='#';for(int  i=1;i<=n;i++)st[i*2-1]='#';//填'#'號
    n=n*2+1;Man();//匹配
    printf("%d\n",ans);
    return  0;
}

推介

在這個OJ上找視訊,還可以,以前就是在這學的

例題很多

其實Manacher在學完EXKMP後,在學一點Manacher的概念,就可以手推了,畢竟我也是在歷史課上手推的。(應該也歸功於以前學過一次)。

總之也挺好理解的,就不一一贅述了。

簡單的練習

基本上都是自己會做的。。。

Manacher一大部分的統計題都是\(O(n)\)可以統計,一般到了\(O(n^{2})\),就不大正常了,簡單的例題都是\(O(n)\)可以統計的。

練習1

在Manacher匹配中,統計每個位置為開頭或結尾的迴文子串最長是多少。

然後在後面在更新全域性:\(ll_{i}=mymax(ll_{i},ll_{i-2}-2)\)(跳過'#'),\(rr_{i}=mymax(rr_{i},rr_{i+2}-2)\)(跳過'#')。

#include<cstdio>
#include<cstring>
#include<cstdlib>
#define  N  210000
using  namespace  std;
char  st[N],stt[N];
int  ma[N],f[N],n,ll[N]/*為開頭*/,rr[N]/*為結尾*/,ans;
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ll[i-ma[i]+2]=ma[i]-1;
        !rr[i+ma[i]-2]?rr[i+ma[i]-2]=ma[i]-1:0;//用貪心來省時間
    }
}
int  main()
{
    scanf("%s",stt+1);n=strlen(stt+1);
    st[n*2+1]='#';for(int  i=1;i<=n;i++)st[i*2]=stt[i],st[i*2-1]='#';
    n=n*2+1;Man();
    for(int  i=2;i<n;i+=2)ll[i]=mymax(ll[i],ll[i-2]-2);
    for(int  i=n-1;i>=2;i-=2)rr[i]=mymax(rr[i],rr[i+2]-2);//後面遞推更新
    for(int  i=4;i<=n;i+=2)
    {
        if(rr[i-2]  &&  ll[i])ans=mymax(ans,rr[i-2]+ll[i]);//統計答案
    }
    printf("%d\n",ans);//輸出
    return  0;
}

練習2

用類似差分統計,然後用快速冪加速一下。

#include<cstdio>
#include<cstring>
#define  N  1100000
#define  mod  19930726
using  namespace  std;
typedef  long  long  ll;
char  st[N];
int  ma[N],n;
ll  sum[N],ans,f[N],k;
void  Man()
{
    f[1]=n;
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        f[ma[i]*2+1]--;
    }
}
ll  kpow(ll  x,ll  p)//快速冪
{
    ll  qwq=1;
    while(p)
    {
        if(p%2==1)qwq*=x,qwq%=mod;
        x*=x;p>>=1;x%=mod;
    }
    return  qwq;
}
int  main()
{
    scanf("%d%lld",&n,&k);
    scanf("%s",st+1);
    Man();
    for(int  i=1;i<=n;i+=2)f[i]+=f[i-2];//差分的字首和。
    ans=1;
    for(int  i=n;i>=1;i--)
    {
        if((i&1)==0)continue;//不是'#'
        sum[i]=f[i]+sum[i+2];
        if(sum[i]>=k)//達到限制
        {
            ans*=kpow(i,k-sum[i+2]);ans%=mod;
            break;//退出
        }
        if(i==1)//沒有
        {
            printf("-1\n");
            return  0;
        }
        ans*=kpow(i,f[i]);ans%=mod;
    }
    printf("%lld\n",ans);//輸出
    return  0;
}

練習三

同樣記錄是否是開頭或結尾,然後用乘法統計,一個數字乘以一個字首和而已。

當然還是用了差分。。。

就不多講了,luogu有題解

//用了與第一題不同的統計方法
#include<cstdio>
#include<cstring>
#define  N  4100
using  namespace  std;
char  st[N],stt[N];
int  ma[N],n,ll[N],rr[N];
long  long  ans;
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=b+ma[b]-1;
        }
        ll[i-ma[i]+1]++;ll[i]--;rr[i+ma[i]-1]++;rr[i]--;//差分
    }
}
int  main()
{
    scanf("%s",stt+1);n=strlen(stt+1);
    st[1]='#';for(int  i=1;i<=n;i++)st[i*2+1]='#',st[i*2]=stt[i];
    n=n*2+1;Man();//匹配
    for(int  i=1;i<=n;i++)ll[i]+=ll[i-1];
    for(int  i=n;i>=1;i--)rr[i]+=rr[i+1];//先處理每個數的和
    for(int  i=3;i<=n;i+=2)rr[i]+=rr[i-2];//再處理一個的字首和
    for(int  i=1;i<=n;i+=2)ans+=ll[i]*rr[i];//統計
    printf("%lld\n",ans);
    return  0;
}

練習四

這裡稍微有些不同,我們不是找兩邊相等的迴文子串,而是找兩邊相反的迴文子串,如:1100。

而且必須是以'#'為中心,首先,按題意理解,迴文子串必須是偶數長度的,其二,由於換了迴文子串的定義,所以在Manacher的匹配中,以一個數字為中心也會出錯,這個很容易想為什麼,所以Manacher只找以'#'為中心的情況就是了。

#include<cstdio>
#include<cstring>
#define  N  1100000
using  namespace  std;
char  st[N],stt[N];
int  ma[N],n;
long  long  ans;
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i+=2/*只找'#'號*/)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  (st[i-pp]=='#'?1:st[i-pp]==(st[i+pp]^1)/*不同的計算方法*/))pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ans+=(ma[i]-1)/2;//統計
    }
}
int  main()
{
    scanf("%d",&n);
    scanf("%s",stt+1);
    st[1]='#';for(int  i=1;i<=n;i++)st[i*2+1]='#',st[i*2]=stt[i];
    n=n*2+1;
    Man();
    printf("%lld\n",ans);
    return  0;
}

練習五

用一個數組記錄以這個位置為開頭的最長不下降子序列的長度。

然後匹配的時候統計答案。

#include<cstdio>
#include<cstring>
#define  N  210000
using  namespace  std;
int  n,st[N],ma[N],f[N],ans;
inline  int  mymin(int  x,int  y){return  x<y?x:y;}//最小值
inline  int  mymax(int  x,int  y){return  x>y?x:y;}//最大值
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ans=mymax(mymin(ma[i]-1,f[i]-1),ans);//統計
    }
}
int  main()
{
    int  T;scanf("%d",&T);
    while(T--)
    {
        ans=0;
        scanf("%d",&n);
        st[1]=-1;
        for(int  i=1;i<=n;i++)
        {
            scanf("%d",&st[i*2]);
            st[i*2+1]=-1;
        }
        n=n*2+1;
        //輸入
        f[1]=1;
        int  mind=251,minid=0;
        for(int  i=2;i<=n;i+=2)
        {
            if(mind<=st[i])mind=st[i],f[i]=i-minid+2;//包括一個-1
            else  mind=st[i],minid=i,f[i]=2;
            f[i+1]=f[i]+1;//方便後面記錄答案
        }//處理最長不下降子序列
        Man();
        printf("%d\n",ans);
    }
    return  0;
}

恐怖的練習

練習一

聽說是Hash+Manacher判重,不過不想做了QAQ。

練習二

這個就真的不會了QAQ,好像是Manacher+樸素統計+優化。

小結

Manacher真的是一個不錯的演算法QMQ