【算法笔记·回文子串匹配】Manacher算法(马拉车算法)

参考资料:
https://www.cnblogs.com/cloudplankroader/p/10988844.html
https://segmentfault.com/a/1190000008484167
感谢大大的优秀博客!🥰

小熊の算法笔记:Manacher算法

从简单的扩散讲起:

首先我们先看最朴素的“扩散”算法。
扩散算法的想法非常的简单。
比如我们有一个序列为:

字符ACDABACD
序号01234567

如果我门以序号4为中心,然后半径为2(直径为3)我们就可以扩散出一个ABA的回文序列。所以我们以每个序号为中心,然后慢慢的扩大回文半径,算法的直观复杂度应该是
O ( n 2 ) O(n^2) O(n2)
在我们想再次的考虑优化算法的复杂度的时候,我们先再看看这个算法的小问题。
那就是其实当序列长度为奇数的偶数的时候,我们应该在3和4之间进行扩散。
我们这里有个小小技巧:
# A # C # D # A # B # A # C # D # \#A\#C\#D\#A\#B\#A\#C\#D\# #A#C#D#A#B#A#C#D#
当我们在每个字符之间加入#的时候,序列总会变成奇书的长度(无论原来的序列是偶数还是奇数)。这样我们就可以继续的扩散了。所以最终我们找到的回文子序列可能是#A#B#A#长度为7,7/2=3才能的到我们不加入# 号时候的长度。

考虑开始加速我们的算法:

我们知道,我们总是想着利用之前算过的结果,来加速程序的计算过程。比如我们学过的动态规划就是利用了这个特点哦!
我们假设有这样的序列:
(下图数据借用自thson同学的数据)

字符:$#a#b#b#a#h#o#p#x#p#o#^
序号 :012345678910111213141516171819202122
半径:11212521212121216121211

我们假设我们现在已经扩散过了序号为5的位置,并且我们找到了ta的最大半径是5,也就是序号1-9的位置。
在这里插入图片描述
现在我们开始探索8这个位置,而从8这个位置扩散,我们或许没有必要那样的笨因为我们知道1-9这个范围的串串是回文的,我们可以充分的回文的特性(正着读和反着读是一样的),看看序号2的半径是多大。这样就免的再次的慢慢的扩散啦!
这里就是我们的加速的过程了。
从我们的样例来看。答案确实是如此。
但是我们不得不再次考虑一个情况:
玩意序列2的半径大于了10-8=2的长度呢?(就是以8扩散的半径会不会大于2)
在这里插入图片描述
比如上图,这个时候1-9确实还是回文序列,0和4和6也确实有可能为a,然后10为c。这个时候,10为c,因为如果10为a的话,那么以5为半径的回文序列半径应该可以更大。
所以这个时候,我们在8的这个位置不能取序列2的半径,只能取10-8=2的半径。

所以:
我们设定半径数组为p。那么:
id为上图5的位置,mx-i相当于10-8=2。计算i相当于id的位置计算公式是id-(i-id)=id+id-i=2*id-i。i是一定大于id的。

  p[i] = min(p[2 * id - i], mx - i);

我们再次的考虑一种情况。在这里插入图片描述
在这里序列0为a然后4,6,10为c是完全可能的,如果我们照搬前面的那条p[i]公式,我们发现8的位置最大半径只能到达2。
其实,在8这个位置还是可以继续扩大它的最大半径,所以我们在加速完了,也不要忘记看看能不能继续扩大半径圈:
(代码摘抄自:thson同学)

while (s_new[i - p[i]] == s_new[i + p[i]]) // 不需边界判断,因为左有 $,右有 ^
    p[i]++;

然后计算完了p[i]我们也要看看能不能继续的更正我们的id中心点,和最大半径后面一位的mx。

		// 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
        // 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
        if (mx < i + p[i]) {
            id = i;
            mx = i + p[i];
        }

至此,我们所有的核心代码已经完全讲解结束啦!
是不是很简单呢!

很感谢thson同学的代码!thson同学赛高赛高!

算法复杂度证明:

马拉车算法的平均复杂度是:
O ( n ) O(n) O(n)
很容易推出 Manacher 算法的最坏情况,即为字符串内全是相同字符的时候。在这里我们重点研究 Manacher() 中的 for 语句,推算发现 for 语句内平均访问每个字符 5 次,即时间复杂度为: T w o r s t ( n ) = O ( n ) T_{worst}(n)=O(n) Tworst(n)=O(n)。同理,我们也很容易知道最佳情况下的时间复杂度,即字符串内字符各不相同的时候。推算得平均访问每个字符 4 次,即时间复杂度为: T b e s t ( n ) = O ( n ) T_{best}(n)=O(n) Tbest(n)=O(n)

