回文子串马拉车(Manacher)算法

1 介绍

给定一个字符串,要求输出该字符串所有可以构成回文串的子串

2 暴力计算

2.1 思想

对于每个位置,我们从当前位置向左右两侧扩展,依次比对对称位置上的字符,知道字符不匹配为止,就构成了一个回文子串。
我们需要考虑回文子串的长度是奇数还是偶数
(1)当回文子串长度为奇数时:
例如:abcba
我们只需要从c位置向左右两侧扩展即可
(2)当回文子串长度为偶数时:
例如:abba
我们无法找到中间位置,因此上述从中间位置向两侧扩展无法实行
因此,我们需要对输入的字符串进行加工

2.2 字符串的加工

2.2.1 思想

我们要对回文子串长度为偶数的字符串的长度转换成偶数,因为对于奇数长度的回文子串较好处理。
我们可以利用“奇数 + 偶数 = 奇数”的原理
即通过对偶数长度的字符串添加奇数长度的字符串,使其长度变成奇数,对于奇数长度的字符串添加偶数长度的字符串,其长度仍为奇数。
即 如果一个字符串长度为 N,那么就添加N+1个字符
例如:abba
我们可以添加N+1个“#”,字符串变为:
#a#b#b#a#
例如:abcba
#a#b#c#b#a#
通过上述方法即可全部转换成奇数长度的字符串进行处理
注意:我们添加的字符可以是任意一个字符,无论这个字符在字符串中出现与否,但是添加的字符必须是一致的,即只能添加一种字符,原因是,添加的字符在进行回文匹配时,总是发生添加的字符之间进行匹配,而不会出现添加的字符与原字符串上的字符进行匹配,因此我们只需要保证只添加一种字符而不需要保证添加的字符未在原字符串中出现

2.2.2 字符串转换函数

字符串s是我们输入的字符串,help字符型数组用于存放转换后的字符串

void input(){
    cin >> s;
    n = s.length();
    for(int i = 1; i < 2*n+1;){
      help[i] = s[(i-1)/2];
      help[i-1] = '#';
      i += 2;
    }
    help[2*n] = '#';
    for(int i = 0; i < 2*n+1;){
      cout << help[i] << " ";
      i ++;
    }
    cout << endl;
}

2.3 求当前位置可构成的回文串的长度

2.3.1 思想

从当前位置向左右两侧进行匹配,如果匹配成功则回文串长度加2,继续向左右两侧进行匹配,一旦匹配失败则返回回文串长度

2.3.2 求回文串长度

index:当前位置的下标
L:以index为中心,左侧对称的位置,初始值为index-1
R: 以index为中心,右侧对称的位置,初始值为index+1
res:回文串长度,初始值为1,由于我们对原字符串进行了加工,所以res计算了我们添加的字符,因此最后实际返回的字符串长度需要除以2,即res/2
step:为0,这个参数后续在马拉车算法中需要用到

int search(int index, int step){
    int res = 1 + 2 * step;
    int L = index - 1 - step;
    int R = index + 1 + step;
    while(L >= 0 && R <= 2 * n){
        if(help[L] != help[R])
            break;
        L--, R++;
        res += 2;
    }
    return res/2;
}

2.3.3 求最长的回文子串的长度

int getMax(){
    int max = 0;
    for(int i = 0; i < 2*n+1; i++){
        int temp = search(i, 0);
        max= temp > max ? temp : max;
    }
    return max;
}

2.4暴力计算的缺点

时间复杂度O(N^2)

3 马拉车(Manacher)算法

3.1 介绍

马拉车算法是通过对上述暴力匹配的过程进行加速,从而使得整体的时间复杂度为O(N),字符串匹配KMP算法(点击链接跳转)也是通过对过程进行加速来降低时间复杂度

3.2 相关概念

半径(r):即字符串长度/2
例如:abcba,以c为中心,半径为2
中心(C):即字符串的中心
例如:abcba,以c即为中心
R:当前可以构成的回文串所能达到的最右的位置
例如:#c#c#b#a#A#a#b#c#c#
如果当前查找回文子串查到了A的位置,那么当前所能构成的回文子串可以达到的最右的位置R为最后一个位置

3.3 思想

当前位置下标为cur,初始值为0
当前可以构成的回文子串所能达到的最右位置为R,初始值为-1,对应的回文子串的中心为C,该回文子串的最左端标记为L
当前位置以C为中心对称的位置为inageCur,以imageCur位置为中心构成的回文子串的最左端标记为imageL
res数组用于记录每个下标所对应的位置上可以形成的回文串的半径
整个算法的过程可以分为两部分:
(1)cur > R,即当前位置在R的右边:
C = cur,然后计算当前位置所能构成的回文子串的半径curR,记录res数组,即res[cur] = curR,最后将R移动至相应位置,R = R + curR + 1;
(2)cur <= R,即当前位置在R的左边:
根据imageL可以分为三种情况:
a. imageL > L:此时res[cur] = res[imageCur], R、C保持不变
b. imageL < L:此时res[cur] = R - cur,R、C保持不变
c. imageL = L:此时我们需要计算curR,由于R到cur之间的字符一定是匹配的,因此我们只需要从R之后的位置开始匹配即可,此时上述的search函数中的step = R - cur,如果cur + curR大于R,则更新R、C,C = cur,R = cur + curR,最后res[cur] = curR
整个过程的终止标志是cur指向了字符串末端的下一个位置

3.4 Manacher( )函数

常规版本

