求最長迴文子串的四種算法

問題描述:

輸入一個字符串求出其中最長的迴文子串,在判斷時,應該忽略大小寫,但輸出應該保持原樣。輸入字符不超過5000,輸出迴文長度和迴文字符串。

樣例輸入:Confuciuss say : Madam,I'm Adam

樣例輸出:Madam,I'm Adam

主要給出四種算法解答,1:循環比較,2:擴展比較,3、動態規劃,4、Manacher算法

爲了介紹Manacher算法,我們規定,直接輸入由小寫字母組成的串來處理。本質上是一樣的。

1、循環比較

# include <stdio.h>
# include <string.h>
# include <ctype.h> 
//最長迴文子串 
# define MAXN 5000 + 10
char buf[MAXN],s[MAXN]; //buf存原字符串,s存過濾掉非字母字符的字符串
int p[MAXN];  //記錄字母在原串中的位置,待找到迴文串後輸出。 
//暴力法 
int main(){
	
	int n,m=0,max=0; //max存迴文長度
	int i,j,k;
	int x,y;   //記錄迴文串的上下標 
	
	fgets(buf,sizeof(s),stdin); //推薦使用fgets輸入
	n=strlen(buf);
	//將所有的字母符號都轉變成大寫,便於比較 
	for(i=0;i<n;i++){
		if(isalpha(buf[i])){
			p[m]=i; 
			s[m++] = toupper(buf[i]);
		} 
	}
	
	//利用除去其他字符的數組s判斷迴文
	for(i=0;i<m;i++){
		for(j=i;j<m;j++){
			int ok=1; //默認i <-> j是迴文
			//判斷從i到j是否爲迴文 
			for(k=i;k<=j;k++){//找對稱節點的公式,i+j等價於中心節點的2倍,對稱軸公式,a點關於b點的對稱節點,2*b-a
				if(s[k] != s[i+j-k])
					ok=0; 
			} 
			//如果是迴文更新max的值,max保存最長的迴文串 
			if(ok==1 && j-i+1 > max){
				x=p[i];
				y=p[j];
				max= j - i + 1;
			}
		}
	} 
	printf("max = %d\n",max);
	for(int t=x;t<=wegwaegy;t++){
		printf("%c",buf[t]);
	}
	printf("\n");
	return 0;
}

2、擴展比較

# include <stdio.h>
# include <string.h>
# include <ctype.h> 
//最長迴文子串 

# define MAXN 5000 + 10
char buf[MAXN],s[MAXN];
int p[MAXN];  //記錄字母在原串中的位置,待找到迴文串後輸出。 
//中心擴展法 
int main(){
	
	int n,m=0,max=0;
	int i,j,k;
	int x,y;   //記錄迴文串的上下標 
	
	fgets(buf,sizeof(s),stdin);
	n=strlen(buf);
	//將所有的字母符號都轉變成大寫,便於比較 
	for(i=0;i<n;i++){
		if(isalpha(buf[i])){
			p[m]=i; 
			s[m++] = toupper(buf[i]);
		} 
	}
	
	for(i=0;i<m;i++){//判斷以i節點爲中心的串是不是迴文串 
		//奇數迴文判斷 aba 
		for(j = 0; i-j >= 0 && i+j<m; j++){
			if(s[i-j] != s[i+j])//以i爲節點分別向兩遍擴展j個單位。什麼時候結束判斷?就是for循環給出的條件。 
				break;
			if(j*2+1 > max){
				max=2*j+1;
				x=p[i-j];
				y=p[i+j];
			} 
		}
		//偶數迴文判斷abba 
		for(j=0;i-j>=0 && i+j+1<m;j++){
			//以i和i+1爲中心,向兩邊擴展。 
			if(s[i-j] != s[i+1 + j])
				break;
			if(j*2+2 > max){//更新max 
				max=j*2+2;
				x=p[i-j];
				y=p[i+j+1];
			}
		}
	} 

	printf("max = %d\n",max);
	for(int t=x;t<=y;t++){
		printf("%c",buf[t]);
	}
	printf("\n");
	return 0;
}

3、動態規劃

# include <stdio.h>
# include <string.h>
# include <ctype.h> 
//最長迴文子串 

# define MAXN 5000 + 10
char buf[MAXN],s[MAXN];
int p[MAXN];  //記錄字母在原串中的位置,待找到迴文串後輸出。 
int dp[MAXN][MAXN]; //保存 i <-> j 是否爲迴文串。 