综上,Manacher 算法的时间复杂度为 O ( n ) O(n) O(n)
(内容来自:https://www.cnblogs.com/cloudplankroader/p/10988844.html)

完整代码详解:

//参考链接:https://segmentfault.com/a/1190000008484167
#include <algorithm>
#include <cstring>
#include <iostream>

using namespace std;

char s[1000];
char s_new[2000];
int p[2000];

int Init() {
    int len = strlen(s);
    s_new[0] = '$';
    s_new[1] = '#';
    int j = 2;

    for (int i = 0; i < len; i++) {
        s_new[j++] = s[i];
        s_new[j++] = '#';
    }

    s_new[j++] = '^'; // 别忘了哦
    s_new[j] = '\0';  // 这是一个好习惯

    return j; // 返回 s_new 的长度
}

int Manacher() {
    int len = Init(); // 取得新字符串长度并完成向 s_new 的转换
    int max_len = -1; // 最长回文长度

    int id;
    int mx = 0;

    for (int i = 1; i < len; i++) {
        if (i < mx)
            p[i] = min(p[2 * id - i], mx - i); // 需搞清楚上面那张图含义,mx 和 2*id-i 的含义
        else
            p[i] = 1;

        while (s_new[i - p[i]] == s_new[i + p[i]]) // 不需边界判断,因为左有 $,右有 ^
            p[i]++;

        // 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
        // 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
        if (mx < i + p[i]) {
            id = i;
            mx = i + p[i];
        }

        max_len = max(max_len, p[i] - 1);
    }

    return max_len;
}

int main() {
    while (printf("请输入字符串:")) {
        scanf("%s", s);
        printf("最长回文长度为 %d\n\n", Manacher());
    }
    return 0;
}

小熊的代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string.h>
using namespace std;
#define MaxFileLength 10000 + 5
//最长子串长度 最长子串的个数
int iStr_MaxLength = 0, iStr_MaxNumber = 0;
char Str_Ori[MaxFileLength], Str_New[2 * MaxFileLength];
int iStr_P[2 * MaxFileLength] = {0};
void Manacher(const char *, int &, int &);
void MakeNewStr(const char *, char *, int &);

int main() {
    freopen("T01.in", "r", stdin);
    gets(Str_Ori);
    Manacher(Str_Ori, iStr_MaxLength, iStr_MaxNumber);
    printf("%d\n%d", iStr_MaxLength, iStr_MaxNumber);
    return 0;
}

void Manacher(const char *Str_Ori, int &i_MaxLength, int &i_MaxNumber) {
    int iStr_Length = 0; //新创建的字符串长度
    MakeNewStr(Str_Ori, Str_New, iStr_Length);
    int id = 0, mx = 0;
    iStr_P[0] = 1;
    for (int i = 1; i < iStr_Length; i++) {
        if (i < mx) {
            iStr_P[i] = min(mx - i, iStr_P[2 * id - i]);
        } else {
            iStr_P[i] = 1;
        }
        while (Str_New[i - iStr_P[i]] == Str_New[i + iStr_P[i]]) {
            iStr_P[i]++;
        }
        if (i + iStr_P[i] > mx) {
            mx = i + iStr_P[i];
            id = i;
        }
        //答案更新
        if (iStr_P[i] - 1 > i_MaxLength) {
            i_MaxLength = iStr_P[i] - 1;
            i_MaxNumber = 1;
        } else if (iStr_P[i] - 1 == i_MaxLength) {
            i_MaxNumber++;
        }
    }
    /*
    for (int i = 0; i < iStr_Length; i++)
        cout << iStr_P[i];
        */
}

//生成全新的字符串
void MakeNewStr(const char *Str_Ori, char *Str_New, int &i_Len) {
    Str_New[0] = '^';
    Str_New[1] = '#';
    int j = 2;
    for (int i = 0; i < strlen(Str_Ori); i++) {
        Str_New[j++] = Str_Ori[i];
        Str_New[j++] = '#';
    }
    Str_New[j++] = '*';
    i_Len = j;
    Str_New[j] = '\0';
}

Java版本:

(摘抄自: https://www.cnblogs.com/cloudplankroader/p/10988844.html)

public class Manacher {

	public static char[] manacherString(String str) {
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length() * 2 + 1];
		int index = 0;
		for (int i = 0; i != res.length; i++) {
			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
		}
		return res;
	 }

	public static int maxLcpsLength(String str) {
		if (str == null || str.length() == 0) {
			return 0;
		}
		char[] charArr = manacherString(str);
		int[] pArr = new int[charArr.length];
		int C = -1;
		int R = -1;
		int max = Integer.MIN_VALUE;
		for (int i = 0; i != charArr.length; i++) {
			pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
					pArr[i]++;
				else {
					break;
				}
			}
			if (i + pArr[i] > R) {
				R = i + pArr[i];
				C = i;
			}
			max = Math.max(max, pArr[i]);
		}
		return max - 1;
	}

	public static void main(String[] args) {
		String str1 = "abc123321cba";
		System.out.println(maxLcpsLength(str1));
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值