131.分割回文串
給定一個字串 s,將 s 分割成一些子串,使每個子串都是迴文串。
返回 s 所有可能的分割方案。
示例: 輸入:"aab" 輸出: [ ["aa","b"], ["a","a","b"] ]
思路
本題這涉及到兩個關鍵問題:
- 切割問題,有不同的切割方式
- 判斷迴文
回溯究竟是如何切割字串呢?
分析一下切割,其實切割問題類似組合問題。
例如對於字串abcdef:
- 組合問題:選取一個a之後,在bcdef中再去選取第二個,選取b之後在cdef中在選組第三個.....。
- 切割問題:切割一個a之後,在bcdef中再去切割第二段,切割b之後在cdef中在切割第三段.....。
感受出來了不?
所以切割問題,也可以抽象為一顆樹形結構,如圖:
遞迴用來縱向遍歷,for迴圈用來橫向遍歷,切割線(就是圖中的紅線)切割到字串的結尾位置,說明找到了一個切割方法。
此時可以發現,切割問題的回溯搜尋的過程和組合問題的回溯搜尋的過程是差不多的。
回溯三部曲
- 遞迴函式引數
全域性變數陣列path存放切割後迴文的子串,二維陣列result存放結果集。 (這兩個引數可以放到函式引數裡)
本題遞迴函式引數還需要startIndex,因為切割過的地方,不能重複切割,和組合問題也是保持一致的。
void process (String s, int startIndex,List<List<String>> res,List<String> path)
- 遞迴函式終止條件
從樹形結構的圖中可以看出:切割線切到了字串最後面,說明找到了一種切割方法,此時就是本層遞迴的終止終止條件。
那麼在程式碼裡什麼是切割線呢?
在處理組合問題的時候,遞迴引數需要傳入startIndex,表示下一輪遞迴遍歷的起始位置,這個startIndex就是切割線。
所以終止條件程式碼如下:
void process (string s, int startIndex) {
// 如果起始位置已經大於s的大小,說明已經找到了一組分割方案了
if (startIndex >= s.size()) {
result.push(path);
return;
}
}
- 單層搜尋的邏輯
來看看在遞迴迴圈,中如何擷取子串呢?
在for (int i = startIndex; i < s.size(); i++)
迴圈中,我們 定義了起始位置startIndex,那麼 [startIndex, i] 就是要擷取的子串。
首先判斷這個子串是不是迴文,如果是迴文,就加入在List<string> path
中,path用來記錄切割過的迴文子串。
程式碼如下:
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是迴文子串
// 獲取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push(str);
} else {
// 如果不是則直接跳過
continue;
}
process(s, i + 1); // 尋找i+1為起始位置的子串
path.remove(paht.size-1); // 回溯過程,彈出本次已經填在的子串
}
注意切割過的位置,不能重複切割,所以,process(s, i + 1); 傳入下一層的起始位置為i + 1。
判斷迴文子串
最後我們看一下回文子串要如何判斷了,判斷一個字串是否是迴文。
可以使用雙指標法,一個指標從前向後,一個指標從後先前,如果前後指標所指向的元素是相等的,就是迴文字串了。
bool isPalindrome(String s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
總結
那麼難究竟難在什麼地方呢?
我列出如下幾個難點:
- 切割問題可以抽象為組合問題
- 如何模擬那些切割線
- 切割問題中遞迴如何終止
- 在遞迴迴圈中如何擷取子串
- 如何判斷迴文