manacher算法解析

manacher算法是用来求一个字符串的最长回文子串的,这个算法的时间复杂度是线性的O(n)。

基本概念:

一:回文直径:字符串:abccba   ----->   回文直径:6

二:回文半径:很显然  3

三:回文半径数组:我们定义一个一维数组,数组上i位置记录以字符串i位置字符为中心所得到的回文半径。

四:最右回文右边界:当前所得到的回文中最右的位置。

五:最右回文右边界的中心:第一次得到最右回文位置的中心位置

算法解析:

先说一下暴力解法:

对于一个字符串   abccba   ,暴力解法是先将字符串变化成   #a#b#c#c#b#a#   ,然后以每个字符为中心开始向两边扩,将最后得到的结果除以2就是我们最终想要的结果。

演示一下:

一:

二:

三..................最后:

最后结果就是6。

manacher算法:

manacher算法是暴力解法改过来的,我们通过一个数组记录之前算的结果,然后通过这个结果来推接下来要算的结果,从而得到加速。实际上这个数组就是回文半径数组

算法的执行过程:

先将字符串处理成加#的形式

一:还是从字符串的0位置开始往外推,当推不动时,我们在回文半径数组0位置记录所得到的回文半径,然后还要记录所到达的最右回文右边界和最右回文右边界的中心。

二:接下来到字符串i(1~~~size())位置,我们先判断i位置是否在最右回文右边界的左边,通过这个判断,我们接下来分为了两种情况:

(1):i位置在回文右边界的右边,还是按照暴力解法从字符串i位置往两边扩,扩完后更新最右回文右边界和最右回文右边界的中心。

(2):i位置在回文右边界的左边,我们又分成了三种情况情况:

          《1》:i位置关于最右回文右边界的中心的对称点位置所得到的最长回文的左边界在当前所得到的最长回文最左左边界的                           右边

                        

                        此时以i位置为中心的最长回文半径就是以 i’位置为中心最长回文半径。为啥呢?我们证明一下:

                             证明:

                             

                             通过上图可得,f位置和 f’ 位置字符相等,h位置和 h’ 位置字符相等,又因为 i’ 位置的最长回文已经确定,所                                 以 h’ 位置和 f’ 位置的字符肯定不相等,所以,f位置和h位置的字符也肯定不相等,又因为整个字符串关于c对                               称 所以红色部分和蓝色部分相等,所以以i位置为中心的最长回文半径就是以 i’位置为中心最长回文半径。证                                 毕。

          《2》:i位置关于最右回文右边界的中心的对称点位置所得到的最长回文的左边界和当前所得到的最长回文最左左边界

                       重叠。

                       

                       此时我们就可以确定从i位置到R位置已经是回文半径了,但不一定是最长的回文半径,还需要往外扩继续比对。

              《3》:i位置关于最右回文右边界的中心的对称点位置所得到的最长回文的左边界在当前所得到的最长回文最左左边界                             的左边

                           

                          此时以i位置为中心的最长回文半径是( R - i )。

                                 证明:

                                 

                                  通过上图可知:  b = b'       b' = s'      s != s'    可推出   s!= b,证毕。

三:重复上述二操作直到全部访问完成。

代码:

#include<bits/stdc++.h>
#define MAXN 999999
using namespace std;
int pArr[MAXN];//pArr[i]代表一字符串的i位置为中心扩展出的最长回文子串的长度的一半,注意,这里的字符串不是原串,而是加上'#'后的字符串
string manacherString(string op)//将op字符串加上'#'字符
{
     int len=op.size()+op.size()+1;
     char copyop[len];
     int j=0;
     for(int i=0;i<len;i++)
     {
         if((i==0)||(i%2==0))
            copyop[i]='#';
         else
            copyop[i]=op[j++];
     }
     return copyop;
}
int manacher(string op)
{
    if(op.size()==0)
        return 0;
    int R=-1,C=-1;//R代表匹配过程中能够到达的字符串最右的位置,C代表到达R位置时的回文中心位置
    int maxz=0;
    memset(pArr,0,sizeof(pArr));
    string opz;
    opz=manacherString(op);
    for(int i=0;i<opz.size();i++)//从处理后的字符串的第一个字符开始跑
    {
        pArr[i]=i>R?1:min(pArr[C-(i-C)],R-i);//如果i大于R,说明是第一种大情况,pArr[i]初值设为1,从字符串i位置往两边扩,如果i小于R,说明是第二种大情况,此时以i位置字符串为中心的回文子串最短是(pArr[C-(i-C)])和(R-i)中的最小值(这里是可能值,最小是这些,可能还会更长)
        while ((i+pArr[i]<opz.size())&&(i-pArr[i]>=0))//开始匹配字符串,上面一行语句是给pArr[i]赋值一个最短的可能值,减少匹配的次数
        {
            if(opz[i+pArr[i]]==opz[i-pArr[i]])
                pArr[i]++;
            else
                break ;
        }
        if(i+pArr[i]>R)//到达了更右的位置,更新R和C
        {
            R=i+pArr[i];
            C=i;
        }
        maxz=max(maxz,pArr[i]);
    }
    return maxz-1;
}
int main()
{
    string arr;
    cin>>arr;
    cout<<manacher(arr)<<endl;
}

