刷题笔记(回溯算法)

90. 子集 II

c++ 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>&nums,vector<bool> &used,int startIndex){
        if(startIndex>=nums.size()) return;
        for(int i=startIndex;i<nums.size();i++){
            if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;//树层去重
            
            path.push_back(nums[i]);
            res.push_back(path);
            used[i]=true;
            backtracking(nums,used,i+1);//回溯
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        res.push_back({});
        sort(nums.begin(),nums.end());//排序
        vector<bool> used(nums.size(),false);
        backtracking(nums,used,0);
        return res;
    }
};

491. 非递减子序列

同样是树层去重,那这道题和上道题的区别是什么呢?

上道题由于可以排序,所以我们可以通过判断现在的数和上一个数是否相等,而跳过循环

这道题为保证递增,无法排序,我们只知道是在一个树层上的元素不能相等,所以需要用unordered_set来记录在同一树层上取过的元素,从而完成去重

c++ 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>&nums,int startIndex){
        unordered_set<int> uset;
        for(int i=startIndex;i<nums.size();i++){
            if(uset.find(nums[i])!=uset.end()) continue;//树层去重
            if(path.empty()||(!path.empty()&&nums[i]>=path.back())){
                path.push_back(nums[i]);
                uset.insert(nums[i]);
                //uset是记录出现过的元素,所以回溯时不用再删除添加进去的nums[i]
                if(path.size()>1) res.push_back(path);
                backtracking(nums,i+1);
                path.pop_back();
            }
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums,0);
        return res;
    }
};

46. 全排列

明白排列与组合的区别:不需要startIndex记录起始位置,只需要used数组判断当前元素是否是否加入到排列中

c++ 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(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]==true) continue;//不重复加入元素,只加入没有加入过的元素
            //标准回溯,回溯函数前后对称
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,used);
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        backtracking(nums,used);
        return res;
    }
};

47. 全排列 II

这道题本质上也是对重复元素进行树层去重,根据前面做过的题,我们会发现

我们既可以先排序再跳过相同元素,也可以不排序而用unordered_set出现过的元素

(这里对应方法一和方法二)

方法一:

c++ 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>&nums,vector<bool>&used){
        if(nums.size()==path.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]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,used);
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums,used);
        return res;
    }
};

方法二:

一次backtracking的调用,相当于在树形结构中往深/下一层的探索,所以在递归函数内部定义unordered_set,就相当于每个树层去检验重复元素

