中间扩散算法和Manacher算法求解最长回文子串

1.问题背景

给出一个字符串,找到这个字符串中,最长的回文子串
例如字符串:S = “aacabdkacaa”
最长的回文子串是:“aca”
其中1<=S.length <=1000
S仅由字母数字组成

2.中间扩散算法

2.1.思路:

把字符串中每一个字符当作一个中心点,然后同时向两边搜索,但是会有一个问题,当这个字符串的个数可能是一个奇数也可以是一个偶数的情况,所以需要做对奇数个数 字符串与偶数个数字符串分别处理

奇数:从自己的位置开始向两边搜索, i 与 i 判断,i - 1与i + 1判断,i - 2 与 i + 2 判断…

偶数:从自己的位置与i+1位置开始向两边搜索, i 与 i+1 判断,i - 1 与 i + 2 判断,i - 2 与 i + 3判断

最后,将每一个字符向两边扩散得到的开始位置与终止位置,终止位置 减 起始位置就是这个字符向两边扩散的长度,每一次向两边扩散会得到这个一个长度,当下一次得到的这个长度比上一次得到的这个长度要长,则更新记录长度的变量

2.2代码解析

2.2.1 两边搜索

//将求得的每一个字符为中心的最长回文串 
void expandAroundCenter(char* s, int left, int right, int* l, int* r) {
	while(left >= 0 && right < strlen(s) && s[left] == s[right]) { left--; right++; }
	*l = left + 1; *r = right - 1; //l指向子回文串的第一个字母位置  r指向子回文串最后一个字母位置 
} 

这个函数有5个参数,第一个参数S, 传递过来的原字符串,第二个参数left,往左遍历的起始位置,第三个参数right,往右遍历的起始位置,第三第四参数,用来保存传递回去的最终左边和右边停下来的位置。

left是不断往左走,right是不断往右走,所以需要对left和right进行合法的判断,是不是在遍历的范围内,然后左边那个元素与右边那个元素进行判断,是否相同,如果相同则,往左走的继续往左一位,往右走的继续往右走一位,不断的这样循环判断,最终以当前字符为中心,left指向的左边的下标,right指向的右边的下标,通过两个指针传递回去

2.2.2 核心代码

char* longestPalindrome(char* s) {
	int left1, right1;
	int left2, right2;
	int start = 0, end = 0; 
	for (int i = 0; s[i]; i++) {
		
		//中间扩散法分别求奇数串和偶数串的每一个字符最长子回文串 
		expandAroundCenter(s, i, i, &left1, &right1);    //奇数 
		expandAroundCenter(s, i, i + 1, &left2, &right2);//偶数
		
		if (end - start < right1 - left1) {
			end = right1;
			start = left1;
		} 
		if (end - start < right2 - left2) {
			end = right2;
			start = left2;
		} 
	}
	return newstr(s, start, end);
}

其中以下这段代码的意思:如果当前right1与left1的区间长度 大于 end与start的区间长度,则更新新的区间长度

if (end - start < right1 - left1) {
	end = right1;
	start = left1;
} 

3.中间扩散法整体代码

/*
LeetCode 5.
输入:s = "babad"
输出:"bab"
解释:"aba" 


输入:s = "cbbd"
输出:"bb"
*/ 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

//截取指定区间的串返回
char* newstr(char* s, int start, int end) {
	char* str = (char*)malloc(sizeof(char) * 1000 + 1);
	int k = 0;
	for (int i = start; i <= end; i++) {
		str[k++] = s[i];
	}
	str[k] = '\0';
	return str;
} 

