「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」
问题
1848年,国际象棋棋手马克思·贝瑟尔提出了一个问题:
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
(比如上图就是一种合乎要求的解)
根据这个问题,很多人都给出了自己的答案。大名鼎鼎的数学天才高斯
给出了他的答案:76种摆法;但后来又有人利用图论的方法得出共92种摆法的结论;
那么,到底谁才是对的呢?
ok,为了解决这个问题,今天我们要学习的就是一种(可以将天才高斯击败的)算法——回溯算法!
分析
穷举和暴力的可行性
先思考一下,8个皇后,在8*8的格子里,有多少种组合方式?
C(64,8) = 4,426,165,368
44亿……
哪怕是对于计算机,要遍历这样的数量级,也是一个恐怖的时间消耗了;
那么,有没有更合理的算法呢?
当然是有的,也就是今天我们要学习的主角:回溯算法!
深度搜索和广度搜索
“回溯算法” 是一种 “深度优先搜索(Depth First Search)” 算法,与之相对的是 “广度优先搜索(Breadth First Search)”。
二者有啥区别呢?
举个形象点的例子(摘自 知乎@一只菜鸡):
深度优先 可以这样想,一个人迷路,遇到很多分叉路口,他只有一个人,并且想走出去,所以只能一个个尝试,一条道路走到黑,发现到头了,然后再拐回去走刚才这条路的其他分叉路口,最后发现这条路的所有分叉路口走完了,选择另外一条路继续以上操作,直到所有的路都走过了。
广度优先 并不是这样,一个人迷路,但是他有技能(分身术)它遇到分叉路口,不是选一个走,而是分身多个人都试试,比如有A、B、C三个分叉路口,它A路走一步,紧接着B路也走一步,然后C路也赶紧走一步,步伐整齐统一,直到所有的路走过了。
回溯算法就是 深度优先搜索 的一种实现,我们要做的就是一条路走到黑,并且:1、如果在当前道路我们找到的一条出路,那么将其记录下来;2、如果在某个节点发现无论如何都无法继续下去时,则后退一步,在该节点尝试其他未尝试过的子节点,直到找到下一个出路:
上图即为回溯算法的基本思路:不撞南墙心不死,撞了南墙快回头! 如果换成本文探讨的8皇后问题,换成3皇后问题(3个皇后,在3*3的格子内放置,其他规则不变),则如下图:
伪代码
让我们梳理一下本题的伪代码:
function backtracking(节点层级,其他参数):void {
if (终止条件) {
存放结果;
return;
}
for (遍历:当前层级的所有节点) {
if (本层节点验证通过) {
处理节点;
backtracking(节点层级 + 1,其他参数);
回溯,撤销处理结果
}
}
return;
}
复制代码
其中,比较难理解的点,其实在于这几行代码:
if (本层节点验证通过) {
处理节点;
backtracking(节点层级 + 1,其他参数);
回溯,撤销处理结果
}
复制代码
仔细理解以下,即,有三种情况,查找路径会从子节点回到当前节点:
- 子节点符合题目要求,
属于解
; - 子节点被验证为
不和乎验证要求
;(放在N皇后问题中可以理解为有若干个皇后出现冲突) - 子节点已经完成了所有节点的检索;
实现代码 by javaScript
var solveNQueens = function(n) {
const map = [...Array(n)].map(_ => [...Array(n)].map(_ => '.'))
const check = (m,n) => {}
const results = []
const backtracking = (index) => {
if (index === n) {
results.push(flat(map))
return;
}
for(let i = 0;i < n;i ++) {
if (check(index, i)) {
map[index][i] = 'Q'
backtracking(index + 1)
map[index][i] = '.'
}
}
}
backtracking(0)
return results
};
复制代码
先看上面代码,是按照伪代码实现的一个骨架;主体包含了:
- 初始化的棋盘;
- 校验当前节点是否合法的check方法;
- 回溯方法
以下是最终代码
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function(n) {
const map = [...Array(n)].map(_ => [...Array(n)].map(_ => '.'))
const check = (m,n) => {
for(let i = 0; i < m; i ++) {
if(map[i][n] === 'Q') {
return false;
}
}
for(let i = 0; i < n; i ++) {
if(map[m][i] === 'Q') {
return false;
}
}
for (let i = 1; i <= m; i++) {
const x_before = m-i;
const y_before = n-i;
const y_after = n+i;
if (map[x_before][y_before] === 'Q' || map[x_before][y_after] === 'Q'){
return false;
}
}
return true
}
const flat = (arr) => {
return arr.map(t => t.join(''));
}
const results = []
const backtracking = (index) => {
console.log(index)
if (index === n) {
results.push(flat(map))
return;
}
for(let i = 0;i < n;i ++) {
map[index][i] = 'Q'
if (check(index, i)) {
backtracking(index + 1)
}
map[index][i] = '.'
}
}
backtracking(0)
return results
};
复制代码