int *manacher(){
    int *res = new int[2*n+1];
    int R = -1, L;
    int C, imageCur, imageL;
    int cur = 0, curR;
    while(cur < 2*n+1){
        if(cur > R){
            C = cur;
            curR = search(cur, 0);
            R = R + curR + 1;
            res[cur] = curR;
        }else{
            imageCur = 2 * C - cur;
            L = 2 * C - R;
            imageL = imageCur - res[imageCur];
            if(imageL > L){
                res[cur] = res[imageCur];
            }else if(imageL < L){
                res[cur] = R - cur;
            }else if(imageL == L){
                curR = search(cur, R - cur);
                if(cur + curR > R){
                    C = cur;
                    R = cur + curR;
                }
                res[cur] = curR;
            }
        }
        cur++;
    }
    return res;
}

精简版本

int *manacher2(){
    int *res = new int[2*n+1];
    int C = -1;
    int R = -1;
    for(int cur = 0; cur < 2*n+1; cur++){
        res[cur] = R > cur ? min(res[2*C-cur], R-cur) : 0;
        while(cur + res[cur]+1 < 2*n+1 && cur - res[cur]-1 > -1){
            if(help[cur+res[cur]+1] == help[cur - res[cur]-1]) res[cur]++;
            else break;
        }
        if(cur + res[cur] > R){
            R = cur + res[cur];
            C = cur;
        }
    }
    return res;
}

3.5 打印某个回文子串

C:回文子串的中心
R:回文子串的半径

void printSub(int C, int R){
    for(int i = C - R + 1 ; i < C+ R;){
        cout << help[i];
        i += 2;
    }
    cout << endl;
}

3.6 打印所有回文子串

void printAllSub(int res[]){
    for(int i = 1; i < 2*n+1; i++){
        if(res[i] > 1)
            printSub(i, res[i]);

    }
}

3.7 算法应用

给定一个字符串,尽可能的少添加字符使其整体构成一个回文串

3.7.1 思想

我们只需要找到包含原字符串最后一个字符的回文子串,将原字符串除去该回文子串构成的字符串逆序添加到原字符串末尾即可,我们知道,在马拉车算法中,当我们的R来到最后一个位置时,就是我们需要的那个回文子串,它的中心就是C,因此我们只需要将0到C - res[C](不包括这个位置)之间的字符串逆序添加到原字符串末尾即可

3.7.2 代码

int *getMin(){
    int *res = new int[2*n+1];
    int R = -1, L;
    int C, imageCur, imageL;
    int cur = 0, curR;
    while(cur < 2*n+1){
        if(R == 2*n) break;
        if(cur > R){
            C = cur;
            curR = search(cur, 0);
            R = R + curR + 1;
            res[cur] = curR;
        }else{
            imageCur = 2 * C - cur;
            L = 2 * C - R;
            imageL = imageCur - res[imageCur];
            if(imageL > L){
                res[cur] = res[imageCur];
            }else if(imageL < L){
                res[cur] = R - cur;
            }else if(imageL == L){
                curR = search(cur, R - cur);
                if(cur + curR > R){
                    C = cur;
                    R = cur + curR;
                }
                res[cur] = curR;
            }
        }
        cur++;
    }
    return new int[2] {C, res[C]};
}
void printSuffix(int C, int R){
    int length = 0;
    printSub((2*n+1)/2, (2*n+1)/2);
    for(int i = C - R - 1; i >= 1;){
        cout << help[i];
        i -= 2;
        length++;
    }
    cout << " " << length << endl;
}

3.7.3 实验结果

在这里插入图片描述

4 完整代码

#include <iostream>

using namespace std;

string s;
char help[100];
int n;
int search(int index, int step){
    int res = 1 + 2 * step;
    int L = index - 1 - step;
    int R = index + 1 + step;
    while(L >= 0 && R <= 2 * n){
        if(help[L] != help[R])
            break;
        L--, R++;
        res += 2;
    }
    return res/2;
}

int getMax(){
    int max = 0;
    for(int i = 0; i < 2*n+1; i++){
        int temp = search(i, 0);
        //cout << temp << endl;
        max= temp > max ? temp : max;
    }
    return max;
}

int *manacher(){
    int *res = new int[2*n+1];
    int R = -1, L;
    int C, imageCur, imageL;
    int cur = 0, curR;
    while(cur < 2*n+1){
        if(cur > R){
            C = cur;
            curR = search(cur, 0);
            R = R + curR + 1;
            res[cur] = curR;
        }else{
            imageCur = 2 * C - cur;
            L = 2 * C - R;
            imageL = imageCur - res[imageCur];
            if(imageL > L){
                res[cur] = res[imageCur];
            }else if(imageL < L){
                res[cur] = R - cur;
            }else if(imageL == L){
                curR = search(cur, R - cur);
                if(cur + curR > R){
                    C = cur;
                    R = cur + curR;
                }
                res[cur] = curR;
            }
        }
        cur++;
    }
    return res;
}

void input(){
    cin >> s;
    n = s.length();
    for(int i = 1; i < 2*n+1;){
      help[i] = s[(i-1)/2];
      help[i-1] = '#';
      i += 2;
    }
    help[2*n] = '#';
    for(int i = 0; i < 2*n+1;){
      cout << help[i] << " ";
      i ++;
    }
    cout << endl;
}

void printSub(int C, int R){
    for(int i = C - R + 1 ; i < C+ R;){
        cout << help[i];
        i += 2;
    }
    cout << endl;
}

void printAllSub(int res[]){
    for(int i = 1; i < 2*n+1; i++){
        if(res[i] > 1)
            printSub(i, res[i]);

    }
}

int main()
{

    input();
    cout << "The longest sub-string length is : " << getMax() << endl;
    int *res = manacher();
    for(int i = 0; i < 2*n+1; i++){
        cout << res[i] << " " ;
    }
    cout << endl;
    printAllSub(res);
    return 0;
}

5 实验结果

在这里插入图片描述

6 相关阅读

KMP算法

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值