LeetCode-回溯

1. 77. 组合

每次选择一个数,然后下一个数从他之后选择
在这里插入图片描述
剪枝:
当剩余元素加一起也不满足k时也不再遍历,
当元素个数==k时不再向下遍历

1.已经选择元素个数:path.size()
2.还需要选择元素个数k - path.size()
3.n=4,k=4,还需要四个元素,最多从倒数第4个元素选,倒数第四个元素为n - 3
比n - 还需要的元素个数(4),大1,所以要加1
即索引最多遍历结果为n - (k - path.size()) + 1

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(int n, int k, int startIndex){
        if (path.size() == k){
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
            path.push_back(i);
            backtracking(n, k, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return res;
    }
};

2. 216. 组合总和 III

与上题基本相同,但加了更多的限制条件
剪枝:
1.当元素个数==k时不再向下遍历
2.当元素之和大于要求时不再继续(接下来的数只会更大)
3.当剩余元素不足k个时停止遍历
参数n代表剩余需要数值大小,为0代表相等,为负数相当于当前元素和大于要求,直接剪枝
所以每次传参n - i,传型参之后n不变(相当于n + i),隐含了回溯

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(int n, int k, int startIndex){
        if(n < 0) return ;
        if (path.size() == k){
            if (n == 0){
                res.push_back(path);
            }
            return ;
        }
        for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
            path.push_back(i);
            backtrack(n - i, k, i + 1);
            path.pop_back();
        }
    } 
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtrack(n, k, 1);
        return res;
    }
};

3. 17. 电话号码的字母组合

多个集合求组合,用哈希把每个号码对应的字符串连接起来
index代表号码索引,表示选择哪个号码集合(当index==号码长度时,结束回溯,也可以用path.size(),但index更省时间
用引用传参是应为就不用拷贝一份了
注意:path + d[i]这种临时变量传引用必须加const

class Solution {
private:
    string letterMap[10] = {
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };
    vector<string> res;
    void backtrack(string& digits, int index,const string& path){
        if(index == digits.size()){
            res.push_back(path);
            return;
        }
        int num = digits[index] - '0';  //当前电话号码
        string cur = letterMap[num];   //对应的字符串
        for(int i = 0; i < cur.size(); i++){
            backtrack(digits, index + 1, path + cur[i]);
        }
    }
public:
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0) return res;
        backtrack(digits, 0, "");
        return res;
    }
};

4. 39. 组合总和

树枝去重
防止重复元素:也采用startIndex,但不同的是因为可以访问重复元素,所以传startIndex为i
这样就不会出现(123,213)这种情况了,也同样能遍历到所有集合(12同时在的情况在访问1时已得到过,2就不需要访问1了)
回溯终止条件:当target==0,加入集合
target<0写在最上面,停止向下查找
剪枝:在for循环判断target,如果大于停止遍历(横向停止,纵向也停止),但需要排序数组,因为7可能在1前面,前面溢出不代表后面也溢出

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(vector<int> candidates, int target, int startIndex){
        if (target == 0){
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && target - candidates[i] >= 0; i++){
            path.push_back(candidates[i]);
            backtrack(candidates, target - candidates[i], i);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        if(candidates.size() == 0) return res;
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0);
        return res;
    }
}

5. 40. 组合总和 II

树层去重
本题集合中是有重复元素的,集合中单个元素可以重复,但集合整体不能都相同(可以有112的存在,但不能还有个121)
所以每个树枝上的元素可以重复,但同一层的相同元素不可以重复遍历两次(会产生相同的集合)
每层的起始位置为startIndex,在他之前都是上层的元素了,可以重复,所以不可以是if (i > 0 && candidates[i] == candidates[i - 1]) ,这样树枝也会被去重
所以是 i > startIndex,处于同一层的才会被去除
妙妙妙

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(vector<int> candidates, int target, int startIndex){
        if(target == 0){
            res.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size() && target - candidates[i] >= 0; i++){
            if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
            path.push_back(candidates[i]);
            backtrack(candidates, target - candidates[i], i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        if(candidates.size() == 0) return res;
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0);
        return res;
    }
};

6. 131. 分割回文串

与求组合类似,startIndex之前相当于已经被切割过,每次从startIndex继续切割,同时判断当前切割下来的字符串是不是回文字符串,不是就直接停止该分支的回溯,这样最后到末尾的分支就都是符合要求的了
回溯出口:切割到了字符串末尾
剪枝:中途不是回文字符串的都终止该树枝,同时保证了res中都是已符合要求的字符串