例题一:

Description
母亲节就要到了,小 H 准备送给她一个特殊的项链。这个项链可以看作一个用小写字
母组成的字符串,每个小写字母表示一种颜色。为了制作这个项链,小 H 购买了两个机器。第一个机器可以生成所有形式的回文串,第二个机器可以把两个回文串连接起来,而且第二个机器还有一个特殊的性质:假如一个字符串的后缀和一个字符串的前缀是完全相同的,那么可以将这个重复部分重叠。例如:aba和aca连接起来,可以生成串abaaca或 abaca。现在给出目标项链的样式,询问你需要使用第二个机器多少次才能生成这个特殊的项链。
Input
输入数据有多行,每行一个字符串,表示目标项链的样式。
Output
多行,每行一个答案表示最少需要使用第二个机器的次数。
Sample Input
abcdcba
abacada
abcdef
Sample Output
0
2
5
HINT
每个测试数据,输入不超过 5行
每行的字符串长度小于等于 50000

思路:

用manacher算法跑出pArr数组,通过pArr数组计算出以字符串每个位置为中心的最长回文子串的左边界和右边界,题目说可以重合,经典的区间覆盖问题。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 999999
typedef long long ll;
using namespace std;
int pArr[MAXN];
int len[50010],k;
int len1,len2;//len1代表原字符串扩展后的字符串的长度,len2代表原字符串的长度
struct node
{
    int star,end;
}root[50010];//root[i].star代表以字符串i位置为中心的最长回文子串的左位置,root[i].end代表以字符串i位置为中心的最长回文子串的右位置
string arr;
bool cmp(node a,node b)//比较器
{
    return a.star<b.star;
}
string manacherString(string op)
{
     int len=op.size()+op.size()+1;
     len1=len;
     char copyop[len];
     int j=0;
     for(int i=0;i<len;i++)
     {
         if((i==0)||(i%2==0))
            copyop[i]='#';
         else
            copyop[i]=op[j++];
     }
     return copyop;
}
void manacher(string op)
{
    if(op.size()==0)
        return ;
    int R=-1,C=-1;
    memset(pArr,0,sizeof(pArr));
    string opz;
    opz=manacherString(op);
    for(int i=0;i<opz.size();i++)
    {
        pArr[i]=i>R?1:min(pArr[C-(i-C)],R-i);
        while ((i+pArr[i]<opz.size())&&(i-pArr[i]>=0))
        {
            if(opz[i+pArr[i]]==opz[i-pArr[i]])
                pArr[i]++;
            else
                break ;
        }
        if(i+pArr[i]>R)
        {
            R=i+pArr[i];
            C=i;
        }
    }
}
int jisuan()//区间覆盖问题
{
    int far=0,ans=0,i=0;
    for(i=0;root[i].star<=0;i++)
        if(root[i].end>root[far].end)
            far=i;
    while (i<len1)
    {
        ans++;
        int tem=far;
        for(;root[i].star<=root[far].end&&i<len1;i++)
            if(root[i].end>root[tem].end)
                tem=i;
        far=tem;
    }
    return ans;
}
int main()
{
    char arr[50010];
    while (scanf("%s",&arr)!=EOF)
    {
        len1=0;
        memset(len,0,sizeof(len));
        memset(root,0,sizeof(root));
        manacher(arr);
        for(int i=0;i<len1;i++)//通过pArr数组得到以字符串每个位置为中心的最长回文子串的左边界和右边界
        {
            root[i].star=i-pArr[i]+1;
            root[i].end=i+pArr[i]-1;
        }
        sort(root,root+len1,cmp);//以左边界为排序标准进行升序排序
        cout<<jisuan()-1<<endl;
    }
}

例题二:

hdu3068

题意:

Problem Description

给出一个只由小写英文字符a,b,c...y,z组成的字符串S,求S中最长回文串的长度.
回文就是正反读都是一样的字符串,如aba, abba等

