回溯算法框架

回溯算法解题套路框架

其实回溯算法其实就是我们常说的 DFS 算法,本质上就是一种暴力穷举算法。

解决一个回溯问题,实际上就是一个决策树的遍历过程

站在回溯树的一个节点上,你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

回溯算法框架:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」

一、全排列问题

力扣第 46 题「 全排列」就是给你输入一个数组 nums,让你返回这些数字的全排列。

解题代码:

class Solution {
public:
    //回溯        
    vector<vector<int>>res;//存储所有可能排列
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool>used(nums.size(),false);//记录是否走过这个数
        vector<int>track;//记录路径
        backtrack(nums,track,used);
        return res;
    }
    void backtrack(vector<int>nums,vector<int>&track,vector<bool>&used){
        //结束条件
        if(track.size()==nums.size()){
            res.push_back(track);
            return;
        }
        //遍历
        for(int i=0;i<nums.size();i++){
            if(used[i]==true){//如果这个数已经被选择过
                continue;
            }
            used[i]=true;//选择这个数
            track.push_back(nums[i]);//记入路径
            backtrack(nums,track,used);//对其他数进行遍历
            used[i]=false;//撤销选择
            track.pop_back();
        }
    }
};

本质上就是暴力穷举,要深入理解【决策树】这一概念。在这里不做深入解释,想要了解可以去回溯算法解题套路框架 :: labuladong的算法小抄 (gitee.io)自行了解。

注意:

但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高

二、N 皇后问题

力扣第 51 题「 N 皇后」就是这个经典问题,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。

class Solution {
public:
    vector<vector<string>>res;
    vector<vector<string>> solveNQueens(int n) {
        
        vector<string>board(n,string(n,'.'));
        backtrack(0,board);
        return res;
    }
    void backtrack(int row,vector<string>&board){
        //结束条件
        if(row==board.size()){//当选择完的行数等于棋盘的行数时
            res.push_back(board);
            return;
        }
        //遍历 一行中的每个数也就是改变列即可
        for(int col=0;col<board.size();col++){
            //除去不合法的选择 
            if(!isValid(board,row,col)){
                //不符合游戏规则 直接跳过
                continue;
            } 
            //符合规则选择
            board[row][col]='Q';
            //对下一行进行选择
            backtrack(row+1,board);
            //撤销选择
            board[row][col]='.';
        }
    }
    bool isValid(vector<string>&board,int row,int col){
        int n=board.size();
        //检查一列中是否有冲突
        for(int i=0;i<row;i++){
            if(board[i][col]=='Q')return false;
        }
        //检查右上是否有冲突
        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if(board[i][j]=='Q')return false;
        }
        //检查左上是否有冲突
        for (int i = row - 1, j = col - 1;
            i >= 0 && j >= 0; i--, j--) {
         if (board[i][j] == 'Q')
            return false;
            }
        return true;
    }
};

PS:肯定有读者问,按照 N 皇后问题的描述,我们为什么不检查左下角,右下角和下方的格子,只检查了左上角,右上角和上方的格子呢?

因为皇后是一行一行从上往下放的,所以左下方,右下方和正下方不用检查(还没放皇后);因为一行只会放一个皇后,所以每行不用检查。也就是最后只用检查上面,左上,右上三个方向。

三、最后总结

回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:

def backtrack(...):
    for 选择 in 选择列表:
        做选择
        backtrack(...)
        撤销选择

backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集

其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?

某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。

posted @ 2022-03-29 10:22  BailanZ  阅读(177)  评论(0编辑  收藏  举报