从 DFS 到回溯法,再看 N 皇后问题

举报
掘金安东尼 发表于 2022/09/27 09:00:11 2022/09/27
【摘要】 DFS 是深度搜索,是暴力的,是一条道走到黑的,是一次性搜到底的!那么,搜到底发现没有路了,就得回退去找另外的路,再继续莽着搜!既然要回退,就必须保存走过每个点的所有信息,包括先后顺序;这个回退的过程就叫 回溯。根据回溯思想,演进到回溯算法来解决寻找问题。看一下wiki对回溯法的解释:回溯法采用 试错 的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答...

DFS 是深度搜索,是暴力的,是一条道走到黑的,是一次性搜到底的!那么,搜到底发现没有路了,就得回退去找另外的路,再继续莽着搜!既然要回退,就必须保存走过每个点的所有信息,包括先后顺序;这个回退的过程就叫 回溯

根据回溯思想,演进到回溯算法来解决寻找问题。看一下wiki对回溯法的解释:

回溯法采用 试错 的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。

简化理解:回溯算法 = 树的深度优先搜索 + 剪枝函数

  • 什么是剪枝函数?
    为了提高搜索效率,在搜索过程中使用约束函数,可以避免无谓地搜索那些已知不含答案状态的子树。如果是最优化问题,还可以使用限界函数剪去那些不可能含有最优解的子树。约束函数和限界函数目的相同,都是为了剪去不必要搜索的子树,减少问题求解所需实际生成的状态结点数,他们统称为剪枝函数。

OK,以上是概念介绍,下面来一道经典之经典之经典的回溯算法题:N皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

image.png

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入: n = 1
输出: [["Q"]]

N 皇后问题很多时候作为例题出现在教科书中,可以当做理解回溯算法的例题进行学习;

以 4 皇后问题为例,递归树如下:

image.png

解题思路:

  • 回溯算法的通用解题思路就是在递归之前做选择,在退出递归之前撤销选择;
  • 通过恰当的方式将不符合条件的情况剪枝;

回溯三部曲:

  • 递归函数参数;
  • 递归终止条件;
  • 单层搜索的逻辑;

回溯模板:

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

JavaScript 实现:

var solveNQueens = function(n) {
    function isValid(row, col, chessBoard, n) {
        // 检查列
        for(let i = 0; i < row; i++) { // 这是一个剪枝
            if(chessBoard[i][col] === 'Q') {
                return false
            }
        }
        // 检查 45度角是否有皇后
        for(let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if(chessBoard[i][j] === 'Q') {
                return false
            }
        }
        // 检查 135度角是否有皇后
        for(let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if(chessBoard[i][j] === 'Q') {
                return false
            }
        }
        return true
    }
    
    // 存放结果
    function transformChessBoard(chessBoard) {
        let chessBoardBack = []
        chessBoard.forEach(row => {
            let rowStr = ''
            row.forEach(value => {
                rowStr += value
            })
            chessBoardBack.push(rowStr)
        })

        return chessBoardBack
    }

    let result = []
    // 回溯
    function backtracing(row,chessBoard) {
        if(row === n) { // 终止条件
            result.push(transformChessBoard(chessBoard))
            return
        }
        for(let col = 0; col < n; col++) {
            if(isValid(row, col, chessBoard, n)) {
                chessBoard[row][col] = 'Q' // 处理节点
                backtracing(row + 1,chessBoard) // 递归
                chessBoard[row][col] = '.' // 回溯,撤销处理结果
            }
        }
    }
    let chessBoard = new Array(n).fill([]).map(() => new Array(n).fill('.'))
    backtracing(0,chessBoard)
    return result
    
};

代码作者:carlsun-2,已验证通过;


回溯算法跟 DFS 深度搜索算法都很经典,需同步理解,对比、吸收;

我是掘进安东尼,公众号同名,日拱一卒、日掘一金,再会~

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。