class Solution {
private:
    vector<string> path;
    vector<vector<string>> res;
    void backtrack(const string& s, int startIndex){
        if (startIndex == s.size()) {
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++){
            if (isPalindrome(s, startIndex, i)){
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
                backtrack(s, i + 1);
                path.pop_back();
            }
        }
    }
    bool isPalindrome(const string& str, int left, int right){
        while (left < right){
            if (str[left++] != str[right--]){
                return false;
            }
        }
        return true;
    }
public:
    vector<vector<string>> partition(string s) {
        backtrack(s, 0);
        return res;
    }
};

7. 93. 复原 IP 地址

与切割回文串相似,判断回文变为了判断该段ip地址是否合法
具体判断条件:
1.结束索引要大于开始索引(1111)结束索引为3,但开始索引为4,必须要有该判断
2.数字开头不能为0,除非这个数字真的是0
3.中间不能含有数字之外的字符
4.每段ip大小要小于等于255

回溯出口:每分一段记数加一,当已经分完三段后判断剩余的第四段是否合法,如果合法添加到res中
剪枝:该段IP地址不合法时,直接结束该树层和树枝的遍历(break),因为该段不合法该层后的ip地址都包含这个不合法的部分,直接结束即可

class Solution {
private:
    vector<string> res;
    void backtrack(string& str, int startIndex, int pointNum){
        if (pointNum == 3){
            if (isValid(str, startIndex, str.size() - 1)){
                res.push_back(str);
            }
            return;
        }
        for(int i = startIndex; i < str.size(); i++){
            if (isValid(str, startIndex, i)){
                str.insert(str.begin() + i + 1, '.');
                backtrack(str, i + 2, pointNum + 1);
                str.erase(str.begin() + i + 1);
            }
            else {
                break;
            }
        }
    }
    bool isValid(string str, int start, int end){
        if(start > end){
            return false;
        }
        if (str[start] == '0' && start != end){
            return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++){
            if (str[i] < '0' || str[i] > '9'){
                return false;
            }
            num = num * 10 + str[i] - '0';
            if (num > 255){
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        backtrack(s, 0, 0);
        return res;
    }
};

8. 78. 子集

模板题,直接将所有节点填入res,无需任何剪枝
注意res的push_back()要写在最前面,代表将上一层加入结果集,这样就包含了空子集
回溯出口:当startIndex等于数组大小

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(vector<int>& nums, int startIndex){
        res.push_back(path);
        if (startIndex == nums.size()){
            return;
        }
        for (int i = startIndex; i < nums.size(); i++){
            path.push_back(nums[i]);
            backtrack(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        backtrack(nums, 0);
        return res;
    }
};

9. 491. 递增子序列

只有path数组为空或当前元素大于上一个元素,且元素没有在同一层出现过,才进行回溯,直到startIndex==size为止
但本题不同的是不能用排序和i>startIndex的方式判断本层是否有重复元素,因为会打乱原数组顺序
所以就要用哈希来存放并判断当前层是否出现过重复元素

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtrack(vector<int>& nums, int startIndex){
        if(startIndex == nums.size()){
            return;
        }
        vector<int> used(201, 0);
        for(int i = startIndex; i < nums.size(); i++){
            if((path.size() == 0 || nums[i] >= path.back()) && used[nums[i] + 100] != 1){
                path.push_back(nums[i]);
                used[nums[i] + 100] = 1;
                if(path.size() > 1){
                    res.push_back(path);
                }
                backtrack(nums, i + 1);
                path.pop_back();
            }
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtrack(nums, 0);
        return res;
    }
};

10. 46. 全排列

树枝去重
防止111,222的出现
取叶子节点,但注意去重,因为是排列所有元素都可以取(213,,132可以),但一个数只能在集合中出现一次,这样写used约束的树枝中不会有重复
used标记的是位置,而不是数值,所以相同位置的数不会出现在同一树枝
递归出口:path.size() == nums.size()

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(const vector<int>& nums, vector<bool>& used){
        if (path.size() == nums.size()){
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++){
            if(used[i] == false) {
                used[i] = true;
                path.push_back(nums[i]);
                backtrack(nums, used);
                path.pop_back();
                used[i] = false;
            }
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        if(nums.empty()) return res;
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return res;
    }
};

11. 47. 全排列 II

树枝去重和树层去重
防止111,222的出现,
树层去重(相同的数在一个树层使用了两次)同时防止112出现了两次
递归出口:path.size() == nums.size()

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtrack(const vector<int>& nums, vector<bool>& used){
        if (path.size() == nums.size()){
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++){
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                continue;
            }
            if (used[i] == false) {
                used[i] = true;
                path.push_back(nums[i]);
                backtrack(nums, used);
                path.pop_back();
                used[i] = false;
            }
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        if(nums.empty()) return res;
        sort(nums.begin(), nums.end());
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return res;
    }
};

12. 51. N 皇后

传说中的N皇后,
在这里插入图片描述
在每行选一个位置,然后继续遍历下一行(每次都要判断能否落在该位置)
合法性函数:
1.判断同一列是否有皇后
2.判断主对角线方向斜线,上半段(从上往下放棋子,下面的行没有棋子,不用判断),行列都减
3.判断副对角线方向斜线,上半段,行减列加
不用判断同一行的棋子情况,因为咱们回溯过程中同一行只会有一个棋子

递归出口:当最后一行也放完棋子后结束回溯

class Solution {
private:
    vector<vector<string>> res;
    void backtrack(int n, int row, vector<string>& chessboard){
        if (row == n) {
            res.push_back(chessboard);
            return; 
        }
        for (int col = 0; col < n; col++){
            if (isValid(row, col, chessboard)){
                chessboard[row][col] = 'Q';
                backtrack(n, row + 1, chessboard);
                chessboard[row][col] = '.';
            }
        }
    }
    bool isValid(int row, int col, const vector<string>& chessboard){
        //判断同一列
        for (int i = 0; i < row; i++){
            if (chessboard[i][col] == 'Q'){
                return false;
            }
        }
        //判断主对角线方向斜线,上半段
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q'){
                return false;
            }
        }
        //判断副对角线方向斜线,上半端
        for(int i = row - 1, j = col + 1; i >= 0 && j < chessboard.size(); i--, j++){
            if (chessboard[i][j] == 'Q'){
                return false;
            }
        }
        return true;
    }
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<string> chessboard(n, string(n, '.'));
        backtrack(n, 0, chessboard);
        return res;
    }
};

