1. 程式人生 > 其它 >程式碼手記筆錄——遞歸回溯

程式碼手記筆錄——遞歸回溯

遞歸回溯的兩種寫法

(1)遞歸回溯無非是向下走一直到遞迴基,然後向右走。這個 向右 的過程可以通過 for 迴圈控制 (迭代向右法) ,也可以通過控制下標遞迴控制 (下標遞歸向右法)
(2)遞歸回溯函式裡的 push() 與 pop() 個數一定是相等的。【注意:if-else裡的 pop() 只能算一次】
(3)為減少遞迴-回溯的時間,優化的方法是 剪枝
(4)遞歸回溯步驟是向下-向右,其實就暗含著搜尋答案存在 可以向右 的性質。對於給定的不滿足 向右遞迴就可以搜尋到全部答案 陣列,首先要讓該陣列滿足該條件。如對求 sum 的組合元素需 先對 nums 進行排序: 【39 組合總和 】、【40 組合總和 II】
(5)遞歸回溯為了避免 ans 的答案出現重複,需在 向右走

的時候進行處理,跳過會出現重複答案的搜尋路徑。如【40 組合總和 II】
(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;
    }
};