題目:給定一個字符串s,找出s中的最長迴文子串;
迴文是指正着讀和倒着讀,結果一些樣,比如abcba或abba。
方法:暴力法,DP法, 中心擴展法,manacher算法
1、暴力法
求出每一個子串,之後判斷是不是迴文,找到最長的那個。
求遍歷每一個子串的方法的時間複雜度O(n^2),判斷每一個子串是不是迴文的時間複雜度是O(n),所以總時間複雜度爲O(n^3)。
思想:
1)從最長的子串開始,遍歷所有該原字符串的子串;
2)每找出一個字符串,就判斷該字符串是否爲迴文;
3)子串爲迴文時,則找到了最長的迴文子串,因此結束;反之,則繼續遍歷。
string findLongestPalindrome(string &s)
{
int length = s.size();//字符串長度
int maxlength = 0;//最長迴文字符串長度
int start;//最長迴文字符串起始地址
for (int i = 0; i < length; i++)//起始地址
{
for (int j = i + 1; j < length; j++)//結束地址
{
int index1, index2;
// 對每個子串都從兩邊開始向中間遍歷 判斷是不是迴文
for (index1 = i, index2 = j; index1 < index2; index1++, index2--)
{
if (s.at(index1) != s.at(index2))
break;
}
// 若index1=index2,表示串是類似於abcba這種類型;若大於,則是abccba這種類型
if (index1 >= index2 && j - i > maxlength)
{
maxlength = j - i + 1;
start = i;
}
}
}
if (maxlength > 0)
return s.substr(start, maxlength);//求子串
return NULL;
}
2、動態規劃
迴文字符串的子串也是迴文,比如P[i,j](表示以i開始以j結束的子串)是迴文字符串,那麼P[i+1,j-1]也是迴文字符串。這樣最長迴文子串就能分解成一系列子問題了。這樣需要額外的空間O(n^2),算法複雜度也是O(n^2)。
首先定義狀態方程和轉移方程:
P[i,j]=false: 表示子串[i,j]不是迴文串。
P[i,j]=true: 表示子串[i,j]是迴文串。
P[i,i]=true: 當且僅當P[i+1,j-1] = true && (s[i]==s[j]) 否則p[i,j] =false;
string findLongestPalindrome(string &s)
{
const int length = s.size();
int maxlength = 0;
int start;
bool P[50][50] = { false };
// 子串長度爲1和爲2的初始化
for (int i = 0; i < length; i++)//初始化準備
{
P[i][i] = true;
if (i < length - 1 && s.at(i) == s.at(i + 1))
{
P[i][i + 1] = true;
start = i;
maxlength = 2;
}
}
// 使用上述結果可以dp出子串長度爲3~len -1的子串
for (int len = 3; len <= length; len++)//子串長度
{
for (int i = 0; i <= length - len; i++)//子串起始地址
{
int j = i + len - 1;//子串結束地址
if (P[i + 1][j - 1] && s.at(i) == s.at(j))
{
P[i][j] = true;
maxlength = len;
start = i;
}
}
}
if (maxlength >= 2)
return s.substr(start, maxlength);
return NULL;
}
3、中心擴展
中心擴展就是把給定的字符串的每一個字母當做中心,向兩邊擴展,這樣來找最長的子迴文串。算法複雜度爲O(n^2)。
但是要考慮兩種情況:
1、長度爲奇數: aba
2、長度爲偶數: abba
思想:
1)將子串分爲單核和雙核的情況,單核即指子串長度爲奇數,雙核則爲偶數;
2)遍歷每個除最後一個位置的字符index(字符位置),單核:初始low = 初始high = index,low和high均不超過原字符串的下限和上限;判斷low和high處的字符是否相等,相等則low++、high++(雙核:初始high = 初始low+1 = index + 1);
3)每次low與high處的字符相等時,都將當前最長的迴文子串長度與high-low+1比較。後者大時,將最長的迴文子串改爲low與high之間的;
4)重複執行2)、3),直至high-low+1 等於原字符串長度或者遍歷到最後一個字符,取當前截取到的迴文子串,該子串即爲最長的迴文子串。
string findLongestPalindrome(string &s)
{
const int length = s.size();
int maxlength = 0;
int start;
// 類似於aba這種情況,以i爲中心向兩邊擴展 長度爲奇數
for (int i = 0; i < length; i++)
{
int j = i - 1, k = i + 1;
while (j >= 0 && k < length&&s.at(j) == s.at(k))
{
if (k - j + 1 > maxlength)
{
maxlength = k - j + 1;
start = j;
}
j--;
k++;
}
}
// 類似於abba這種情況,以i,i+1爲中心向兩邊擴展 長度爲偶數
for (int i = 0; i < length; i++)
{
int j = i, k = i + 1;
while (j >= 0 && k < length&&s.at(j) == s.at(k))
{
if (k - j + 1 > maxlength)
{
maxlength = k - j + 1;
start = j;
}
j--;
k++;
}
}
if (maxlength > 0)
return s.substr(start, maxlength);
return NULL;
}
4、Manacher法
Manacher算法首先通過在每個字符的兩邊都插入一個特殊的符號(未在字符串中出現過),將所有可能的迴文子串都轉換成奇數。例如"aba"的兩邊都插入字符'#'就變成了"#a#b#a#"。爲了更好處理越界問題,可以在字符串的開始和結尾加入另一個特殊字符,例如在"#a#b#a#"的開始和結尾插入字符'%'變成"%#a#b#a#%"。這個算法就是利用已有迴文串的對稱性來計算的,由於Manacher算法只有在遇到還未匹配的位置時才進行匹配,已經匹配過的位置不再匹配,所以對於對於字符串S的每一個位置,都只進行一次匹配,所以算法的總體複雜度爲O(n)。
1.思想:
1)將原字符串S的每個字符間都插入一個永遠不會在S中出現的字符(本例中用“#”表示),在S的首尾也插入該字符,使得到的新字符串S_new長度爲2*S.length()+1,保證Len的長度爲奇數(下例中空格不表示字符,僅美觀作用);
例:S: a a b a b b a
S_new: # a # a # b # a # b # b # a #
2)根據S_new求出以每個字符爲中心的最長迴文子串的最右端字符距離該字符的距離,存入Len數組中,即S_new[i]—S_new[r]爲S_new[i]的最長迴文子串的右段(S_new[2i-r]—S_new[r]爲以S_new[i]爲中心的最長迴文子串),Len[i] = r - i + 1;
S_new: # a # a # b # a # b # b # a #
Len: 1 2 3 2 1 4 1 4 1 2 5 2 1 2 1
Len數組性質:Len[i] - 1即爲以Len[i]爲中心的最長迴文子串在S中的長度。在S_new中,以S_new[i]爲中心的最長迴文子串長度爲2Len[i] - 1,由於在S_new中是在每個字符兩側都有新字符“#”,觀察可知“#”的數量一定是比原字符多1的,即有Len[i]個,因此真實的迴文子串長度爲Len[i] - 1,最長迴文子串長度爲Math.max(Len) - 1。
3)Len數組求解(線性複雜度(O(n))):
a.遍歷S_new數組,i爲當前遍歷到的位置,即求解以S_new[i]爲中心的最長迴文子串的Len[i];
b.設置兩個參數:sub_midd = Len.indexOf(Math.max(Len)表示在i之前所得到的Len數組中的最大值所在位置、sub_side = sub_midd + Len[sub_midd] - 1表示以sub_midd爲中心的最長迴文子串的最右端在S_new中的位置。起始sub_midd和sub_side設爲0,從S_new中的第一個字母開始計算,每次計算後都需要更新sub_midd和sub_side;
c.當i < sub_side時,取i關於sub_midd的對稱點j(j = 2sub_midd - i,由於i <= sub_side,因此2sub_midd - sub_side <= j <= sub_midd);當Len[j] < sub_side - i時,即以S_new[j]爲中心的最長迴文子串是在以S_new[sub_midd]爲中心的最長迴文子串的內部,再由於i、j關於sub_midd對稱,可知Len[i] = Len[j];
當Len[j] >= sub.side - i時說明以S_new[i]爲中心的迴文串可能延伸到sub_side之外,而大於sub_side的部分還沒有進行匹配,所以要從sub_side+1位置開始進行匹配,直到匹配失敗以後,從而更新sub_side和對應的sub_midd以及Len[i];
d.當i > sub_side時,則說明以S_new[i]爲中心的最長迴文子串還沒開始匹配尋找,因此需要一個一個進行匹配尋找,結束後更新sub_side和對應的sub_midd以及Len[i]。
#define min(x, y) ((x)<(y)?(x):(y))
#define max(x, y) ((x)<(y)?(y):(x))
string findLongestPalindrome3(string s)
{
int length = s.size();
for (int i = 0, k = 1; i < length - 1; i++)//給字符串添加 #
{
s.insert(k, "#");
k = k + 2;
}
length = length * 2 - 1;//添加#後字符串長度
int *rad = new int[length]();
rad[0] = 0;
for (int i = 1, j = 1, k; i < length; i = i + k)
{
while (i - j >= 0 && i + j < length&&s.at(i - j) == s.at(i + j))
j++;
rad[i] = j - 1;
//鏡像,遇到rad[i-k]=rad[i]-k停止,這時不用從j=1開始比較
for (k = 1; k <= rad[i] && rad[i - k] != rad[i] - k; k++)
rad[i + k] = min(rad[i - k], rad[i] - k);
j = max(j - k, 0);//更新j
}
int max = 0;
int center;
for (int i = 0; i < length; i++)
{
if (rad[i] > max)
{
max = rad[i];
center = i;
}
}
return s.substr(center - max, 2 * max + 1);
}