//将求得的每一个字符为中心的最长回文串 
void expandAroundCenter(char* s, int left, int right, int* l, int* r) {
	while(left >= 0 && right < strlen(s) && s[left] == s[right]) { left--; right++; }
	*l = left + 1; *r = right - 1; //l指向子回文串的第一个字母位置  r指向子回文串最后一个字母位置 
} 
char* longestPalindrome(char* s) {
	int left1, right1;
	int left2, right2;
	int start = 0, end = 0; 
	for (int i = 0; s[i]; i++) {
		
		//中间扩散法分别求奇数串和偶数串的每一个字符最长子回文串 
		expandAroundCenter(s, i, i, &left1, &right1);    //奇数 
		expandAroundCenter(s, i, i + 1, &left2, &right2);//偶数
		
		if (end - start < right1 - left1) {
			end = right1;
			start = left1;
		} 
		if (end - start < right2 - left2) {
			end = right2;
			start = left2;
		} 
	}
	return newstr(s, start, end);
}

int main() {
	char s[] = "cbbd";
	printf("字符串:%s \n最长回文子串为:%s", s, longestPalindrome(s));
	
	return 0;
}

3.1 运行结果

在这里插入图片描述

4.Manacher算法

 Manachar算法其实名叫马拉车算法,这个算法是在原本的中间扩散法进行优化的一个算法,而中间扩散算法平均的时间复杂度在O(n²),而马拉车算法优化后的时间复杂度是在O(n),马拉车算法需要理解几个东西。

1.在一段区间内求 i 的对称点怎么计算
2.回文半径是什么
3.d[i]数组是做什么的

5.Manacher算法基本思路

在这里插入图片描述
在区间[L,R],要进行分类讨论
1.当 i 在[L,R]区间内,那么 i 的对称点 i1 是R - i + L
  1.1 如果d[R - i + L ] < R - i + 1,则d[i] = d[R - i + L]
  1.2 如果d[R - i + L ] >= R - i + 1,则d[i] = R - i + 1,然后再继续暴力枚举

2.如果 i 不在[L,R]区间内, 那么直接进行暴力枚举

3.如果 i + d[i] - 1 > R
根据已经求出的d[i]判断是否需要更新新的区间 ,L = i - d[i] + 1, R = i + d[i] - 1

5.1 i 的对称点

例如在这么一个区间内 1 2 3 4 5 6 7 8
L是2 R是8
当i是6,则求 i 的对称点
R - i: 8 - 6得到2,意思就是从i到R的距离就是2,表示偏移量
那么从L + 这个偏移量就是 i 的对称点, i1 = R - i + L = 8 - 6 + 2 = 4

i 的对称点: R - i + L

5.2 回文半径

对于一个回文的字符串:abcba,它的回文半径是3,abc或者cba

回文半径:回文字符串的长度 / 2
也可以算成3,也可以算成2,算成3是加上自己就是3,如果不算自己就是2

5.3 d[i]数组

例如有这么一个字符串: S = “abcdcba”,假设下标从1开始,S[ 1 ] = ‘a’, S[ 1 ] = ‘b’
d[ 1 ] = 1, d[ 2 ] = 1, d[ 3 ] = 1
d[4] = 4
这个d[ i ]数组的意思是:记录以当前 i 位置的字符作为回文串的中点,统计当前回文串的半径
S[ 4 ]是d,那么半径是 abcd 和 dcba是4,而其他字母假设回文中心点,只有自己,所以就是1

d[ i ]用来记录当前 i 位置字符为回文中点,记录当前 i 的回文半径

5.4 理解基本思路

在这里插入图片描述

在区间[L,R],要进行分类讨论
1.当 i 在[L,R]区间内,那么 i 的对称点 i1 是R - i + L
  1.1 如果d[R - i + L ] < R - i + 1,则d[i] = d[R - i + L]

这句话的意思:当如果 i 的半径长度 小于 i 到 R 的偏移量,则d[ i ]的值直接取 i 的对称点,就是半径长度  d[ i1 ], 当 i + 半径小于R,直接取对称点,因为 i 就是 i1

  1.2 如果d[R - i + L ] >= R - i + 1,则d[i] = R - i + 1,然后再继续暴力枚举

如果i + 半径大于R或者等于R, 则取i到R的距离作为d[ i ]的值,然后继续暴力