Input

输入有多组case,不超过120组,每组输入为一行小写英文字符a,b,c...y,z组成的字符串S
两组case之间由空行隔开(该空行不用处理)
字符串长度len <= 110000

Output

每一行一个整数x,对应一组case,表示该组case的字符串中所包含的最长回文长度.

Sample Input

aaaa

abab

Sample Output

4

3

思路:

manacher算法的裸题,模板题。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 999999
typedef long long ll;
using namespace std;
int pArr[MAXN];//pArr[i]代表一字符串的i位置为中心扩展出的最长回文子串的长度的一半,注意,这里的字符串不是原串,而是加上'#'后的字符串
string manacherString(string op)//将op字符串加上'#'字符
{
     int len=op.size()+op.size()+1;
     char copyop[len];
     int j=0;
     for(int i=0;i<len;i++)
     {
         if((i==0)||(i%2==0))
            copyop[i]='#';
         else
            copyop[i]=op[j++];
     }
     return copyop;
}
int manacher(string op)
{
    if(op.size()==0)
        return 0;
    int R=-1,C=-1;//R代表匹配过程中能够到达的字符串最右的位置,C代表到达R位置时的回文中心位置
    int maxz=0;
    memset(pArr,0,sizeof(pArr));
    string opz;
    opz=manacherString(op);
    for(int i=0;i<opz.size();i++)//从处理后的字符串的第一个字符开始跑
    {
        pArr[i]=i>R?1:min(pArr[C-(i-C)],R-i);//如果i大于R,说明是第一种大情况,pArr[i]初值设为1,从字符串i位置往两边扩,如果i小于R,说明是第二种大情况,此时以i位置字符串为中心的回文子串最短是(pArr[C-(i-C)])和(R-i)中的最小值(这里是可能值,最小是这些,可能还会更长)
        while ((i+pArr[i]<opz.size())&&(i-pArr[i]>=0))//开始匹配字符串,上面一行语句是给pArr[i]赋值一个最短的可能值,减少匹配的次数
        {
            if(opz[i+pArr[i]]==opz[i-pArr[i]])
                pArr[i]++;
            else
                break ;
        }
        if(i+pArr[i]>R)//到达了更右的位置,更新R和C
        {
            R=i+pArr[i];
            C=i;
        }
        maxz=max(maxz,pArr[i]);
    }
    return maxz-1;
}
int main()
{
    char arr[110000];
    while (~scanf("%s",arr))
        cout<<manacher(arr)<<endl;
}

例题三:

给定一个字符串str1,只能往str1的后面添加字符变成str2,要求str2
整体都是回文串且最短。

思路:

1、只要求出包含了str1中最后一个字符的最长回文子串s即可。

2、然后将子串s在str1中之前的字符逆序添加到str1末尾即可。

如“abc12321”,包含“1”的最长回文子串是“12321”,将“abc”逆序添加到“abc12321”末尾,变成“abc12321cba”,即为所求的str2。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
#define MAXN 999999
typedef long long ll;
using namespace std;
int pArr[MAXN];
int len1[MAXN];
int len,k;
string manacherString(string op)//将op字符串加上'#'字符
{
     len=op.size()+op.size()+1;
     char copyop[len];
     int j=0;
     for(int i=0;i<len;i++)
     {
         if((i==0)||(i%2==0))
            copyop[i]='#';
         else
            copyop[i]=op[j++];
     }
     return copyop;
}
void manacher(string op)
{
    if(op.size()==0)
        return ;
    int R=-1,C=-1;//R代表匹配过程中能够到达的字符串最右的位置,C代表到达R位置时的回文中心位置
    int maxz=0;
    memset(pArr,0,sizeof(pArr));
    string opz;
    opz=manacherString(op);
    for(int i=0;i<opz.size();i++)//从处理后的字符串的第一个字符开始跑
    {
        pArr[i]=i>R?1:min(pArr[C-(i-C)],R-i);//如果i大于R,说明是第一种大情况,pArr[i]初值设为1,从字符串i位置往两边扩,如果i小于R,说明是第二种大情况,此时以i位置字符串为中心的回文子串最短是(pArr[C-(i-C)])和(R-i)中的最小值(这里是可能值,最小是这些,可能还会更长)
        while ((i+pArr[i]<opz.size())&&(i-pArr[i]>=0))//开始匹配字符串,上面一行语句是给pArr[i]赋值一个最短的可能值,减少匹配的次数
        {
            if(opz[i+pArr[i]]==opz[i-pArr[i]])
                pArr[i]++;
            else
                break ;
        }
        if(i+pArr[i]>R)//到达了更右的位置,更新R和C
        {
            R=i+pArr[i];
            C=i;
        }
    }
}
int main()
{
    string str1;
    string str2;
    string str3;
    cin>>str1;
    manacher(str1);
    for(int i=1,k=0;i<len;i=i+2,k++)
        len1[k]=pArr[i]/2;
    int oz=0;
    for(int k=0;k<str1.size();k++)//这个循环找的是包含了str1中最后一个字符的最长回文子串s的回文中心位置
    {
        if(k+len1[k]==str1.size())
        {
            oz=k-len1[k];
            break ;
        }
    }
    str2=str1.substr(0,oz+1);//得到str1-s后剩余的子串,添加到str1后面
    str3=str1+str2;
    cout<<str3;
}

