字符子串和字符子序列的區別
字符字串指的是字符串中連續的n個字符;如palindrome中,pa,alind,drome等都屬於它的字串
而字符子序列指的是字符串中不一定連續但先后順序一致的n個字符;如palindrome中,plind,lime屬於它的子序列,而mod,rope則不是,因為它們與字符串的字符順序不一致。
Manacher's Algorithm
在計算機科學中,最長回文子串或最長對稱因子問題是在一個字符串中查找一個最長的連續的回文的子串,例如“banana”最長回文子串是“anana”。最長回文子串並不一定是唯一的,比如“abracadabra”,沒有超過3的回文子串,但是有兩個回文字串長度都是3:“ada”和“aca”。在一些應用中,我們求出全部的極大回文子串(不被其他回文串包含的回文子串)。
Manacher於[1]發現了一種線性時間算法,可以在列出給定字符串中從任意位置開始的所有回文子串。並且,Apostolico, Breslauer & Galil [2]發現,同樣的算法也可以在任意位置查找全部極大回文子串,並且時間復雜度是線性的。因此,他們提供了一種時間復雜度為線性的最長回文子串解法。另外,Jeuring (1994)[3], Gusfield (1997)[4]發現了基於后綴樹的算法。也存在已知的高效並行算法。
最長回文子串算法不應當與最長回文子序列算法混淆。
要在線性時間內找出字符串的最長回文子串,這個算法必須利用回文和子回文的這些特點和觀察
C++實現
constexpr auto MAXN = (int)7000000; char s[MAXN << 1], str[MAXN]; int RL[MAXN]; int Manacher(void) { size_t len = strlen(str); *s = '#'; for (int i = 0; i < len; i++) { s[(i << 1) + 1] = str[i]; s[(i << 1) + 2] = '#'; }len = (len << 1) + 1; int max = 1, pos, maxRight = -1; memset(RL, 0, sizeof(RL)); for (int i = 0; i < len; i++) { if (i < maxRight) RL[i] = std::min(RL[(pos << 1) - i], maxRight - i); else RL[i] = 1; while (i - RL[i] >= 0 && i + RL[i] < len && s[i - RL[i]] == s[i + RL[i]])++RL[i]; if (i + RL[i] > maxRight) { pos = i; maxRight = i + RL[i]; } max = std::max(max, RL[i] - 1); } return max; }
具體實現過程演示
#include<cstring> #include<cstdio> #include<iostream> #include<algorithm> #include<cmath> #include<queue> #define lson l,mid,idx<<1 #define rson mid+1,r,idx<<1|1 #define lc idx<<1 #define rc idx<<1|1 #define N 100010 #define ll long long using namespace std; char str[N],s[N]; int len[N]={0}; int manachr(){ s[0]='$'; int n=1; for(int i=0;str[i];i++)s[n++]='#',s[n++]=str[i]; s[n++]='#';s[n]='\0'; int MAX=0,id=0,mix=0; for(int i=1;i<n;i++){ if(i<mix)len[i]=min(len[2*id-i],mix-i); else len[i]=1; while(s[i-len[i]]==s[i+len[i]])len[i]++; if(len[i]+i>mix)mix=len[i]+i,id=i,MAX=max(MAX,len[i]); } for(int i=1;i<n;i++)printf("len[%d]=%d\n",i,len[i]); return MAX-1; } //mix 為id為中心 回文串最右端 當i<mix 時 // 利用回文串的性質 len[i]=len[id-(i-id)] 考慮到i會超過mix 和mix-i取最小 // int main(int argc, char const *argv[]) { cin >> str; cout << manachr() << endl; return 0; }
MATLAB實現
function m = Manacher(a) T = ['$#' reshape([a;ones(size(a))*'#'], 1, '') '@']; l = length(T); P = zeros(1, l); mx = 0; %range id = 0; %center for i = 2:l-1 if i < mx P(i) = min(P(2 * id - i), mx - i); else P(i) = 1; end while T(i+P(i)) == T(i-P(i)) P(i) = P(i) + 1; end if P(i) + i > mx mx = P(i) + i; id = i; end end m = max(P)-1;
最長回文子序列
分析
對任意字符串,如果頭和尾相同,那么它的最長回文子序列一定是去頭去尾之后的部分的最長回文子序列加上頭和尾。如果頭和尾不同,那么它的最長回文子序列是去頭的部分的最長回文子序列和去尾的部分的最長回文子序列的較長的那一個。
str[0...n-1]是給定的字符串序列,長度為n,假設f(0,n-1)表示序列str[0...n-1]的最長回文子序列的長度。
1.如果str的最后一個元素和第一個元素是相同的,則有:f(0,n-1)=f(1,n-2)+2;例如字符串序列“AABACACBA”,第一個元素和最后一個元素相同,其中f(1,n-2)表示紅色部分的最長回文子序列的長度;
2.如果str的最后一個元素和第一個元素是不相同的,則有:f(0,n-1)=max(f(1,n-1),f(0,n-2));例如字符串序列“ABACACB”,其中f(1,n-1)表示去掉第一元素的子序列,f(0,n-2)表示去掉最后一個元素的子序列。
設字符串為s,f(i,j)表示s[i..j]的最長回文子序列。
狀態轉移方程如下:
當i>j時,f(i,j)=0。
當i=j時,f(i,j)=1。
當i<j並且s[i]=s[j]時,f(i,j)=f(i+1,j-1)+2。
當i<j並且s[i]≠s[j]時,f(i,j)=max( f(i,j-1), f(i+1,j) )。
由於f(i,j)依賴i+1,所以循環計算的時候,第一維必須倒過來計算,從s.length()-1到0。
最后,s的最長回文子序列長度為f(0, s.length()-1)。
以"BBABCBCAB"為例:
(注:本程序的填表方向斜向左上,即每次從最后一行最后一個數開始,依次向左填寫)
C++實現
int dp[1005][1005]; char str[N]; // dp[i][j] 代表 str[i]到str[j] 中回文子序列的最大長度 // str[i]==str[j] dp[i][j]=dp[i+1][j-1]+2; // str[i]!=str[j] dp[i][j]=max(dp[i][j-1],dp[i+1][j]); int main(){ cin>>(str+1); int n=strlen(str+1); for(int i=n;i>=1;i--){ dp[i][i]=1; for(int j=i+1;j<=n;j++){ if(str[i]==str[j])dp[i][j]=dp[i+1][j-1]+2; else dp[i][j]=max(dp[i+1][j],dp[i][j-1]); } } cout<<dp[1][n]<<endl; }
為進一步減小空間復雜度,我們發現計算第i行時只用到了第i+1行,這樣我們便不需要n行,只需要2行即可。
滾動數組優化
#include <iostream> #include <bits/stdc++.h> using namespace std; int dp[2][1500]; int main() { string str; while(cin >> str) { int len = str.length(); memset(dp,0,sizeof(dp)); int cur = 0; for(int i = len - 1; i >= 0; i--) { cur ^= 1; dp[cur][i] = 1; for(int j = i + 1; j < len; j++) { if(str[i] == str[j]) dp[cur][j] = dp[cur^1][j-1] + 2; else dp[cur][j] = max(dp[cur][j-1],dp[cur^1][j]); } } cout<<dp[cur][len-1]<<endl; } }