代码随想录算法训练营第22天 | 第八章 回溯算法 part04

第七章 回溯算法 Part 04

491. 递增子序列

本题和大家刚做过的 90. 子集 II 非常相似,但也有很多不同,很容易掉进坑里。建议大家结合两题一起理解。

class Solution {
public:
vector<vector<int>> result; 
vector<int> path; 
void backtracking( vector<int>& nums ,int startIndex){
            if(path.size()>1)
            result.push_back(path);
            unordered_set<int> used;
            for(int i=startIndex;i<nums.size();i++)
            {
               if (used.find(nums[i]) != used.end()) 
                continue;
                if (!path.empty() && nums[i] < path.back()) 
                continue;
                used.insert(nums[i]);
                path.push_back(nums[i]);
                backtracking(nums,i + 1); //继续递归
                path.pop_back();//回溯  
            }
            //满足条件,return        
}
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear(); // 可以不写
        path.clear();   // 可以不写      
        backtracking(nums, 0);
        return result;
    }
};

回溯算法确实主打一个一看就会,一做就废,自己研究了半个上午,总是出各种各样的问题,最后还是在gpt帮助下完成的。
递增子序列的查找:通过回溯算法,遍历数组中的所有可能组合,检查当前路径中的元素是否递增。
去重:使用 unordered_set 记录每一层递归中已经使用过的元素,避免同一层中出现重复子序列。
递归与回溯:递归过程中,继续尝试加入后续元素;通过 path.pop_back() 实现回溯,恢复状态。
用数组来做哈希,效率就高了很多。

注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。

程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如果数值范围小的话能用数组尽量用数组。

class Solution {
public:
vector<vector<int>> result; 
vector<int> path; 
void backtracking( vector<int>& nums ,int startIndex){
            if(path.size()>1)
            result.push_back(path);
            int used[201] = {0}; 
            for(int i=startIndex;i<nums.size();i++)
            {
               if ((!path.empty() && nums[i] < path.back())|| used[nums[i] + 100] == 1)
                continue;
                used[nums[i] + 100] = 1;
                path.push_back(nums[i]);
                backtracking(nums,i + 1); //继续递归
                path.pop_back();//回溯  
            }
            //满足条件,return        
}
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear(); // 可以不写
        path.clear();   // 可以不写      
        backtracking(nums, 0);
        return result;
    }
};



46. 全排列

本题重点是体会排列问题组合问题组合总和子集问题的区别。为什么排列问题不需要 startIndex?建议细细体会。

  • 题目讲解链接:全排列
  • 视频讲解:Bilibili 视频
    还是要将代码拆分才能理解思路。
    首先先判断回溯终止条件,当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点,这时候把一次结果push到result中
    去重 used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
    最大的特点就是:每次都要从头开始搜索。
class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue; // path里已经收录的元素,直接跳过
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

47. 全排列 II

这道题是40. 组合总和 II 去重逻辑与 46. 全排列 的结合。可以先自己做一下,再重点看文章中的拓展内容。特别关注 used[i - 1] == trueused[i - 1] == false 之间的区别。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue; // path里已经收录的元素,直接跳过
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

大家发现,去重最为关键的代码为:

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
    continue;
}

如果改成 used[i - 1] == true, 也是正确的!,去重代码如下:

这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!用输入: [1,1,1] 来举一个例子。树层上去重(used[i - 1] == false),的树形结构如下:按照这个顺序顺几遍,发现这个就是回溯算法的核心,搞懂这个就搞懂回溯.先从下面开始,反正第一个肯定会被压入其中,大的for循环(层遍历)后序发现重复,如果前面是0,就不管。

在这里插入图片描述

树枝上去重(used[i - 1] == true)的树型结构如下:
反正第一个肯定会被压入其中,for循环后并置1,在接下来的循环中发现重复,如果前面是1,就说明,已经用过了,纵向就断了。本质上是从最后一个重复的往前遍历,这样used[i - 1]永远不为真。
在这里插入图片描述

大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
难度确实挺大,


建议跳过的一些难题

以下三道题较难,建议一刷时可以选择适当跳过,等到二刷时随着对回溯算法的深入理解,再尝试解决这些问题。

332. 重新安排行程(可跳过)

这道题难度较大,很多同学在一刷时可能会比较费劲,建议留到二刷时再来解决。这道题当初没有录制视频,原因是录制时参考的是《代码随想录》出版的目录,当时没有这道题。
我的评价是这题TM难了,看了半个小时只懂些许思路。放弃了放弃了。


51. N 皇后(适当跳过)

N 皇后问题是经典题目,但建议一刷时了解大体思路即可。如果时间紧张,可以直接跳过,二刷时再深入解决。实话实说,这题不算太难,就是稍微复杂了点,考虑多种情况,就是很简单的回溯,试错。

class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
    if (row == n) {
        result.push_back(chessboard);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
            chessboard[row][col] = 'Q'; // 放置皇后
            backtracking(n, row + 1, chessboard);
            chessboard[row][col] = '.'; // 回溯,撤销皇后
        }
    }
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
    // 检查列
    for (int i = 0; i < row; i++) { // 这是一个剪枝
        if (chessboard[i][col] == 'Q') {
            return false;
        }
    }
    // 检查 45度角是否有皇后
    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    // 检查 135度角是否有皇后
    for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}
public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

37. 解数独(适当跳过)

和 N 皇后问题类似,解数独也是一道经典的回溯问题,建议一刷时了解大体思路即可,二刷时再深入解决。

  • 题目讲解链接:解数独
  • 视频讲解:Bilibili 视频
    代码是看懂了,但写肯定写不出来,确实思路和N皇后差不多,就是多了个二维数组,
    最大的问题是:递归函数的返回值需要是bool类型,为什么呢?
    因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。如果数独棋盘已经全部填满(没有空格 ‘.’),递归函数返回 true,表示已经找到了解。如果某一步填入的数字 k 无法满足数独规则,则函数返回 false,表示当前路径无解,回到上一层递归,撤销刚才的操作(回溯)。
class Solution {
private:
bool backtracking(vector<vector<char>>& board) {
    for (int i = 0; i < board.size(); i++) {        // 遍历行,// 行不一定为9
        for (int j = 0; j < board[0].size(); j++) { // 遍历列
            if (board[i][j] == '.') {
                for (char k = '1'; k <= '9'; k++) {     // (i, j) 这个位置放k是否合适
                    if (isValid(i, j, k, board)) {
                        board[i][j] = k;                // 放置k
                        if (backtracking(board)) return true; // 如果找到合适一组立刻返回
                        board[i][j] = '.';              // 回溯,撤销k
                    }
                }
                return false;  // 9个数都试完了,都不行,那么就返回false
            }
        }
    }
    return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}
bool isValid(int row, int col, char val, vector<vector<char>>& board) {
    for (int i = 0; i < 9; i++) { // 判断行里是否重复
        if (board[row][i] == val) {
            return false;
        }
    }
    for (int j = 0; j < 9; j++) { // 判断列里是否重复
        if (board[j][col] == val) {
            return false;
        }
    }
    int startRow = (row / 3) * 3;
    int startCol = (col / 3) * 3;
    for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
        for (int j = startCol; j < startCol + 3; j++) {
            if (board[i][j] == val ) {
                return false;
            }
        }
    }
    return true;
}
public:
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

总结

刷完了这么多回溯算法的题目,大家可以尝试做一下总结。反思这些题目的共性和区别,尤其是在递归和回溯的应用场景中,巩固对回溯法的理解。不得不说,卡尔确实牛逼,总结的非常好。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值