C++实现Manacher算法【马拉车】

最近研究了一下Manacher算法,本人非科班出身,参考很多大牛资料,最终终于搞明白了,感觉甚是不易,遂做此笔记,整理很多次,思路感觉还算清晰,希望对大家有所帮助!!!纯手工画图,感觉不错的朋友,点个赞哟@_@

主要功能:解决最长回文子串的问题[给定一个字符串,求解其最长回文子串的长度]

1、大体思路阐述

给定一个字符串str = “abbc”,求解其最长回文子串的长度???

对原来的字符串进行预处理['#']>求解辅助数组p>求解数组p中的最大值max>最长回文子串的长度为(max-1)

 

0

1

2

3

4

5

6

7

8

str

a

b

b

c

 

 

 

 

 

预处理str'

#

a

#

b

#

b

#

c

#

辅助数组p

1

2

1

2

3

2

1

2

1

辅助数组是如何求解的呐???

p[i]表示以字符str'[i]为中心的最长回文子串的最右(左)字符到Str[i]的距离(包括Str[i]。以p[4]=3为例说明:str'[4]自身占据一个计数1;str'[3]=str'[5]=b占据一个计数1;str'[2]=str'[6]=#占据一个计数1。所以总计p[4]=3

PS:对str数组预处理,添加‘#’目的是将原来字符串不管是奇数个还是偶数个都处理为奇数个,方便统一编码处理


2、如何通过代码求解回文半径数组p????

明确一下引入的3个概念

(1)回文半径数组pp[i]表示以字符str'[i]为中心的最长回文子串的最右(左)字符到Str[i]的距离(包括Str[i]。=>p[i]的值对应为以str'[i]为中心的最长回文半径

(2)回文右边界R

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

str'

#

0

#

1

#

2

#

1

#

3

#

1

#

2

#

1

#

0

#

p

1

2

1

2

1

4

1

2

1

10

1

2

1

4

1

2

1

2

1

R

1

3

3

5

5

9

9

9

9

19

19

19

19

19

19

19

19

19

19

由此看来:R的大小只增不减,当R[i]<(i+p[i]),则R[i+1]=(i+p[i]),否则R[i+1]=R[i]

(3)回文右边界的中心id这个概念和回文右边界R相互联系的,id的值表示取得R值的时,对应的回文中心位置。=> id更新与否要看R更新与否

例如:当R[0]=1时,id=0;当R[1]=3时,id=1;当R2]=3时,id=1;当R[5]=9时,id=5;当R[9]=19时,id=9;当R[14]=19时,id=9


求解数组p的几种可能性

可能性1:当前索引的位置i不在回文右边界R之内,采用暴力扩

 

-1

0

1

2

3

4

5

6

str

 

#

1

#

2

#

1

#

R

-1

0

2

2

6

6

6

6

根据上表阐述说明:
1)当索引的位置i=0,R[i-1]=-1时:因为i不在回文右边界的内部,所以对于i=0位置只能采用暴力扩,发现i=0时只能扩到自己,即更新回文右边界R[i]=0;
2)当索引的位置i=1,R[i-1]=0时:因为i还不在回文右边界的内部,所以对于i=1位置只能采用暴力扩,发现i=1时可以扩到下标2的位置,即更新回文右边界R[i]=2;
3)当索引的位置i=2,R[i-1]=2时:因为此时索引的位置在回文右边界之内,所以可以不采用暴力扩【manacher算法就是在这里改进的】

根据该可能性,得到如下代码

//当前索引的位置i不在回文右边界R之内:暴力扩
if(i >R)
{
    p[i] = 1;//这里需要默认的,固定为1
    while((i+p[i]<strlen(man_arr)) && (i-p[i]>-1))
    {
        if(man_arr[i+p[i]] == man_arr[i-p[i]])
            p[i]++;
        else
            break;
    }
}

可能性2:当前索引的位置i在回文右边界R之内

明确一点:i'iid为中心的对称点。由id = (i+i')/2,可得:i'=2*id-i

2.1 str'[i']为中心的最长回文子串包含在L的内部,即i'的回文在L~R内部【时间复杂度:O(1)

结论该种情况下,p[i]=p[i'](由于此时在求解p[i]=?说明p[i']早已经求解完毕)

2.2 str'[i']为中心的最长回文子串扩展到了L的外部,即i'的回文在L~R外部【时间复杂度:O(1)

结论该种情况下,p[i] = R-i

2.3 str'[i']为中心的最长回文子串恰好和L临界,即i'的回文左边界恰好压在L边界上面(从R的右边扩)

