Manacher 马拉车 算法

Manacher

马拉车算法用于寻找最长回文串。

其思路如下:

First

首先,对于一个字符串,其子串的长度一定是奇数或者偶数。判断回文串过程中也是如此,这就增加了判断的难度。对此,马拉车算法采用如下方法将要找的子串全部变为奇数长度:在每个字符前添加同一个字符#

也就是:对于字符串

"a b a b a"

将其变为:

"#a#b#a#b#a#"

这样,对于任意的原字符串中的字符,其回文串一定是奇数长度:

"#a#" || "#a#b#a#" || "#a#b#a#b#a"

但是在马拉车算法中,我们会在两端添加两个与原字符前的特殊字符不同的字符:

"^#a#b#a#b#a#$"

我们先不看原因,按照这种思路继续走。

Second

接下来我们要在经过预处理之后的字符串中找到最大回文串。

在马拉车算法中要到了一个整型数组:p[size], 以及两个整型变量:c(圆心), r(最大半径)。

string:^#a#b#a#b#a#$
p[]   :0020406040200

我们可以发现以中心字符为圆心,两边的p数组的值是对称的。

利用这个性质,在寻找回文串的过程中我们可以少算一部分比如对于a右侧的字符b, 我们可以用一下a左侧的字符b的p数组的值。

那么这里就会由几个问题:

Problem 1

以右侧b的回文串的半径大于左侧b的回文串长度。

对于上述例子,很可能有下面的情况:

string:^#a#b#a#b#a#b#$
p[]   :00204060_______

这个时候我们看右侧b的回文串长度,明显大于左侧b

那么这个时候我们就应该用中心扩展法继续寻找最大半径。

Problem 2

如果以右侧b为中心得到的回文串长度不足左侧b呢?

比如:

string:^#b#a#b#a#b#a#$
p[]   :002040406040___

此时对于右侧a,回文串半径为左侧a的p数组的值时, 就会导致数组越界。

所以这个时候我们应该多一个判断。

比较对称点的p数组的值和该点到边界的距离的大小,选出其中较小的值。

Third

c圆心和r半径的更新:

我们逐步地得出p[i]的值,当此时的p[i]+i大于当前r,则更新rc的值。

比如:

i     : 012345678901234
string: ^#c#b#c#b#c#b#$
p[]   : 00204060_______
mx = 6;
id = 6;
//此时字符b位置为 8,可以得出p[i]值为6, p[i] + i == 14 > mx;

那么为什么要用p[i]+i

p[i]是当前的最大半径,i为当前位置,那么p[i]+i则是当前字符位置的回文串的最大范围。id作为对称点,自然要跟随更新。

接下来我们来看代码:

Code

class mnc {
public:
    static string init(string s) {
        int len1 = s.size();
        string str;
        str += "^#";
        for (int i = 0; i < len1; i++) {
            str+=s[i];
            str+="#";
        }
        str+="$";
        return str;
    }

    static string MNC(const string& s) {
        if (s.empty()||s.size()<1)return "";
        string str = init(s);
        int size = str.size();
        int id = 0, mx = 0;
        int p[size];
        memset(p, 0, sizeof(p));
        for (int i = 0; i < size; i++) {
            p[i] = mx < i ? 1 : min(mx-i, p[2*id-1]); //如果此时半径小于当前位置则返回 1,因为此时不能使用对称性。
            while (str[i-p[i]]==str[i+p[i]])p[i]++; //这里不需要考虑越界的情况。
            //更新圆心和半径的值。
            if (i+p[i] > mx) {
                mx = i+p[i];
                id = i;
            }
        }
        int len = 0;
        int center = 0;
        //我们可以根据p数组来确定新回文串的最大长度和其中的中心字符位置,那么我们怎么通过这两个值得到原字符串中的回文串呢?我们需要得到原串中的回文串的起始位置和串的长度。
        for (int i = 0; i < size; i++) {
            if (p[i]>len)len = p[i], center = i;
        }
        center = (center - len) / 2;
        return s.substr(center, len-1);
    }
};

寻找原字符串中起始字符的位置

目前我们已知:len, 即新回文串的最大长度;p[i],即新字符串中中心字符对应的回文串的最大半径。

比如,对于子串:

[0020406040200] p
"^#a#b#a#b#a#$" New string
[0123456789012] i
"ababa"         Old string

其新回文半径中中心字符的位置为6,新回文串半径为5,神奇的是,它的值也就是原字符串的长度,那么该回文串的起始字符在原字符串中的位置即为:(i-p[i])/2

同理,下面的字符串中,新半径为3,新位置为3, 则起始字符位置为(3-3)/2

[0204020] p
"#a#b#a#" Ns
[0123456] i
"aba"     Os

那么为什么要在两端加上^$呢?

我们去掉这两个看一看。

[020] p
"#a#" Ns
[012] i
"a"   Os

此时,新半径为2, 新位置为1i-p[i],得到了负数-1。

为了避免这种情况,我们在字符串两端加一个新的特殊字符,同时防止增加的特殊字符影响p数组的值,这两个特殊字符需和字符前添加的特殊字符不同。

当然,你会发现如果你将上面所有的p数组的非零值减一,这样所算出的i-p[i]就是正的。当时试一下的话你会发现,这样会造成数组越界,在上面的代码中我们用到了中心扩展,但是并没有对中心扩展进行范围限制,靠的就是两端的特殊字符一定不同。

所以当然可以不加边界的字符,而在中心扩展时添加条件,但是边界值的确定会有些麻烦,可以自己尝试一下。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值