Manacher算法学习与应用

Manacher算法学习与应用

提出问题

给出一个字符串,要求字符串的最长回文子串,怎么办?

最暴力的做法(O(n^3))

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <cstring>
#include <cstdlib>

bool isPalindrome(const char* str, int n) {
	int j = 0, upper = (n + 1) / 2;
	while (j < upper) {
		if (str[j] != str[n - j - 1])
			return false;
		j++;
	}
	return true;
}

char* getLongestPalind(const char* str) {
	int len = (int)strlen(str), ans = 0;
	int retp = 0, retq = 0, k = 0;
	char* ret = (char*)malloc(sizeof(char) * len);
	for (int i = 0; i < len; ++i) {
		for (int j = i; j < len; ++j) {
			if (isPalindrome(str + i, j - i + 1)) {
				if (ans < j - i + 1) {
					ans = j - i + 1;
					retp = i, retq = j;
				}
			}
		}
	}
	for (int s = retp; s <= retq; ++s)
		ret[k++] = *(str + s);
	ret[k] = '\0';
	return ret;
}

int main() {
	char s[100] = { '\0' };
	scanf("%s", s);
	printf("%s\n", getLongestPalind(s));
	return 0;
}

简单测试一个数据:
Manacher算法学习与应用

再去leetcode上找一个题来测试一下:

Manacher算法学习与应用
在这个问题里就会超时~~
Manacher算法学习与应用

改进的做法(O(n^2))

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <cstring>
#include <cstdlib>

char* getLongestPalind(char* str) {
	int len = (int)strlen(str), ans = 0, l = 0, r = 0, t = 0;
	char* ret = (char*)malloc(sizeof(char) * len + 1);
	if (0 == len || 1 == len)
		return str;
	for (int i = 0; i < len; ++i) {
		int j = 0;
		while (i - j >= 0 && i + j < len && str[i - j] == str[i + j])
			j++;
		if (ans < j) {
			ans = j;
			l = i - j + 1, r = i + j - 1;
		}
	}
	for (int k = l; k <= r; ++k)
		ret[t++] = str[k];
	ret[t] = '\0';
	return ret;
}

int main() {
	char s[100] = { '\0' };
	scanf("%s", s);
	printf("%s\n", getLongestPalind(s));
	return 0;
}

这下再去交一下,看看结果哈:

Manacher算法学习与应用
这样就AC了哈~~
这种算法很简单,复杂度是平方级别~
思想就是选定一个中心,然后向两边扩展,求得最长的回文子串~

最好的做法(O(n))

详解Manacher算法

第一步:扩展原串(padding)

首先,大家伙看完上面那一份代码之后,就会晓得:在处理回文子串的时候,奇数串和偶数串处理起来是不一样的,因为上面那个做法是枚举子串的中心字符,然后两边扩展。但是偶数串是没有中心字符的,它的中心是长度除以二后,的左右两个字符,那样处理是不方便的,所以我们先统一两者,给字符串填充相同,且不会出现在原串中的字符 ‘#’ ,但是上面那份代码在扩展的时候,还是要注意边界,给了些判断条件,防止越界~所以我们干脆再给头尾加上与原串字符、’#’ 都不相同的字符,比如 ‘@’ 、 ‘$’ 等等!

实现第一步的代码:(注意看小注释哦~)

void padding(char* s){
    int len = strlen(s), i;
    pad[0] = '$';
    // pad数组是事先声明好了的~最好是静态初始化哦
    pad[1] = '#';
    for(i = 0;i < len;++i){
        pad[2 * i + 2] = s[i];
        pad[2 * i + 3] = '#';
    }
    pad[2 * len + 2] = '@';
    k = 2 * len + 2;
    pad[k + 1] = '\0';
    // 别忘了字符串要以 '\0' 结尾哦~
}

第二步:分析manacher算法原理

初步理解原理

