夕拾演算法進階篇:16)最長迴文子串(動態規劃DP)
給出一個字串S,求S的最長迴文子串的長度。
樣例:字串“PATZJUJZTACCBCC”的迴文子串為“ATZJUJZTA”,長度為9。
如果使用暴力解法,列舉子串的兩個端點i和j,時間複雜度需要O(n^2)。判斷子串是否為迴文需要O(n),總體時間複雜度為O(n^3),使用動態規劃可以達到最優的O(n^3),而使用動態規劃解決最長迴文子串的方法有很多種,下面討論最簡單的一種方法。
令dp[i][j]表示S[i]至S[j]所表示的子串是否為迴文子串,是則為1,不是為0。如此根據S[i]是否等於S[j],可以把問題分為兩類:
(1)S[i]==S[j],那麼只要S[i+1]至S[j-1]是迴文子串,S[i]至S[j]就是迴文子串;如果S[i+1]至S[j-1]是不是迴文子串,則S[i]至S[j]也不是迴文子串。
(2)S[i]!=S[j],那麼S[i]至S[j]一定不是迴文子串。
由此可以 寫出其狀態轉移方程:
邊界:dp[i][i]=1,dp[i][i+1]=(S[i]==S[i+1]?0:1) ps:這裡的dp初始化記錄了長度為1和2的迴文子串
但是這裡還存在一個問題,就是在求dp[i][j]時,無法保證dp[i+1][j-1]已經被計算了,比如先固定i=0,然後j從2開始列舉。當求解dp[0][2]時,的dp[1][1]已經在初始中得到;當求解dp[0][3]時,會轉換求dp[1][2],而dp[1][2]也在初始化是獲得了;當求解dp[0][4]是,轉換求解dp[1][3],但dp[1][3]之前卻沒有被計算出來,因此無法轉移狀態。
根據上面的公式,邊界的長度表示長度為1和2的迴文子串,且每次轉移時都說對子串的長度減1。因此不妨按照子串的長度和子串的初始位置進行列舉,即第一次可以列舉長度為3的子串的dp值,第二次在第一次的基礎上列舉長度為4的子串的dp值....直到列舉到原字串的長度。長度為3的示意圖如下:
根據上面的分析,可以給出下面的程式碼:
#include <iostream> #include <string> #include <algorithm> using namespace std; const int M=1010; int dp[M][M]; int main(){ int i,j,k,s,e,ans=1; string str; cin>>str; int len=str.length(); for(i=0;i<len;i++){ dp[i][i]=1; if(str[i]==str[i+1]){ dp[i][i+1]=1; ans=2; } } for(k=3;k<=len;k++){ //子串的長度 for(i=0;i+k-1<len;i++){ //子串左端點 j=i+k-1; //子串右端點 if(str[i]==str[j]&&dp[i+1][j-1]){ dp[i][j]=1; ans=k; s=i;e=j; //儲存最長迴文子串的下標 } } } for(i=s;i<=e;i++){ cout<<str[i]; } cout<<endl<<ans<<endl; }
下面具體看一個題目:
題目描述
輸入一個字串,求出其中最長的迴文子串。子串的含義是:在原串中連續出現的字串片段。迴文的含義是:正著看和倒著看相同。如abba和yyxyy。在判斷迴文時,應該忽略所有標點符號和空格,且忽略大小寫,但輸出應保持原樣(在迴文串的首部和尾部不要輸出多餘字元)。輸入字串長度不超過5000,且佔據單獨的一行。應該輸出最長的迴文串,如果有多個,輸出起始位置最靠左的。
輸入
一行字串,字串長度不超過5000。
輸出
字串中的最長迴文子串。
樣例輸入
Confuciuss say:Madam,I'm Adam.
樣例輸出
Madam,I'm Adam
這個題目完全可以使用上面的思路來求解,需要注意的是:上面的方法是求最右端的子串,而題目要求是最左端的子串。解決方法也很容易,從字串的末端開始列舉即可:
#include <cstdio>
#include <cstring>
using namespace std;
const int M=5002;
int dp[M][M];
int pos[M]; //儲存新位置到原位置的對映
bool isAlptha(char c){
return 'a'<=c && c<='z' || 'A'<=c && c<='Z';
}
bool isDigit(char c){
return '0'<=c && c<='9';
}
char* toUpper(char* v){
for(int i=0;v[i];i++){
if('a'<=v[i] && v[i]<='z'){
v[i]-=32;
}
}
return v;
}
int main(){
int i,j,k,s,e,ans=1,c=0;
char* str1=new char[M];
char* str=new char[M];
gets(str1);
for(i=0;str1[i];i++){
if(isAlptha(str1[i])||isDigit(str1[i])){
str[c]=str1[i];
pos[c++]=i; //儲存新位置到原位置的對映
}
}
str=toUpper(str);
int len=strlen(str);
for(i=0;i<len;i++){
dp[i][i]=1;
if(str[i]==str[i+1]){
dp[i][i+1]=1;
ans=2;
}
}
for(k=3;k<=len;k++){ //子串的長度
for(j=len-1;j-k+1>=0;j--){ //子串右端點
i=j-k+1; //子串左端點
if(str[i]==str[j]&&dp[i+1][j-1]){
dp[i][j]=1;
ans=k;
s=i;e=j;
}
}
}
s=pos[s]; e=pos[e];
for(i=s;i<=e;i++){
printf("%c",str1[i]);
}
printf("\n");
}