【回溯算法】N皇后问题·构建多叉决策树,遍历决策节点,做出决策(边),收集答案

0、前言

由树形解空间入手,深入分析回溯、动态规划、分治算法的共同点和不同点这篇博客,其实已经对回溯算法的思想、做题框架做出了详细的阐述。这篇文章我们再从N皇后问题,加深我们对其理解。

这里在简单再次对其进行概述:

回溯算法的核心就是构建和遍历一棵【多叉决策树】

  • 树的节点是一个决策节点,站在一个节点上我们只需要思考三个问题
    1. 当前已经做出的选择:路径
    2. 当前还可以做出哪些选择(选择列表)
    3. 结束条件:到达叶子决策节点,得到一个答案,进行收集
  • 树的边:代表一个决策(选择)

在这里插入图片描述

  • 🪧代码框架:

    result = []
    def backtrack(路径, 选择列表):
        if 满足结束条件:
            result.add(路径)
            return
        
        for 选择 in 选择列表:
            做选择
            backtrack(路径, 选择列表)
            撤销选择
    
  • backtrack函数就相当于游走在这颗多叉决策树上的一个指针,它来遍历决策节点,并且做出决策。进入backtrack函数,就以为着我们进入了一个决策节点,也意味着我们需要思考上面所示的三个问题。

  • 其核心就是 for 循环里面的递归, 在递归调用之前在这个决策节点上「做选择」,在递归调用之后「撤销选择」

一、51.N 皇后

1.1:题目

力扣链接
在这里插入图片描述

1.2:解题思路

  • 分析:这题分析用回溯算法来解其实不难。下棋的棋子该落在哪个位置有多种可能(需要满足限制条件),这个棋盘的每一层就相当于决策多叉树的每一层,我们需要遍历这个决策多叉树,进行所有可能的决策,最终把所有解法收集起来,这就是一个回溯的过程。

  • 实现:分析出来回溯之后,我们核心是需要写出backtrack递归函数,它是实现整个回溯遍历的核心。进入到backtrack函数,就相当于进入到一个决策节点,我们必须思考以下3个问题

    1. 当前已经做出选择的路径:可以用一个棋盘存储:vector <string> board
    2. 选择列表(站在当前决策节点可以做出哪些选择):可以用row来表示,在board的第row行的每一列是否放置皇后,有n列那么当前该层(行)就有n个选择
    3. 终止条件:当row<=n时,说明0~n-1行已经全部放置好了函数,当前这个路径(board棋盘)可以作为一个答案被收集

    注意的是,不是每一列都可以放置皇后,题目中对皇后的放置有限制条件,所有在遍历选择列表时,对选择需要进行决策可行性判断 (也就是剪枝,代表不能做这个选择)。从这点可以看到,⭐如果不了解回溯算法,一开始感觉会执着于这个限制条件,从而不知道如何下手,但是在决策算法里,它只是一个对决策的限制,我们只需要在遍历到这个决策边时,再进行判断(剪枝)即可。
    在这里插入图片描述

1.3:完整代码

class Solution {
private:
    vector<vector<string>> res; //最终答案的收集
    
    /*backtrack函数,相当于游走在这个多叉树每一个决策节点形解空间的一个指针(指向的就是一个决策节点)
    作用是来构建这个决策多叉树、遍历多叉树的边(一个边就代表一个选择)和收集答案
    - 当前路径:board棋盘用来记录当前路径(在小于row行时做出的选择)
    - 选择列表:在board的第row行的每一列是否放置皇后,有n列那么当前该层(行)就有n个选择
    - 结束条件:当row<=n时,说明0~n-1行已经全部放置好了皇后,当前这个路径(board棋盘)可以作为一个答案被收集
    */
    void backtrack(vector<string> board, int row){
        if(row == board.size()){
            res.push_back(board);
            return;
        }

        //进行当前决策节点(这一行)的选择遍历
        int col = board[row].size();
        for (int i = 0; i < col; i++){

            /*********前序位置:做出选择(从当前决策节点到下一个决策节点)**************/
            //剪枝,如果当前选择不合理
            if(!isValid(board, row , i)){
                continue;
            }
            //合理,则做出选择
            board[row][i] = 'Q';

            /********进入下一个决策节点,接着向下遍历*****************************/
            backtrack(board, row+1);

            /*********后序位置,撤销当前选择*******************/
            board[row][i] = '.';
        }
    }

    /*
    剪枝,判断当前(row,col)这个位置放入皇后的话是否合理
    */
    bool isValid(vector<string> &board, int row, int col){
        //判断列
        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 >= 0; i--, j--){
            if(board[i][j] == 'Q') return false;
        }
        //判断右上方
        for(int i =row-1, j = col+1; i >= 0 && j < board.size(); i--, j++){
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<string> board(n,string(n, '.')); //存储路径
        backtrack(board, 0);
        return res;
    }
};

二、52. N 皇后 II

2.1:题目

力扣链接
在这里插入图片描述

2.2:解题思路

这题解题思路和上面一个一模一样,唯一不同的就是答案收集的变量需要变一下而与!!!之前是存储整个答案,这里的话用一个计数的整形变量ans来存储即可,遍历到底层叶子节点,收集答案ans++即可。

 if(row == board.size() ){
            ans ++;
            return;
        }

2.3:完整代码

class Solution {
public:
    int ans = 0; //在回溯过程中收集和存储最终答案

    /*回溯指针函数backtrack
    - 当前路径:由board来记录’
    - 选择列表:当前第row行的每一列都是可以选择的
    - 结束条件(收集到一个答案): row == board.size()
    */
    void backtrack(vector<string>& board, int row){
        if(row == board.size() ){
            ans ++;
            return;
        }
        
        //下面进行当前决策节点边的遍历(选择列表中做出选择)
        for(int col = 0; col < board.size(); col ++){
            //剪枝,如果当前选择不合法,就不继续遍历该决策子节点
            if(!isValid(board, row , col)){
                continue;
            }
            /***前序位置:表示做出当前边的决策**/
            board[row][col] = 'Q';

            //接着向下遍历子决策节点
            backtrack(board, row + 1);

            /***后序位置,撤销当前决策**********/
            board[row][col] = '.';
        }
    }

    bool isValid(vector<string>& board, int row, int col){
        //首先判断列
        for(int i = row -1; i >=0; i --){
            if(board[i][col] == 'Q') return false;
        }
        //判断左上方
        for(int i = row-1, j = col-1; i >=0 && j >= 0; i--, j--){
            if(board[i][j] == 'Q') return false;
        }
        //判断右上方
        for(int i = row-1, j = col +1; i >=0 && j <board.size(); i--, j++){
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }

    int totalNQueens(int n) {
        vector<string> board(n, string(n, '.')); //存储当前路径
        backtrack(board, 0);
        return ans;
    }
};
  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是瑶瑶子啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值