2.如果 i 不在[L,R]区间内, 那么直接进行暴力枚举

3.如果 i + d[i] - 1 > R
根据已经求出的d[i]判断是否需要更新新的区间 ,L = i - d[i] + 1, R = i + d[i] - 1

6.核心代码

//马拉车算法求解d数组 
void Manacher(char* s, int n, int d[]) {
	int L, R;
	d[1] = 1;
	for (int i = 2, L, R = 1; i <= n; i++) {
		if (i <= R) d[i] = min(d[R - i + L], R - i + 1);
		while(s[i - d[i]] == s[i + d[i]]) d[i]++;
		if (i + d[i] - 1 > R) {
			L = i - d[i] + 1;
			R = i + d[i] - 1;
		}
	}
	for(int i = 1; i<= n; i++) {
		printf("%3d", d[i]);
	} 
}

需要注意的是,需要对原字符串做一个预处理,头和尾都需要插入一个特殊字符,两个字符中间也要插入特殊字符
原字符串 a b c b a
处理后 # a # b # c # b # a
这样处理的好处是,不管原字符串是奇数个数还是偶数个数,最终都会是奇数个数字符串

7 代码

/*
LeetCode 5.
输入:s = "babad"
输出:"bab"
解释:"aba" 


输入:s = "cbbd"
输出:"bb"
*/ 	

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

//将原字符串插入特殊符号#,保证原字符串的字符个数不管是奇数还是偶数最终字符个数是奇数
char* newString(char* s) {
	int index = 1;
	char* newstr = (char*)malloc(sizeof(char) * strlen(s) * 2 + 3);
	newstr[index++] = '#';
	for (int i = 0; s[i]; i++) {
		newstr[index++] = s[i];
		newstr[index++] = '#';
	}
	
	newstr[index] = '\0';
	
	return newstr;
} 
int min (int a, int b) {
	if (a < b) return a;
	return b;
}
//马拉车算法求解d数组 
void Manacher(char* s, int n, int d[]) {
	int L, R;
	d[1] = 1;
	for (int i = 2, L, R = 1; i <= n; i++) {
		if (i <= R) d[i] = min(d[R - i + L], R - i + 1);
		while(s[i - d[i]] == s[i + d[i]]) d[i]++;
		if (i + d[i] - 1 > R) {
			L = i - d[i] + 1;
			R = i + d[i] - 1;
		}
	}
	for(int i = 1; i<= n; i++) {
		printf("%3d", d[i]);
	} 
}
//返回最大值下标 
int maxlue_dex(int d[], int n) {
	int index_max = 1;
	for (int i = 1; i <= n; i++) {
		if (d[index_max] <= d[i]) index_max = i;
	}
	return index_max;
}
//拷贝指定区间上面的字符串,返回一个新的字符串
char* cpy_newstr(char* str, int dex, int d[]) {
	int section = strlen(str) * 2 - 1;
	int index = 0; 
	char* newstr = (char*)malloc(sizeof(char) * section); //开辟一个回文子串长度的空间
	for (int i = dex - d[dex] + 1; i<= dex + d[dex] - 1; i++) {
		if (str[i] != '#') newstr[index++] = str[i];
	}
	newstr[index] = '\0';
	return newstr;
}
char* longestPalindrome(char* s) { 
	int d[2002] = { 0 };
    char* newstr = newString(s); 
    Manacher(newstr, strlen(s) * 2 + 1, d);
    
    
    //获取以当前i位置向两边扩散的最长的回文串下标 
    int dex = maxlue_dex(d, strlen(s) * 2 + 1); 
    return cpy_newstr(newstr, dex, d);
} 
int main() {
	char s[] = "aacabdkacaa";
	printf("字符串:%s \n最长回文子串为:%s", s, longestPalindrome(s));
	
	return 0;
}

运行结果:
在这里插入图片描述

8.推到过程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值