您的位置:首页 > 编程语言 > Go语言

Manacher's ALGORITHM: O(n)时间求字符串的最长回文子串

2015-10-30 10:57 274 查看
首先:大家都知道什么叫回文串吧,这个算法要解决的就是一个字符串中最长的回文子串有多长。这个算法可以在O(n)的时间复杂度内既线性时间复杂度的情况下,求出以每个字符为中心的最长回文有多长,

这个算法有一个很巧妙的地方,它把奇数的回文串和偶数的回文串统一起来考虑了。这一点一直是在做回文串问题中时比较烦的地方。这个算法还有一个很好的地方就是充分利用了字符匹配的特殊性,避免了大量不必要的重复匹配。先列一下这个算法的核心

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

max = max[p[id]+id]

其中max代表的是现在所求的会文子串在整个字符串中的最大位置,id代表max所在字串的中心位置 ,p[id]代表这个子串的半径。

上面说的有点混乱,现在我重新整理一下思路

首先我们在字符串的每个字符中间插入一个字符。(注意这个字符是要在字符串中不存在的)

以一个字符为中心去搜索它的回文串。(这个算法的核心就在如何减少搜索的范围,每次从最应该搜索的长度开始搜索)

既然是如何减少范围的,请看这个p[j] = min(p[2*id-i],max-i)(条件是max>i)

分析一下,当max>i,意味着你要搜索的这个字符(str[i])已经包含在某一个回文字串中了,既然是一个回文字串,你们我们可以知道,在id的左侧一定存在一个字符(str[j])与我们现在要搜索的字符是相同的,而且这个字符一定也是属于相同的回文字串的。那么我们就可以知道j到id和id到i的字符是相同的。

这个时候我们假设以str[j]为中心存在一个回文字串,这个回文字串的长度不超过j-(id-p[id]),注意id-p[id]表示我们记录的那个回文字串的左边界。那么是不是以i为中心也存在一个这样的回文字串呢?毕竟他们是对称的。这个就是p[2*id-i]的由来了,因为id-j=i-id(即j=2*id-i)。



那如果str[j]为中心的回文字串的长度超过了j-(id-p[id])呢?,这个时候我们没办法保证超出的部分,因为对于我们来说,我们找到的回文字串最右边在max。这个时候我们就要怪怪的去匹配超过(j-(id-p[id]))的部分,这部分的长度我们可以知道的,因为j=2*id-i,那么(j-(id-p[id]))就等于2*id-i-id+p[id]=id+p[id]-i,而id+p[id]=max,那么可以得出这段的长度为max-i。

那我们为什么要从中间取到最小值呢?因为我们并不能保证以str[j]为中心存在一个回文字串。当它不存在回文字串的时候当然是p[2*id-i]即p[j]是等于1,如果存在回文字串的话就是我们上面讨论的情况。



。。。

4. 这个时候求出了p[i],这个p[i]其实代表的就是以i为中心存在的长度为p[i]的回文字串。但是这个回文字串只是我们自己已知的最短的回文字串。还需要已i为中心,p[i]长度为半径继续向外搜索。

5. 如果搜索到的i+p[i]>max,也就是说我们求的所有回文字串的最右边变化的时候我们要记录下这个最大的,这样我们就可以使用上面的第3步来处理了。这样就可以减少我们处理的次数。

6. 好了,贴一段别人的代码吧

void pk()
{
int i;
int mx = 0;
int id;
for(i=1; i<n; i++)
{
if( mx > i )
p[i] = MIN( p[2*id-i], mx-i );
else
p[i] = 1;
for(; str[i+p[i]] == str[i-p[i]]; p[i]++)
;
if( p[i] + i > mx )
{
mx = p[i] + i;
id = i;
}
}
}


放个hiho的AC代码

package com.liubin.study.hiho;

import java.util.Arrays;
import java.util.Scanner;

/**
* @Description:
*  题目链接:
*      https://hihocoder.com/contest/hiho1/problem/1 *  最长回文子串
*  解答思路:
*      http://blog.csdn.net/liu20111590/article/details/49508013 * @ClassName: Hiho1
* @Package: com.liubin.study.hiho
* @Author: liubin@hsyuntai.com
* @Date: 2017/3/20 10:35
* @Copyright: 版权归 HSYUNTAI 所有
* <ModifyLog>
* @ModifyContent:
* @Author:
* @Date: </ModifyLog>
*/
public class Hiho1 {

/** 输入的原字符串. */
private String orginal;

/**
* !!!注意
* 此处的属性变量仅仅是manacher算法使用
*
*/

/** 字符串填充. */
private static final Character FILL_CHAR = '#';

/** 计算半长度. */
private int[] len;

/** 新的字符串(填充了字符串). */
private char[] elementData;

public void setOrginal(String orginal) {
this.orginal = orginal;
}

public String getOrginal() {
return orginal;
}

/**
* 获取对应的长度
* 此处以后可以使用策略模式,
* 使用不同的最长回文子串的算法
* @return
*/
public Integer solve(){
this.fillChar();
return this.manacher();
}

/**
* 填充数据
*/
private void fillChar(){
this.elementData = new char[2*getOrginal().length()+1];

this.elementData[0] =  FILL_CHAR;
for(int i=0;i < getOrginal().length(); i++){
this.elementData[2*i+1] = getOrginal().charAt(i);
this.elementData[2*i+2] = FILL_CHAR;
}

//        System.out.println(Arrays.toString(this.elementData));
}

/**
* manacher算法
* @return
*/
private Integer manacher(){
this.len  = new int[2*getOrginal().length()+1];

//最长回文子串长度
Integer maxLen = 1;
//maxId + f(maxId) = rightId
Integer maxId = 0;
//最右边的ID
Integer rightId = 0;

this.len[0] = 1;
for(int i = 1; i < this.elementData.length; i++){

//manacher算法的核心
if(rightId > i){
this.len[i] = Math.min(this.len[2*maxId - i],rightId - i);
}else{
this.len[i] = 1;
}

//以i为中心的,求最大的长度
for(;i-this.len[i] >= 0
&& i+this.len[i] < this.elementData.length
&& this.elementData[i-this.len[i]] == this.elementData[i+this.len[i]];
this.len[i]++);

if(this.len[i] + i > rightId){
rightId = this.len[i] + i;
maxId = i;
}

//整个子串中最长回文子串的长度
if(this.len[i] > maxLen){
maxLen = this.len[i];
}
}
//释放内存
this.elementData = null;
this.len = null;
return maxLen - 1;
}

public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int num = in.nextInt();
Hiho1 hiho1 = new Hiho1();
for(int i = 0 ; i < num ; i++){
String str = in.next();
hiho1.setOrginal(str);
System.out.println(hiho1.solve());
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息