13. 37. 解数独

与N皇后不同的是,N皇后是每行选择一个位置,数烛是每个位置都要填入数字,N皇后实质上只填写N个(一行一个),但数烛要填写N方个
按顺序填写,所以要用双重循环,该位置有数字直接跳过,当前位置为空且1到9都不能填入当前位置,直接返回false,一层层向上撤销结果,该树枝回溯结束
回溯出口:当所有位置都填入数字才会返回true,一层一层向上返回,但不会撤销填入的数字,最终获得想要的结果
判断合法性:横、竖、九宫格都不能一样,
整除3再乘3是每个数所在九宫格的起始位置,妙啊妙啊

class Solution {
private:
    bool backtrack(vector<vector<char>>& board){
        for (int i = 0; i < board.size(); i++){
            for (int j = 0; j < board[0].size(); j++){
                if(board[i][j] != '.') continue;
                for (char k = '1'; k <= '9'; k++) {
                    if (isValid(i, j, k, board)){
                        board[i][j] = k;
                        if (backtrack(board)) return true;
                        board[i][j] = '.';
                    }
                }
                return false;
            }
        }
        return true;
    }
    bool isValid(int row, int col, char val, const vector<vector<char>>& board){
        for (int i = 0; i < 9; i++){
            if (board[row][i] == val){
                return false;
            }
        }
        for (int i = 0; i < 9; i++){
            if (board[i][col] == val){
                return false;
            }
        }
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++){
            for (int j = startCol; j < startCol + 3; j++){
                if (board[i][j] == val){
                    return false;
                }
            }
        }
        return true;
    }
public:
    void solveSudoku(vector<vector<char>>& board) {
        backtrack(board);
    }
};

收获与体会

1.回溯模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

2.回溯三部曲

1.递归函数返回值即参数(返回值一般为void2.递归终止条件
3.单层搜索过程

3.回溯本质是穷举,可以用剪枝来减少穷举的次数
4. 回溯一般用于解决排列、组合、字符串切割、子集、棋盘问题
5. 子集是收集所有节点,组合是所有叶子节点
6. 组合时要避免使用当前层已经重复的元素,否则结果集元素会重复
当数组元素顺序可以改变时,用

sort(numns.begin(), nums.end());
if(i > startIndex && nums[i] == nums[i - 1]) continue;

但数组元素顺序不可以改变时,如递增子序列,就要用哈希记录当前层出现过的元素(而不是哈希结果集,这样存放数据更少),如果元素已经在哈希表中就不进行遍历
7.剪当前树枝在for循环中用continue,直接结束当前树层用break
8.排列问题由于没有startIndex,去重用used数组
记录该位置有没有被使用过,集合中没有重复元素,只需注意同一位置元素不可以被重复使用
集合中有重复元素,树枝可以有重复元素(112),但树层不能有重复元素(否则会有两个112)

used[i - 1] == true;	上一层被使用过(置为true还没有回溯撤销为false)
used[i - 1] == false;	同一层被使用过

树枝去重

if (used[i] == false) 才将该位置元素加到path

树层去重

不是首元素,与前元素相同,且前元素被使用过
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
      continue;
}

9.单一集合求组合都要用startIndex
10.排列组合求叶子节点,子集求每个节点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值