//動態規劃法 
/*
使用dp[i][j]的值表示i到j的串是否爲迴文串,值爲1表示是,值爲0表示不是。
基本思想是,當我要計算i到j的串是否是迴文串時,依賴於兩個條件: 
	1) s[i]與s[j]是否相等.
	2) dp[i+1][j-1]是否是迴文。 

初始條件:
dp[i][i]=1;   //長度爲1的串是確定爲迴文的。 
dp[i][i+1];   //長度爲2的串可用一層循環來判斷。 

狀態轉移方程:
        dp[i][j] = 1;                   if(j-i == 0)  串長爲1
     dp[i][j] = (s[i]==s[j])            if(j-i == 1)  串長爲2
dp[i][j] = (s[i]==s[j] && dp[i+1][j-1]) if(j-i >= 2)  串長大於等於3 

*/
int main(){
	
	int n,m=0,max=0;
	int i,j,k;
	int x,y;   //記錄迴文串的上下標 
	
	fgets(buf,sizeof(s),stdin);
	n=strlen(buf);
	//將所有的字母符號都轉變成大寫,便於比較 
	for(i=0;i<n;i++){
		if(isalpha(buf[i])){ 
			p[m]=i; 
			s[m++] = toupper(buf[i]);
		} 
	}
	memset(dp,0,sizeof(dp));
	
	//初始化i到i是迴文串 
	for(int i=0;i<m;i++){
		dp[i][i] = 1;
		x=p[i]; 
		y=p[i]; 
	} 
	//初始化i到i+1是否爲迴文串 
	for(int i=0;i<m-1;i++){
		if(s[i] == s[i+1]){
			dp[i][i+1]=1;
			max=2;
			x=p[i];   //記錄在原串中的下標。
			y=p[i+1]; 
		}
	} 
	
	//前面長度爲1和長度爲2的串都判斷完畢,現在從長度爲3開始。 
	for(int len=3;len<=m;len++){
		//從串的首端開始判斷,每次len個長度,往後移動。 
		for(int i=0; i<m-len+1; i++){
			int j=i+len-1;
			if(s[i] == s[j] && dp[i+1][j-1] == 1){
				dp[i][j]=1;
				max=len;
				x=p[i];
				y=p[j];
			}
		}
	} 
	
	printf("max = %d\n",max);
	for(int t=x;t<=y;t++){
		printf("%c",buf[t]);
	}
	printf("\n");
	return 0;
}

4、Manacher算法

簡介:Manacher算法,俗稱馬拉車算法。這個算法的基本思想是,後面字符迴文的判斷可以利用前面的結果。爲了介紹Manacher算法本質,我們只求迴文長度,迴文串的輸出稍加修改即可。

1)算法首先有一個預處理,在字符串中間加入字符#或者其他,這樣可以將偶數字符串和奇數字符串都轉換成奇數串,統一操作。另外也可以在字符串前面加上@符號,這樣下標變成1 - n-1,避免不必要的麻煩。

2)算法利用P[]數組記錄字符串中每個字符的迴文半徑,例如下面的字符串串迴文長度,

S     #   a  #  b  #   b  #  a   #  b  #  c  #   b   #  a  #
P     1   2  1  2  5   2  1  4   1  2  1  6   1  2   1  2  1

3)因此算法的關鍵就是求P數組。


基本思想就是,前面的P值已經求出來了,後面要求的P值就可以利用前面已經計算出來的結果。而這個利用是有一定條件的,比如上圖,下標等於11的字符它的迴文半徑很長,左邊[2-10],右邊[12-20],這兩個區間的字符串對應相等。那麼在求i=[12 - 19]的迴文半徑時候我們可否利用一下區間[2 - 10]的結果,因爲他們的迴文半徑早已經求出來了,比方說在求i=13字符b的迴文半徑時,和i=9時的迴文半徑是相同的,所以等於1。那是不是就可以一直這麼算下去,算到i=19?其實不然,當i=15時,對稱的i=7就是圖中的 i' ,它的迴文半徑爲7.這個值超出了[2....11]的範圍,也即超出了[11....20]的範圍。當沒超出範圍時,因爲對稱可以直接賦值,現在超出了,就說明在R右側的部分還沒有比較,並不知道是不是迴文。那隻能老老實實比較了,這裏不用從i=15開始分別向兩邊比較,因爲以i=15爲中心的字符串已經有一部分是對稱的了,因爲i'的迴文半徑爲7,而你這裏R-i = 20-15=5,所以分別從i=20開始向右,i=15-(20-15) 向左比較就可以了。因此這裏的 R-i 和P[i']的值決定了不同的情況。

要保存這個R值,並不斷更新對應代碼 if(i + p[i] > R ),它是已求的所有P[i]值中,最右的邊界,並不一定是P[i]值最大就是最右邊界。初始值爲1,代表第一個字符的迴文半徑爲1,默認爲最長的迴文半徑。

從前往後遍歷,依次求每個字符的迴文值,看R和i的關係來確定從何處開始匹配,也就是給P[i]一個初始值,如果i在R右邊時,只能重新匹配,沒有信息可以利用,p[i]=1; 如果在左側,還要看i對應i' 的P[i']的值,R-i和P[i']取較小值。對應min(R-i,p[i_mirror])。

# include <stdio.h>
# include <string.h>
# include <math.h>
# include <algorithm>  
using namespace std;

const int MAX=1000000;
int len,p[2*MAX];
char str[MAX],newstr[2*MAX];
/*
Manacher算法求最長迴文子串 
*/
int main(){
	
	int i,j,k,t,n,ans = 0,temp =1;
	while(scanf("%s",&str)){
		if(strcmp(str,"END") == 0) break;
	
		//預處理字符串 
		n=strlen(str);
		j=0;
		newstr[j++]='@';
		for(i=0;i<n;i++){
			newstr[j++]='#';
			newstr[j++]=str[i];
		}
		newstr[j++]='#';
		newstr[j]='\0';
		n=j;
		
		//算法開始 
		ans = 0;
		int R=1,id=1;
		for(i=1;i<n;i++){
			
			int i_mirror = 2*id-i; //equals to i' = id-(i-id)
			p[i] = (R>i) ? min(R-i,p[i_mirror]) : 1;
			//往兩邊擴展比較 
			while(newstr[i+p[i]] == newstr[i-p[i]]){
				p[i]++;
			} 
			if(i+p[i] > R){
				id=i;
				R=i+p[i];
			}
		}
		
		//輸出 
		int maxlen = 0;
		for(i=0;i<n;i++){
			if(p[i]>maxlen)
				maxlen=p[i];
			printf("%d ",p[i]);
		}
		printf("Case %d: %d\n",temp++,maxlen-1);			
	}
}



參考文章:http://blog.csdn.net/zhangjun03402/article/details/50514722








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章