在manacher算法中,我们需要维护一个数组 p[] ,设定 p[i] 的含义是:以 i 为中心的最长回文半径的长度~设定右边界是rx,初始情况rx = 0,由于之前字符串进行了填充,于是长度由 length = 2 * length + 1(刨去了首位防越界的限定符!),所以这个 p[i] - 1 就会是 length * 2,那么也就是原串回文半径的两倍,这个两倍要注意重复计算了回文中心的字符,于是这个值减1就是最长回文串的长度了~

细究实现细节

大家看到了,能够维护出 p[] 就能得到每个回文子串的长度!!!那么,如何维护它就是技术的重点!

Manacher算法学习与应用

上面这个图是举的一个求 p[] 的示例,我们先来分析一下这个 p[] 如何求得!

Manacher算法学习与应用
对于任意一个位置 i ,假设 j 是 i 关于 id 的对称点,也就是说j = 2 * id - 1,如果 i <= mx,那么对应的 j 也就会在 mx 的对称点的右侧。现在来分类讨论:

第一:如果 j 为中心的最长回文子串左边界在 mx 的对称点的右边

此时,设 L = 以 j 为中心的最长回文子串的左边界 - 1,R = 以 j 为中心的最长回文子串的右边界 + 1
肯定有Str[L] != Str[R] ,否则最长回文子串的左边界就会是 L 了!因为此时必有: p[i] >= p[j] ,如果p[i] 会比 p[j] 大,那么以 i 为中心的最长回文子串的右边界会和它的左边界相同。又因为以 j 为中心的回文子串的左边界 + 1 会和 p[j] 的右边界相同,所以如果 p[i] > p[j],就会让 L != 以 j 为中心的最长回文子串的左边界 - 1,这就矛盾了!!!

所以此时:p[i] = p[j]

第二:如果 j 为中心的最长回文子串左边界在 mx 的对称点或其左边

首先,如果 j 为中心的最长回文子串左边界超过了 mx 的对称点,举个例子:

e [ a b c b a e f (g) f e a b c b a] y

这里,我们的回文串是以 ‘g’ 为中心的,假设 Str[j] = Str[i] = 'c' ,则此回文子串左侧的 ‘e’ 就有可加入 j 为中心的最长回文子串。但是呢?以 i 为中心的最长回文子串就不可以加入右侧的 ‘y’ !所以此时,p[i] = mx - i;
但是如果 j 为中心的最长回文子串左边界恰恰在 mx 的对称点呢?比如:

y [ a b c b a e f (g) f e a b c b a] e

像这种情况呢???那么初始情况就只能是:p[i] = p[j]; !大家可以发现,这两种情况啊在不确定的时候,就是取两者的最小值的!!!于是得到结论:

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

这id啊,就是上一步得到的最长回文子串的中心~

第三:如果这个 i > mx 呢?

这个就很简单啦~只需要给 p[i] = 1 初始化就好!因为在初始情况下,我们认为一个字符本身就是回文串~

第四:还是讨论之前的一个问题

我们知道,在第二点里,以 i 为中心的最长回文子串右边界是有可能扩展的,所以需要加个循环判断一下,考虑可能会增加 p[i] 的值。其实这个每种情况都需要考虑,所以要加上:

while (pad[i - p[i]] == pad[i + p[i]])
    p[i]++;

第三步:实现Manacher算法的代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

char pad[2005], str[1005];
int k, p[2005];

int min(int a, int b) {
    return a < b ? a : b;
}

void padding(char* s) {
    int len = strlen(s), i;
    pad[0] = '$';
    pad[1] = '#';
    for (i = 0; i < len; ++i) {
        pad[2 * i + 2] = s[i];
        pad[2 * i + 3] = '#';
    }
    pad[2 * len + 2] = '@';
    k = 2 * len + 2;
    pad[k + 1] = '\0';
}

char* manacher(char* s) {
    if (strlen(s) == 0 || strlen(s) == 1)
        return s;
    int i, rx = 0, id, ans = 0, pos = 0, len = 0, n = 0;
    padding(s);
    for (i = 1; i < k; ++i) {
        if (rx >= i)
            p[i] = min(rx - i, p[2 * id - i]);
        else
            p[i] = 1;
        while (pad[i - p[i]] == pad[i + p[i]])
            p[i]++;
        if (i + p[i] > rx) {
            rx = i + p[i];
            id = i;
        }
        if (ans < p[i]) {
            ans = p[i];
            pos = i, len = p[i] - 1;
        }
    }
    for (int t = pos - len; t < pos + len + 1; ++t)
        if ('#' != pad[t])
            str[n++] = pad[t];
    str[n] = '\0';
    return str;
}

