一文整明白8皇后和回溯算法

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

问题

1848年,国际象棋棋手马克思·贝瑟尔提出了一个问题:

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。

8-queens.png
(比如上图就是一种合乎要求的解)

根据这个问题,很多人都给出了自己的答案。大名鼎鼎的数学天才高斯给出了他的答案:76种摆法;但后来又有人利用图论的方法得出共92种摆法的结论;
那么,到底谁才是对的呢?
ok,为了解决这个问题,今天我们要学习的就是一种(可以将天才高斯击败的)算法——回溯算法!

分析

穷举和暴力的可行性

先思考一下,8个皇后,在8*8的格子里,有多少种组合方式?

C(64,8) = 4,426,165,368

44亿……

amazing.jpg
哪怕是对于计算机,要遍历这样的数量级,也是一个恐怖的时间消耗了;
那么,有没有更合理的算法呢?
当然是有的,也就是今天我们要学习的主角:回溯算法!

深度搜索和广度搜索

回溯算法” 是一种 “深度优先搜索(Depth First Search)” 算法,与之相对的是 “广度优先搜索(Breadth First Search)”。
二者有啥区别呢?
举个形象点的例子(摘自 知乎@一只菜鸡):

深度优先 可以这样想,一个人迷路,遇到很多分叉路口,他只有一个人,并且想走出去,所以只能一个个尝试,一条道路走到黑,发现到头了,然后再拐回去走刚才这条路的其他分叉路口,最后发现这条路的所有分叉路口走完了,选择另外一条路继续以上操作,直到所有的路都走过了。

广度优先 并不是这样,一个人迷路,但是他有技能(分身术)它遇到分叉路口,不是选一个走,而是分身多个人都试试,比如有A、B、C三个分叉路口,它A路走一步,紧接着B路也走一步,然后C路也赶紧走一步,步伐整齐统一,直到所有的路走过了。

回溯算法就是 深度优先搜索 的一种实现,我们要做的就是一条路走到黑,并且:1、如果在当前道路我们找到的一条出路,那么将其记录下来;2、如果在某个节点发现无论如何都无法继续下去时,则后退一步,在该节点尝试其他未尝试过的子节点,直到找到下一个出路:

8-queens-tree.png

上图即为回溯算法的基本思路:不撞南墙心不死,撞了南墙快回头! 如果换成本文探讨的8皇后问题,换成3皇后问题(3个皇后,在3*3的格子内放置,其他规则不变),则如下图:

3-queens.png

伪代码

让我们梳理一下本题的伪代码:

function backtracking(节点层级,其他参数):void {
  if (终止条件) {
    存放结果;
    return;
  }
  for (遍历:当前层级的所有节点) {
    if (本层节点验证通过) {
      处理节点;
      backtracking(节点层级 + 1,其他参数);
      回溯,撤销处理结果
    }
  }
  return;
}
复制代码

其中,比较难理解的点,其实在于这几行代码:

  if (本层节点验证通过) {
    处理节点;
    backtracking(节点层级 + 1,其他参数);
    回溯,撤销处理结果
  }
复制代码

仔细理解以下,即,有三种情况,查找路径会从子节点回到当前节点:

  1. 子节点符合题目要求,属于解
  2. 子节点被验证为不和乎验证要求;(放在N皇后问题中可以理解为有若干个皇后出现冲突)
  3. 子节点已经完成了所有节点的检索;

实现代码 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
};
复制代码

先看上面代码,是按照伪代码实现的一个骨架;主体包含了:

  1. 初始化的棋盘;
  2. 校验当前节点是否合法的check方法;
  3. 回溯方法

以下是最终代码

/**
 * @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
};
复制代码

Supongo que te gusta

Origin juejin.im/post/7032647486451744799
Recomendado
Clasificación