這篇文章是 LeetCode 131. Palindrome Partitioning 的分析與解法。
問題描述
Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
For example, given s = "aab"
, Return
[
["aa","b"],
["a","a","b"]
]
這道題的意思就是將給定的字符串分成迴文串的組合,就像例子中所說,aab
有兩種迴文串組合:aa
,b
和a
,a
,b
.
問題分析
對於這個問題,我們很簡單的將它分解爲兩個子問題:
- 拆分字符串
- 判斷一個字符串是否是迴文串
Step 1 判斷迴文字符串
如果一個字符串正讀和反讀結果都一樣,我們就說它是一個迴文字符串。判斷一個字符串是不是迴文的有很多種方法,我想起來 3 種方法,都會在接下來的文章中進行介紹,並給出源碼(文中的代碼皆爲 C++)。
反轉字符串法
這個方法是最容易理解的,將字符串反轉,如果和原來的字符串一樣,那麼它就是迴文的,這個方法在編碼上也是最簡單的:
bool isPalindrome_reverse(string s, int i, int j){
string r = s;
reverse(s.begin(),s.end());
if(s.compare(r)!=0){
return false;
}
return true;
}
雙指針法
雙指針法是通過兩個指針,一個指向字符串首,另一個指向字符串尾,如果兩個指針指向的字符相同,則兩個指針向中間移動,繼續判斷。
bool isPalindrome_doublepoints(string s, int i, int j){
while(i < j){
if(s[i] != s[j]){
return false;
}
i++;
j--;
}
return true;
}
遞歸法
遞歸法和雙指針法很類似,當前字符串是否迴文取決於首尾字符是否相同,然後遞歸的判斷除去首尾的剩餘字符串是否迴文。
bool isPalindrome_recursion(string s, int i,int j){
if(i == j){
return true;
}
else{
if(s[i] == s[j]){
i++;
j--;
if(i < j){
return isPalindrome_recursion(s, i, j);
}
else{
return true;
}
}
else{
return false;
}
}
}
Step 2 拆分字符串
這一步是這個問題的關鍵,解決拆分字符串的方案也有 2 種:暴力回溯法 和 遞歸法。
暴力回溯法
暴力回溯法比較好理解,它使用的是回溯法的思想,我們窮舉出來字符串的所有子串組合,然後判斷其中的子串是不是迴文的,去掉不符合要求的組合,剩餘的就是我們要的結果。
在進行窮舉的時候,如果遇到不是迴文的子串,我們就進行回溯。
以題目中的aab
爲例:
實現代碼如下:
void backtrace(vector<vector<string>> &vec, vector<string> &temp, string s, int start){
if(start == s.length()){
vec.push_back(temp);
}
else{
for(int i = start; i < s.length(); i++){
if(isPalindrome(s, start, i)){
temp.push_back(s.substr(start, i-start+1));
backtrace(vec, temp, s, i+1);
temp.pop_back();
}
}
}
}
遞歸法
遞歸法的思路是把一個字符串分爲 A+B,如果 A 爲迴文則遞歸的求 B 的迴文組合,然後將 A 和 B 的迴文串組合做笛卡爾積。
以字符串 aabb 爲例:
- 將aabb 分爲 a+abb,然後求 abb 的迴文組合爲[a, b, b], [a, bb],所以做笛卡爾積後爲:[a, a, b,b ], [a, a, bb]
- 將字符串分爲 aa+bb,然後求 bb 的迴文組合爲[b, b], [bb],結果爲[aa, b, b], [aa, bb]
- 將字符串分爲 aab+b,aab 不迴文
- aabb 迴文,結果爲[aabb]
- 最終結果爲:[a, a, b,b ], [a, a, bb], [aa, b, b], [aa, bb], [aabb]
實現代碼如下:
vector<vector<string>> partition_recursion(string s){
vector<vector<string>> vec;
if(s.length() == 0){
return vec;
}
if(isPalindrome_recursion(s, 0, s.length()-1)){
vector<string> temp;
temp.push_back(s);
vec.push_back(temp);
}
for(int i = 1; i <= s.length(); i++){
string left = s.substr(0, i);
if(isPalindrome(left, 0, left.length()-1)){
string right = s.substr(i, s.length()-i);
vector<vector<string>> rightVec = partition_recursion(right);
if(rightVec.size() > 0){
for(int j = 0; j < rightVec.size(); j++){
vector<string> temp;
temp.push_back(left);
for(int x = 0; x < rightVec[j].size(); x++){
temp.push_back(rightVec[j][x]);
}
vec.push_back(temp);
}
}
}
}
return vec;
}
結果測試
將幾種方法組合後的測試結果如下:
反轉字符串法 | 雙指針法 | 遞歸法 |
---|---|---|
暴力回溯法 | 16 ms | 13 ms |
遞歸法 | 89 ms | 76 ms |
我們看到回溯法要明顯優於遞歸的方法。
本文的完整代碼詳見我的 GitHub
本文的版權歸作者 羅遠航 所有,採用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請註明出處。感謝配合!