程式碼手記筆錄——遞歸回溯
遞歸回溯的兩種寫法
(1)遞歸回溯無非是向下走一直到遞迴基,然後向右走。這個 向右 的過程可以通過 for 迴圈控制 (迭代向右法) ,也可以通過控制下標遞迴控制 (下標遞歸向右法)
(2)遞歸回溯函式裡的 push() 與 pop() 個數一定是相等的。【注意:if-else裡的 pop() 只能算一次】
(3)為減少遞迴-回溯的時間,優化的方法是 剪枝
(4)遞歸回溯步驟是向下-向右,其實就暗含著搜尋答案存在 可以向右 的性質。對於給定的不滿足 向右遞迴就可以搜尋到全部答案 陣列,首先要讓該陣列滿足該條件。如對求 sum 的組合元素需 先對 nums 進行排序: 【39 組合總和 】、【40 組合總和 II】
(5)遞歸回溯為了避免 ans 的答案出現重複,需在 向右走
(6)對於某些題目,在到達向下遞迴基時,無需向右走。【93 復原 IP 地址】
迭代向右法
迭代向右法模板如下:【push() 與 pop() 個數需相等】
void backtracking(vector<vector<int>> &ans, vector<int> &item, int idx, int n, int k) { if (...) // 向右遞迴出口 return; for (....) { // (含剪枝條件)// for 迴圈向右同層遞迴 item.push_back(curIdx); // 若不是出口就可以直接 push_back if (....) // 不滿足一組遞迴答案,繼續向下遞迴 backtracking(ans, item, curIdx+1, n, k); // 從上到下走 else ans.push_back(item); item.pop_back(); // push() 與 pop() 對應 } }
章節題目總結
39 組合總和
class Solution { public: vector<vector<int>> combinationSum(vector<int>& candidates, int target) { vector<vector<int>> ans; vector<int> item; // 這道題有點坑,我們在進行操作之前必須使得給定陣列是升序排序的 sort(candidates.begin(), candidates.end()); backtracking(ans, item, 0, candidates, target); return ans; } private: void backtracking(vector<vector<int>> &ans, vector<int> &item, int idx, vector<int>& candidates, int target) { if (idx >= candidates.size()) return; int curNum = candidates[idx]; // 採用push-pop結構 item.push_back(curNum); target -= curNum; if (target <= 0) { // 停止向下遞迴 if (target == 0) ans.push_back(item); item.pop_back(); target += curNum; } else { backtracking(ans, item, idx, candidates, target); // 向下遞迴 item.pop_back(); target += curNum; backtracking(ans, item, idx+1, candidates, target); // 向右邊遞迴 } } };
40 組合總和 II
備註:排序預處理+while() 向右走的策略防止答案重複
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> item;
sort(candidates.begin(), candidates.end());
backtracking(ans, item, 0, candidates, target);
return ans;
}
private:
void backtracking(vector<vector<int>> &ans, vector<int> &item, int idx, vector<int>& candidates, int target) {
if (idx >= candidates.size())
return;
int curNum = candidates[idx];
item.push_back(curNum);
target -= curNum;
if (target <= 0) {
if (target == 0)
ans.push_back(item);
item.pop_back();
}
else {
backtracking(ans, item, idx+1, candidates, target);
item.pop_back();
target += curNum;
int tmpIdx = idx+1;
while (tmpIdx<candidates.size() && candidates[tmpIdx] == candidates[idx])
++tmpIdx;
backtracking(ans, item, tmpIdx, candidates, target);
}
}
};
下標遞歸向右法
下標遞歸向右法模板如下:
void backtracking(vector<vector<int>> &ans, vector<int> &item, int idx, int n, int k) {
if (...) // (含剪枝條件)向右遞迴出口
return;
item.push_back(idx); // 若不是出口就可以直接 push_back
if (....) { // 不滿足一組遞迴答案,繼續向下遞迴
backtracking(ans, item, idx+1, n, k); // 從上到下走
item.pop_back(); // 回溯
backtracking(ans, item, idx+1, n, k); // 從左到右走
}
else { // 到達向下遞迴出口,只繼續向右遞迴
ans.push_back(item); // 記錄一組答案
item.pop_back(); // 對 item.push_back(idx) 的呼應
backtracking(ans, item, idx+1, n, k); // // 從左到右走
}
}
章節題目總結
131 分割回文串【高頻考題】
備註:(1)考點一:迴文串 dp 的遍歷順序;(2)遞歸回溯
迴文串的遍歷順序問題
首先回文字串 dp 初始化矩陣為下三角矩陣,且元素只為 1:
for (int i=0; i<n; ++i) {
for (int j=i; j<n; ++j)
dp[i][j] = 1;
}
然後明確字串 s[i:j] 是迴文串的條件=>s[i]==s[j] && dpSys[i+1][j-1]
,即長元素 s[i:j] 的判定條件依賴於內部短元素 s[i+1:j-1]。因此遍歷順序應該從短字串開始:
for (int i=0; i<n; ++i) {
for (int j=0; j<=i; ++j)
dpSyms[i][j] = 1;
}
for (int i=n-1; i>=0; --i) {
for (int j=i+1; j<n; ++j)
dpSyms[i][j] = (s[i] == s[j]) && dpSyms[i+1][j-1];
}
131 分割回文串
對於此類涉及字元的題目(對比【17 電話號碼的字母組合】),適合用下標遞歸向右法。
備註:(1)竟然在 if(!cond)
犯迷糊,以後統一寫 if(cond > 0)
;(2)如果整個字串能被分割,idx 就會到達 n,否則不會到達 n;因此在向右遞迴出口 Push Into ans 是正確的。
class Solution {
public:
vector<vector<string>> ans;
vector<vector<string>> partition(string s) {
int n = s.size();
vector<vector<int>> dpSyms(n, vector<int>(n, 0));
init(dpSyms, s, n);
vector<string> item;
backtracking(dpSyms, item, s, 0);
return ans;
}
private:
void init(vector<vector<int>> &dpSyms, string s, int n) {
for (int i=0; i<n; ++i) {
for (int j=0; j<=i; ++j)
dpSyms[i][j] = 1;
}
for (int i=n-1; i>=0; --i) {
for (int j=i+1; j<n; ++j)
dpSyms[i][j] = (s[i] == s[j]) && dpSyms[i+1][j-1];
}
}
void backtracking(vector<vector<int>> &dpSyms, vector<string> &item, string &s, int idx) {
int n = s.size();
if (idx >= n) { // 向右走遞迴出口
if (item.size())
ans.push_back(item);
return;
}
for (int j=idx; j<n; ++j) { // 向右走
if (dpSyms[idx][j]) {
string curStr = s.substr(idx, j-idx+1);
item.push_back(curStr);
backtracking(dpSyms, item, s, j+1); // 向下走
item.pop_back();
}
}
}
};
93 復原 IP 地址
備註:(1)這道題在到達向右遞迴基時無需向右走;(2)IP 每部分的合法性判斷
bool isValid(string s) {
// 空 || 長度大於3 || 含有前導 0
if (!s.size() || s.size()>3 || (s[0]=='0'&& s.size()>1))
return false;
int num = 0;
for (auto data : s)
num = num*10 + data - '0';
// 介於256-999
if (num > 255)
return false;
return true;
}
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> ans;
if (s.size() > 12)
return ans;
vector<string> itemStr;
backtracking(ans, itemStr, -1, 0, s);
return ans;
}
private:
void backtracking(vector<string> &ans, vector<string> &itemStr, int preIdx , int curIdx, string &s) {
if (curIdx >= s.size() || itemStr.size() >=4)
return;
string curNum = s.substr(preIdx+1, curIdx-preIdx);
if (!isValid(curNum))
return;
itemStr.push_back(curNum);
if (itemStr.size() == 4 && curIdx+1 >= s.size()) {
string ansStr;
for(int i=0; i<4; ++i) {
if (i !=0)
ansStr.push_back('.');
ansStr += itemStr[i];
}
ans.push_back(ansStr);
itemStr.pop_back();
}
else {
backtracking(ans, itemStr, curIdx, curIdx+1, s); // 向下移動
itemStr.pop_back();
backtracking(ans, itemStr, preIdx, curIdx+1, s); // 向右移動
}
}
bool isValid(string s) {
// 空 || 長度大於3 || 含有前導 0
if (!s.size() || s.size()>3 || (s[0]=='0'&& s.size()>1))
return false;
int num = 0;
for (auto data : s)
num = num*10 + data - '0';
// 介於256-999
if (num > 255)
return false;
return true;
}
};