文章目录
- 简要介绍
- 实际应用
- 算法详解
简要介绍
马拉车算法,Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,是一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性O(N)。
实际应用
- 刷题——最长回文子串,回文子字符串的个数。
- 生物中的基因排列可能会用到,DNA/RNA遗传信息。
算法详解
- 本质:利用已有经验不断迭代。
首先我们在求一个字符串的回文子串时,会有一个问题,长度为奇数的和长度为偶数的字符串求法不一。
- 奇数长度——比如:aaa
- 偶数长度——比如:bb
有些朋友到这可能会问了,中心有啥用?
肯定有用了,下面我们就来说,有啥用。
假如给你一个字符串,一步一步找最长回文子串,你会怎么找?
青铜级选手:列出所有的子串,然后判断是否是回文子串,如果是,则计算子串长度,并跟当前的最长回文子串进行比较,如果比之长,则更新。
首先我们需要一层循环,确定长度,再需要一层循环,取遍当前确定长度的所有子串,还需要一层循环确定是否是回文字符串。——O(N3)
那么C语言写出的代码是这样的:
char * longestPalindrome(char * s)
{
int len_s = strlen(s);
int max_len = 0;
int left_str = 0;
int right_str = -1;
for(int len = 0; len < len_s; len++)
{
int left = 0;
int right = len;
while(right < len_s)
{
//判断是否是回文子字符串
int begin = left;
int end = right;
int is_true = 1;
while(begin<=end)
{
if(s[begin]!=s[end])
{
is_true = 0;
break;
}
begin++;
end--;
}
if(is_true)
{
left_str = left;
right_str = right;
break;
}
left++;
right++;
}
}
//开辟数组存储子串
char * str = (char*)malloc(sizeof(char)*(len_s+1));
memset(str,0,sizeof(char)*(len_s+1));
for(int left = left_str,i = 0; left <= right_str; left++,i++)
{
str[i] = s[left];
}
return str;
}
- 虽然能过,但是这是优化了一点,你试着把两个break去掉,就不能过了。
- 暴力求解中心下标可没有用!
黄金级选手:如果是回文子串,那么其中心下标,向两边进行扩展,不还是回文子串吗?这里需要注意的是偶数的中心下标有两种情况,那我们就假设字符串的每一个元素都是中心下标,遍历,然后需要注意第一步确定边界,往两边延伸一次可能是回文,往前面延伸一次也可能是回文。因此我们都要试一次,确定一个边界,再进行两边延伸。总结一下:中心下标一次循环,确定起始边界(两种可能性),然后向两边走,不嵌套的两次循环,因此时间复杂度为——O(N2)。
图解:
因此根据这种思路写出的C代码是这样的:
char * longestPalindrome(char * s)
{
int ce_sut = 0;//center_subscript
int len = strlen(s);
int left_str = 0;
int right_str = 1;
int max_len = 0;
for(ce_sut = 0; ce_sut < len; ce_sut++)
{
//确定起始边界
if(s[ce_sut] == s[ce_sut+1])
{
int begin = ce_sut;
int end = ce_sut + 1;
while(begin>=0&&end<len\
&&s[begin] == s[end])
{
begin--;
end++;
}
//越界或者不是回文字符串
begin++;
end --;
int len_cur = end - begin + 1;
if(len_cur>max_len)
{
max_len = len_cur;
left_str = begin;
right_str = end+1;
}
}
if(ce_sut-1>=0&&ce_sut+1<len&&\
s[ce_sut-1]== s[ce_sut + 1])
{
//确定起始边界
int begin = ce_sut-1;
int end = ce_sut + 1;
while(begin>=0&&end<len\
&&s[begin] == s[end])
{
begin--;
end++;
}
//越界或者不是回文字符串
begin++;
end --;
int len_cur = end - begin + 1;
if(len_cur>max_len)
{
max_len = len_cur;
left_str = begin;
right_str = end+1;
}
}
}
//开辟空间拷贝字符串
char * str = (char*)malloc(sizeof(char)*(len+1));
memset(str,0,sizeof(char)*(len+1));
for(int left = left_str,i = 0; left < right_str; left++,i++)
{
str[i] = s[left];
}
return str;
}
大师级选手:利用manacher——马拉车算法 + 小技巧。
- 小技巧:n个字符,有n+1个空隙,因此可以插入n+1个相同的字符,总共2*n+1个字符,这样是不是就是不管是偶数还是奇数长度的字符,最后都变为了奇数个长度的字符。由于处理过后前后的字符并不相等,因此只管往两边扩!更细一步:为了防止出现越界的情况,我们需要再在开头和结尾放置两个与之都不同两个字符。
- 马拉车算法,前几种只是为了为此铺垫,如果弄清则对现在的理解会有一定的帮助。
我们以一个字符串为例进行介绍
处理之前: wxvxv
处理之后:$#w#x#v#x#v#!
因为既然是往两边扩的话我们也可以只扩右边(对称的性质),只需算一下右边扩的时候与中心坐标对称的的另一个下标的字符是否相等即可。比如中心下标为2,那么如果向右扩到了3,其对称下标为2*2 - 3等于1,那我们设中心下标为Mid,则左边的下标为Left,右边的下标为Right,则关系式就为—————— Right+Left = 2*Mid,则我们可以看这时的回文区间就是【Left,Right】闭区间。
如果这里搞懂了,下面就更容易理解了,我们用这个例子一步一步走。
如果体验过这一个例子,离成功只剩一步之遥了!——总结思路。
第一步:求当前的中心坐标的长度,并保存此长度
第二步: 计算范围[Left,Right]。
第三步:从中心下标+1开始,到Right-1结束,判断当前坐标的对称坐标回文长度,是否等于当前坐标的回文长度——看对称坐标范围是否到边界,如果等于,就继续走,如果不等于就更新中心下标到当前坐标。
第四步:计算出处理后最大回文子串的长度,由处理前的最大回文子串的长度/2即可。因为处理后的最大回文长度必定是奇数,我们举a,处理之后为#a#,处理后的最大回文子串长度为3,/2等于1,再举一个例子,#a#a#,处理之后最大回文子串的长度为5,/2等于2,就等于原来的回文的字串的长度,要说为什么因为是/2,是相0取整的!
因此根据这种思路写出的C代码是这样的。
char * longestPalindrome(char * s)
{
//处理字符串
//开头和结尾需要加上都不同的两个字符,处理越界,这里我加上$和!
//中间隔开的字符我们用#——字符串的长度+1
//不要忘了还要为\0开辟空间
//带上原来的字符串的长度
//总计:n + n + 1 + 2 + 1 == 2*n + 4
int len_s = strlen(s);
char* str = (char*)malloc(sizeof(char)*(2*len_s+4));
memset(str,0,sizeof(char)*(2*len_s+4));
//开头的下标为:0
//结尾的下标为:2*len_s + 2
str[0] = '$';
str[2*len_s + 2] = '!';
printf("%c",str[0]);
//奇数放'#'
//偶数放s的字符
for(int i = 0;i < len_s; i++)
{
str[2*i + 1] = '#';
str[2*i + 2] = s[i];
printf("%c%c",str[2*i+1],str[2*i+2]);
}
//a这里只会处理成$#a!倒数第二个字符没有处理因此我们需要还要加上
str[2*len_s + 1] = '#';
printf("%c%c\n",str[2*len_s + 1],str[2*len_s + 2]);
//manacher思路
//记录中心下标回文字符串长度的数组
int * infor = (int*)malloc(sizeof(int)*(2*len_s+2));
memset(infor,0,sizeof(int)*(2*len_s+2));
//记录最长回文子符串的长度
int max_len = 0;
//记录最长回文字符串的左右下标
int left_str = 0;
int right_str = 1;
for(int mid = 1; mid < 2*len_s + 2;)
{
//先让中心下标往两边进行扩
int left = mid;
int right = mid;
int count = 0;
while(str[left]==str[right])
{
left--;
right++;
count ++;
}
//这里不是回文字符串,但是我们往前再走一步就是回文字符串
left++;
right--;
//我们需要保存一下最大回文半径的值
infor[mid] = count;
//优化
if(right == 2*len_s + 1)
{
break;
}
//这里我们需要判断一下回文字符串的长度大于不大于当前最大的回文字符串的长度
int len_cur = right - left + 1;//这里是左闭右闭
if(len_cur > max_len)
{
max_len = len_cur;
left_str = left;
right_str = right+1;//左闭右开
}
//如果字符串的长度大于3就可能会有
if(mid+1<right)
{
int k = mid + 1;
while( k < right)
{
//优化
//可能的最大半径
int may_len = 2*len_s + 1 - k + 1;
//当前的最大半径
int cur_r = right - mid + 1;;
if(may_len < cur_r)
{
no_need = 1;
break;
}
//有两种情况
//我们需要计算出k的对称下标的左边界与当前中心下标的左边界进行比较。
int n = 2*mid - k;//求出中心下标对应的对称坐标
int r = infor[n];//最大回文子串的半径
int left_n = n - r + 1;//对称下标的左边界
//如果对称下标的左边界小于等于中心下标的左边界,就不一定会有
if(left_n <= left)
{
mid = k;
break;
}
else
{
infor[k] = infor[n];
}
k++;
}
}
else
{
mid++;
}
}
//储存字符串
char* str1 = (char*)malloc(sizeof(char)*(len_s+1));
memset(str1,0,sizeof(char)*(len_s+1));
int i = 0;
for(int left = left_str; left < right_str; left++)
{
if(str[left]!='#')
{
str1[i++] = str[left];
}
}
return str1;
}