int main() {
    char t[100] = { '\0' };
    scanf("%s", t);
    printf("%s\n", manacher(t));
    return 0;
}

提交到leetcode上,看看这次的效果如何:
Manacher算法学习与应用
这下效果很明显啊,从40ms到4ms,这是因为复杂度也下降了一个数量级啊!哈哈,再练一道模板题~

练习一道模板题:

洛谷 P3805 【模板】manacher算法

Manacher算法学习与应用

AC代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

const int maxn = 2.2e7 + 5;
char pad[maxn], str[maxn / 2];
int k, p[maxn];

int min(int a, int b) {
    return a < b ? a : b;
}

void padding(char* s) {
    int len = strlen(s), i;
    pad[0] = '$';
    pad[1] = '#';
    for (i = 0; i < len; ++i) {
        pad[2 * i + 2] = s[i];
        pad[2 * i + 3] = '#';
    }
    pad[2 * len + 2] = '@';
    k = 2 * len + 2;
    pad[k + 1] = '\0';
}

int manacher(char* s) {
    if (!s)
        return 0;
    if (strlen(s) == 1)
        return 1;
    int i, rx = 0, id, ans = 0, pos = 0, len = 0, n = 0;
    padding(s);
    for (i = 1; i < k; ++i) {
        if (rx >= i)
            p[i] = min(rx - i, p[2 * id - i]);
        else
            p[i] = 1;
        while (pad[i - p[i]] == pad[i + p[i]])
            p[i]++;
        if (i + p[i] > rx) {
            rx = i + p[i];
            id = i;
        }
        if (ans < p[i]) 
            ans = p[i];
    }
    return --ans;
}

int main() {
    scanf("%s", str);
    printf("%d\n", manacher(str));
    return 0;
}

Manacher算法的扩展应用

Leetcode 647 回文子串

题目分析:

这个题要求所有回文子串的个数,这个其实本质还是Manacher算法,因为每次维护起来的 p[i] 都是以当前第 i 个位置为中心的最长回文串的长度 + 1,因为我们知道,这个最长回文串左右各减少一个,它仍然是回文串!!于是我们只需要把 (p[i] - 1) / 2 就可以得到所有长度大于1 的回文串的个数,但是要知道一个字符也是一个回文串~所以必须向上取整,也就是:p[i] / 2 !!

解题代码如下:

char pad[2005];
int k, p[2005];

int min(int a, int b){
    return a < b ? a : b;
}

void padding(char* s){
    int len = strlen(s), i;
    pad[0] = '$';
    pad[1] = '#';
    for(i = 0;i < len;++i){
        pad[2 * i + 2] = s[i];
        pad[2 * i + 3] = '#';
    }
    pad[2 * len + 2] = '@';
    k = 2 * len + 2;
    pad[k + 1] = '\0';
}

int countSubstrings(char* s){
    int i, rx = 0, id, ans = 0;
    padding(s);
    for(i = 1;i < k;++i){
        if(rx > i)
            p[i] = min(rx - i, p[2 * id - i]);
        else
            p[i] = 1;
        while(pad[i - p[i]] == pad[i + p[i]])
            p[i]++;
        if(i + p[i] > rx){
            rx = i + p[i];
            id = i;
        }
        ans += p[i] / 2;
    }
    return ans;
}

解题结果截屏如下:

Manacher算法学习与应用

写在后面

关于Manacher算法的基础讲解和代码实现,以及一个基础应用就讲到这里了。由于本人能力有限,如有不正确或者有瑕疵的地方,还请指正!!谢谢!

上一篇:H5移动端实现pad弹窗手上滑上移动,手下滑下移动(和高德地图上的展示框类似)


下一篇:吐血整理!宅家36天咸鱼翻身入职腾讯,绝对干货