c++ 复制代码
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>&nums,vector<bool>&used){
        unordered_set<int> uset;
        if(nums.size()==path.size()){
            res.push_back(path);
            return;
        }
        for(int i=0;i<nums.size();i++){
            if(uset.find(nums[i])!=uset.end()) continue;
            if(used[i]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            uset.insert(nums[i]);
            backtracking(nums,used);
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        backtracking(nums,used);
        return res;
    }
};

51. N 皇后

  • 属于棋盘问题

  • 每次调用回溯函数:进入棋盘的下一行

  • 回溯函数中的for循环:找到皇后在该行中的位置(第几列)

  • 与前面题的最大区别:前面都是在一维数组中取一个数或切割出几个数去加入到路径中,这道题我们是在二维数组中遍历一维数组去找到皇后的位置(即按行递归,每一行在多个列中做选择)

c++ 复制代码
class Solution {
public:
    vector<vector<string>> res;
    //传入二维数组棋盘,当前递归(处理)的行数
    void backtracking(vector<string>&chessboard,int n,int row){
        if(row==n){
            res.push_back(chessboard);
            return;
        }
        //在该行中找到所处列的位置
        for(int col=0;col<n;col++){
            if(isValid(row,col,chessboard,n)){
                chessboard[row][col]='Q';
                backtracking(chessboard,n,row+1);//回溯
                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&&col<n;i--,j++){
            if(chessboard[i][j]=='Q') return false;
        }
        return true;
    }
    vector<vector<string>> solveNQueens(int n) {
        vector<string> chessboard(n,string(n,'.'));
        backtracking(chessboard,n,0);
        return res;
    }
};

37. 解数独

  • 每次调用回溯函数:在空格处添加数字1-9

  • 双侧for循环:遍历二维数组,找到空格

  • 剪枝操作:判断行是否有相同元素、判断列是否有相同元素、判断3x3宫中是否有相同元素

  • 和前面题的主要区别:在前面题目中我们需要找到该数组的全部子集(组合)或N皇后的所有位置,但这道题只需要找到一个满足题意的数独解法就可以

    相当于:前者是搜索二叉树的全部路径(所以返回值是void,需要结果集来存储结果),后者是找到一个满足题意的路径就可以(所以返回值是bool,找到一个直接就返回true

  • 终止条件:这里没有像前面题的"路径中的元素等于数组中的元素"或"row==n"的显示终止条件,而是把它放到回溯函数中

    如果数字1-9都无法递归下去说明当前分支无法得到解,就返回false

    如果双层for循环遍历完都没有找到空格,说明空格被填满(即找到一个可行解),返回true

    注:那truefalse应该写在什么位置呢?就应该写在上方条件的下方(举个例子:写到for循环1-9的下面,返回false),因为如果进入循环就进入递归函数了,就不会走到for循环的下面

  • 递归返回值的层级:

    最底层(成功终止)

    当双层循环结束后没有遇到 空格,说明所有格子都已填满,此时执行return true;。这一层代表找到了完整解,是递归的出口。

    · 中间层(成功传递)

    当某层递归调用backtracking(board)返回true时,意味着从该分支出发的后续填数全部成功。当前层立即return true;,将成功信号逐级向上传递,不再尝试其他数字。

    中间层(失败回溯)

    当某层对当前空格尝试所有数字后,没有一个数字能导致后续递归成功,则执行return false;。这表示以当前棋盘状态为起点的所有路径都行不通,上一层收到false后会撤销刚才放置的数字,继续尝试其他数字。

    最外层(最终结果)
    solveSudoku调用backtracking(board)时,如果最终返回 true,表示数独有解;返回 false 则表示无解(本题保证有解,所以一定是 true)。

c++ 复制代码
class Solution {
public:
    bool backtracking(vector<vector<char>>&board){
        //双层for循环遍历数独表格
        for(int i=0;i<board.size();i++){
            for(int j=0;j<board[0].size();j++){
                //找到空格
                if(board[i][j]=='.'){
                    //尝试数字1-9哪个可以
                    for(char k='1';k<='9';k++){
                        //找到可以的数字
                        if(isVaild(i,j,k,board)){
                            board[i][j]=k;
                            if(backtracking(board)) return true;//满足题意直接返回
                            board[i][j]='.';//否则继续回溯
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
    bool isVaild(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;
        }
        //判断3x3宫
        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;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};
相关推荐
zhooyu2 小时前
GLM中lerp实现线性插值
c++·opengl
我不是懒洋洋2 小时前
预处理详解
c语言·开发语言·c++·windows·microsoft·青少年编程·visual studio
NAGNIP2 小时前
一文搞懂CNN经典架构-ResNet!
算法·面试
计算机安禾2 小时前
【数据结构与算法】第14篇:队列(一):循环队列(顺序存储
c语言·开发语言·数据结构·c++·算法·visual studio
Java_小白呀2 小时前
考研408数据结构(持续更新中...)
数据结构·考研
Frostnova丶2 小时前
(11)LeetCode 239. 滑动窗口最大值
数据结构·算法·leetcode
IT从业者张某某2 小时前
基于EGE19.01完成恐龙跳跃游戏-V00-C++使用EGE19.01这个轮子
c++·游戏
GoCoding2 小时前
YOLO-Master 与 YOLO26 开始
算法
VALENIAN瓦伦尼安教学设备2 小时前
设备对中不良的危害
数据库·嵌入式硬件·算法