结论该种情况下,可以确定p[i]至少是(R-i)或者p[i'](此时二者恰好相等),p[i]还能不能再变大取决于还能不能再扩,即下一步需要采取暴力扩进一步求解p[i]

PS:i的回文半径即p[i]=?,主要关注在L~R的外部,以i为中心的回文串是否可以再扩大【此例中就可以再扩大,但是如果将靠近右边界R的字符‘K’改为s,那么就不能再扩大了】,无需关注L~R的内部。

根据该种可能性,得到以下程序代码

//当前索引的位置i在回文右边界R之内
    //该种情况下,存在3种可能性
    //2.1结论为:p[i] = p[i']
    //2.2结论为:p[i] = R-i;
    //2.3结论为:可以保证此时p[i]的最小值为(R-i)或p[i'],此时二者相等了。
  //综上考虑,仅仅需要求解当前3中可能性中的最小值,然后进行暴力扩就OK了
if(i <= R)
{
    //根据中点公式,逆推可得:i‘=(2*id-i)
    p[i] = p[2*id-i]>(R-i)? (R-i):p[2*id-i];
    //进行暴力扩
    while((i+p[i]<strlen(man_arr)) && (i-p[i]>-1))
    {
        if(man_arr[i+p[i]] == man_arr[i-p[i]])
            p[i]++;
        else
            break;
    }
}

分析各种情况的时间复杂度

(1)很容易得出可能性2.1和2.2的时间复杂度均为O(1)

(2)关于可能性1和可能性2.3的时间复杂度:之前已经明确回文右边界R是只增不减的,可能性1采取的方法是直接暴力扩,即最大的可能性无非就是R从0递增为2N;可能性2.3采取的方法是在原来右边界的基础上,进行暴力扩,即最大的可能性无非也就是R从某一个数递增为2N。

总的来看,可能性1和可能性2.3的时间复杂度都是O(N)。

(3)进一步得出:Manacher算法的时间复杂度为O(N)


初步得到的Manacher算法代码

//初步得到的Manacher算法代码
#include<iostream>
using namespace std;
#include <string.h>
#include <climits>

//转化为Manacher算法的字符串
void convert_manacher(char arr[],char arr_man[], int n)
{
    for(int i = 0; i < n; i++)
    {
        arr_man[2*i+1] = arr[i];
        arr_man[2*i] = '#';
    }
    arr_man[2*n] = '#';//扣边界
}
//Manacher算法的实现
int Manacher(char arr[], char arr_man[], int p[], int n)
{
    //初始化一些引入的基本量
    int i = 0;//当前索引的位置
    int id = -1;//当前的回文右边界的中心
    int R = -1;//当前回文右边界
    int Max = INT_MIN;//存储当前p[i]中最大的一个数

    //转化为manacher算法的字符串
    convert_manacher(arr, arr_man, n);

    for(i = 0; i < strlen(arr_man); i++)
    {
        //当前索引的位置i在回文右边界R之外:暴力扩
        if(i > R)
        {
            p[i] = 1;//这里需要默认的,固定为1
            while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
            {
                if(arr_man[i+p[i]] == arr_man[i-p[i]])
                    p[i]++;
                else
                    break;
            }
        }
        else
        {
//当前索引的位置i在回文右边界R之内
    //该种情况下,存在3种可能性
    //2.1结论为:p[i] = p[i']
    //2.2结论为:p[i] = R-i;
    //2.3结论为:可以保证此时p[i]的最小值为(R-i)或p[i'],此时二者相等了。
  //综上考虑,仅仅需要求解当前3中可能性中的最小值,然后进行暴力扩就OK了
            //根据中点公式,逆推可得:i‘=(2*id-i)
            p[i] = p[2*id-i]>(R-i)? (R-i):p[2*id-i];
            //进行暴力扩
            while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
            {
                if(arr_man[i+p[i]] == arr_man[i-p[i]])
                    p[i]++;
                else
                    break;
            }
        }

        //更新回文右边界R和回文右边界的中心id
        if(p[i]+i > R)
        {
            R = p[i]+i;
            id = i;
        }

        Max = Max>p[i]? Max:p[i];
    }
    //打印回文半径数组p
    for(int i = 0; i < 2*n+1; i++)
        cout << p[i] << " ";
    cout << endl;

    return Max-1;
}

int main()
{
    char arr[] = "Tabcbakkkabcbak";
    int arr_len = strlen(arr);
    int arr_man_len = 2*arr_len+1;
    char* arr_man = new char[arr_man_len];//存储manacher算法的字符串
    int p_len = arr_man_len;
    int* p = new int[p_len];//存储回文半径

    cout << Manacher(arr, arr_man, p, arr_len);

   delete[] arr_man;
    delete[] p;
    return 0;
}

对初步得到的Manacher算法代码进行优化

//对初步得到的Manacher算法代码进行优化
#include<iostream>
using namespace std;
#include <string.h>
#include <climits>

//转化为Manacher算法的字符串
void convert_manacher(char arr[],char arr_man[], int n)
{
    for(int i = 0; i < n; i++)
    {
        arr_man[2*i+1] = arr[i];
        arr_man[2*i] = '#';
    }
    arr_man[2*n] = '#';//扣边界
}
//Manacher算法的实现
int Manacher(char arr[], char arr_man[], int p[], int n)
{
    //初始化一些引入的基本量
    int i = 0;//当前索引的位置
    int id = -1;//当前的回文右边界的中心
    int R = -1;//当前回文右边界
    int Max = INT_MIN;//存储当前p[i]中最大的一个数

    //转化为manacher算法的字符串
    convert_manacher(arr, arr_man, n);

    for(i = 0; i < strlen(arr_man); i++)
    {
        //这一行代码:直接将可能性分为2中情况
        p[i] = R>i? ( p[2*id-i]>(R-i)? (R-i):p[2*id-i] ):1;
        //进行暴力扩
        while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
        {
            if(arr_man[i+p[i]] == arr_man[i-p[i]])
                p[i]++;
            else
                break;
        }

        //更新回文右边界R和回文右边界的中心id
        if(p[i]+i > R)
        {
            R = p[i]+i;
            id = i;
        }

        Max = Max>p[i]? Max:p[i];
    }
    //打印回文半径数组p
    for(int i = 0; i < 2*n+1; i++)
        cout << p[i] << " ";
    cout << endl;

    return Max-1;
}

int main()
{
    char arr[] = "Tabcbakkkabcbak";
    int arr_len = strlen(arr);
    int arr_man_len = 2*arr_len+1;
    char* arr_man = new char[arr_man_len];//存储manacher算法的字符串
    int p_len = arr_man_len;
    int* p = new int[p_len];//存储回文半径

    cout << Manacher(arr, arr_man, p, arr_len);

   delete[] arr_man;
    delete[] p;
    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值