回文字符串問題
一、動態規划法
定義boolean型的 p[i][j],為 Si 到 Sj 是否為回文,true 說明 Si 到 Sj 是回文字符串
則有,P[i,j] = (P[i + 1, j - 1] && Si ==Sj)
初始條件p[i, i] = true, p[i,i+1] = Si==Si+1
動態規划的思想是首先判斷相鄰的字符串是否是回文,然后繼續判斷連續的三個字符是否是回文,然后是四個,…,直到判斷完整個字符串
時間復雜度O(n2),空間復雜度O(n2)
代碼實現:
publicstaticString longestPalindromeDP(String str){
if(str ==null|| str.length()<=0)returnnull;
int len = str.length();
int startIndex =0;
int maxLen =1;
boolean[][] p =newboolean[len][len];
for(int i =0; i < len; i++){
for(int j =0; j < len; j++){
if(i == j){
p[i][j]=true;
continue;
}
p[i][j]=false;
}
}
for(int i =0; i < len -1; i++){
//相鄰的相同
if(str.charAt(i)== str.charAt(i+1)){
p[i][i+1]=true;
startIndex = i;
maxLen =2;
}
}
for(int i =3; i <= len; i++){
for(int j =0; j < len - i +1; j++){
//當前判斷回文長度為i,起始位置為j
int currLast = j + i -1;
if(str.charAt(j)== str.charAt(currLast)&& p[j+1][currLast-1]){
p[j][currLast]=true;
startIndex = j;
maxLen = i;
}
}
}
return str.substring(startIndex, startIndex+maxLen);
}
二、中心檢測法
回文字符串的特點是以中心對稱,從0開始依次遍歷字符串,每次以選取的點為中心,向兩邊檢測,判斷是否符合回文字符串。
時間復雜度O(n2),空間復雜度O(1)
publicstaticString longestPalindromeCerter(String str){
if(str ==null|| str.length()<=0)returnnull;
String longest = str.substring(0,1);
for(int i =0; i < str.length()-1; i++){
//獲得以i為中心的回文字符串
String s = getPalindromeCerter(str, i, i);
if(s.length()> longest.length()){
longest = s;
}
//獲得以i和i+1為中心的回文字符串
s = getPalindromeCerter(str, i, i +1);
if(s.length()> longest.length()){
longest = s;
}
}
return longest;
}
//獲得以i,j為中心的回文字符串i==j時,就是以i為中心的回文字符串
privatestaticString getPalindromeCerter(String str,int i,int j){
while(i >=0&& j < str.length()&& str.charAt(i)== str.charAt(j)){
i --;
j ++;
}
return str.substring(i +1, j);
}
三、添加輔助標志
首先我們把字符串S改造一下變成T,改造方法是:在S的每個字符之間和S首尾都插入一個”#”。這樣做的理由你很快就會知道。
例如,S=”abaaba”,那么T=”#a#b#a#a#b#a#”。
想一下,你必須在以Ti為中心左右擴展才能確定以Ti為中心的回文長度d到底是多少。(就是說這一步是無法避免的)
為了改進最壞的情況,我們把各個Ti處的回文半徑存儲到數組P,用P[i]表示以Ti為中心的回文長度。那么當我們求出所有的P[i],取其中最大值就能找到最長回文子串了。
對於上文的示例,我們先直接寫出所有的P研究一下。
i = 0 1 2 3 4 5 6 7 8 9 A B C
T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0
顯然最長子串就是以P[6]為中心的”abaaba”。
你是否發現了,在插入”#”后,長度為奇數和偶數的回文都可以優雅地處理了?這就是其用處。
現在,想象你在”abaaba”中心畫一道豎線,你是否注意到數組P圍繞此豎線是中心對稱的?再試試”aba”的中心,P圍繞此中心也是對稱的。這當然不是巧合,而是在某個條件下的必然規律。我們將利用此規律減少對數組P中某些元素的重復計算。
我們來看一個重疊得更典型的例子,即S=”babcbabcbaccba”。
上圖展示了把S轉換為T的樣子。假設你已經算出了一部分P。豎實線表示回文”abcbabcba”的中心C,兩個虛實線表示其左右邊界L和R。你下一步要計算P[i],i圍繞C的對稱點是i’。有辦法高效地計算P[i]嗎?
我們先看一下i圍繞C的對稱點i’(此時i’=9)。
據上圖所示,很明顯P[i]=P[i’]=1。這是因為i和i’圍繞C對稱。同理,P[12]=P[10]=0,P[14]=P[8]=0。
現在再看i=15處。此時P[15]=P[7]=7?錯了,你逐個字符檢測一下會發現此時P[15]應該是5。
為什么此時規則變了?
如上圖所示,兩條綠色實線划定的范圍必定是對稱的,兩條綠色虛線划定的范圍必定也是對稱的。此時請注意P[i’]=7,超過了左邊界L。超出的部分就不對稱了。此時我們只知道P[i]>=5,至於P[i]還能否擴展,只有通過逐個字符檢測才能判定了。
在此例中,P[21]≠P[9],所以P[i]=P[15]=5。
我們總結一下上述分析過程,就是這個算法的關鍵部分了。
if P[ i’ ] < R – i,
then P[ i ] ← P[ i’ ]
else P[ i ] ≥ R - i. (此時要穿過R逐個字符判定P[i]).
很明顯C的位置也是需要移動的,這個很容易:
如果i處的回文超過了R,那么就C=i,同時相應改變L和R即可。
每次求P[i],都有兩種可能。如果P[i‘] < R – i,我們就P[i] = P[i’]。否則,就從R開始逐個字符求P[i],並更新C及其R。此時擴展R(逐個字符求P[i])最多用N步,而求每個C也總共需要N步。所以時間復雜度是2*N,即O(N)。
時間復雜度O(n),空間復雜度O(n)
實現:
/**
* 通過添加輔助標識,來獲得最長回文子串
* @param str
* @return
*/
publicstaticString longestPalindromeAddTag(String str){
if(str ==null|| str.length()<=0)returnnull;
StringBuilder sb = addTag(str);
int[] p =newint[sb.length()];//以i為中心的,左右半邊的回文子串長度(包括#)
p[0]= p[sb.length()-1]=0;
int center =0;int r =0;
for(int i =1; i < sb.length()-1; i++){
int i_mirror = center -( i - center);
int diff = r - i;
if(i_mirror>=0){
if(p[i_mirror]< diff) p[i]= p[i_mirror];
else{
center = i;
p[i]= diff;
int pre = i - p[i]-1;//往前
int after = i + p[i]+1;//往后
while(pre >=0&& after < sb.length()&&
sb.charAt(pre)== sb.charAt(after)){
p[i]++;
pre --;//往前
after ++;//往后
}
r = i + p[i];//當前中心的右邊緣
}
}else{
center = i;
p[i]=0;
int pre = i - p[i]-1;//往前
int after = i + p[i]+1;//往后
while(pre >=0&& after < sb.length()&&
sb.charAt(pre)== sb.charAt(after)){
p[i]++;
pre --;//往前
after ++;//往后
}
r = i + p[i];//當前中心的右邊緣
}
}
int maxLen =0;
int index =0;
for(int i =0; i < sb.length(); i++){
if(p[i]> maxLen){
maxLen = p[i];
index = i;
}
}
int start =(index >>1)-(maxLen >>1);
intlast=(index >>1)+(maxLen >>1);
if((index &0x01)==1)last++;
return str.substring(start,last);
}
/**
* 向字符串中插入#,如 ab,則返回 #a#b#
* @param str
* @return
*/
privatestaticStringBuilder addTag(String str){
StringBuilder sb =newStringBuilder();
sb.append('#');
for(int i =0; i < str.length(); i++){
sb.append(str.charAt(i));
sb.append('#');
}
return sb;
}
四、字符串變成回文字符串需要添加的字符數
f(i,j)表示s[i..j]變為回文串需要添加的最少字符數。
f(i,j)=0if i>=j
f(i,j)=f(i+1,j-1)if i<j and s[i]==s[j]
f(i,j)=min(f(i,j-1),f(i+1,j))+1if i<j and s[i]!=s[j]
實現:
/**
* 添加多少字符串使字符串變為回文串
* @param str
* @return
*/
publicstaticint addTobePalindrome(String str){
if(str ==null|| str.length()<=0)return0;
int len = str.length();
int[][] p =newint[len][len];
for(int i =0; i < len; i++){
for(int j =0; j < len; j++){
p[i][j]=0;
}
}
for(int i =2; i <= len; i++){
for(int j =0; j < len - i +1; j++){
int currLast = j + i -1;
if(str.charAt(j)== str.charAt(currLast)){//判斷s[i...j]需要添加的字符個數
p[j][currLast]= p[j+1][currLast-1];
}else{
p[j][currLast]=1+(p[j][currLast -1]< p[j+1][currLast]? p[j][currLast -1]: p[j+1][currLast]);
}
}
}
return p[0][len-1];
}
參考:
[1]http://www.cnblogs.com/bitzhuwei/p/Longest-Palindromic-Substring-Par-I.html
[2]http://www.cnblogs.com/bitzhuwei/p/Longest-Palindromic-Substring-Part-II.html