例题四:

HDU3613

题意:
26个字母都有一个价值,给你一个字符串,将该字符串切成两份,对于每一份,如果是回文串,就获得该子串的字母价值之和,否则该子串的价值为0。求出将字符串切成两份后能够获得的最大价值。

思路:

manacher判断回文子串,枚举所有切割点,找到价值最大的切割点位置。定义一个pre数组,pre[i]代表字符串的前缀截止到i位置的子串是否为回文子串,定义一个suf数组,suf[i]代表字符串的后缀截止到i位置的子串是否为回文子串,我们用manacher算法来实现这两个数组,然后我们还需要定义一个sum数组,sum[i]代表字符串的前i个字符的价值总和,主要是为了方便计算。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1000001
#define mod 1000000007
typedef long long ll;
using namespace std;
int t,len,len1,k;
int root[26];
int pArr[MAXN],sum[500001];
bool pre[500001],suf[500001];
string manacherString(string op)//将op字符串加上'#'字符
{
     len=op.size()+op.size()+1;
     char copyop[len];
     int j=0;
     for(int i=0;i<len;i++)
     {
         if((i==0)||(i%2==0))
            copyop[i]='#';
         else
            copyop[i]=op[j++];
     }
     return copyop;
}
void manacher(string op)
{
    if(op.size()==0)
        return ;
    int R=-1,C=-1;//R代表匹配过程中能够到达的字符串最右的位置,C代表到达R位置时的回文中心位置
    string opz;
    opz=manacherString(op);
    for(int i=1;i<len;i++)//从处理后的字符串的第一个字符开始跑
    {
        pArr[i]=i>R?1:min(pArr[C-(i-C)],R-i);//如果i大于R,说明是第一种大情况,pArr[i]初值设为1,从字符串i位置往两边扩,如果i小于R,说明是第二种大情况,此时以i位置字符串为中心的回文子串最短是(pArr[C-(i-C)])和(R-i)中的最小值(这里是可能值,最小是这些,可能还会更长)
        while ((i+pArr[i]<len)&&(i-pArr[i]>=0))//开始匹配字符串,上面一行语句是给pArr[i]赋值一个最短的可能值,减少匹配的次数
        {
            if(opz[i+pArr[i]]==opz[i-pArr[i]])
                pArr[i]++;
            else
                break ;
        }
        if(i+pArr[i]>R)//到达了更右的位置,更新R和C
        {
            R=i+pArr[i];
            C=i;
        }
        //填充pre数组和suf数组
        if(i-(pArr[i]-1)==0)//前缀字符子串是回文串
            pre[pArr[i]-1]=true;
        if(i+(pArr[i])==len)//后缀回文子串是回文串
            suf[pArr[i]-1]=true;
    }
}
int main()
{
    cin>>t;
    while (t--)
    {
        memset(pArr,0,sizeof(pArr));
        memset(pre,0,sizeof(pre));
        memset(suf,0,sizeof(suf));
        memset(root,0,sizeof(root));
        memset(sum,0,sizeof(sum));
        for(int i=0;i<26;i++)
            cin>>root[i];
        char op[500001];
        scanf("%s",op);
        len1=strlen(op);
        for(int i=1;i<=len1;i++)//填充sum数组
            sum[i]=sum[i-1]+root[op[i-1]-'a'];
        manacher(op);
        int sum1=-1;
        for(int i=1;i<len1;i++)//枚举切割点,假设字符串为acaaca,i等于2,那么两个子串为:ac和aaca
        {
            int ans=0;
            if(pre[i])
                ans+=sum[i];
            if(suf[len1-i])
                ans+=sum[len1]-sum[i];
            sum1=max(sum1,ans);
        }
        cout<<sum1<<endl;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值