返回

从递归回溯到动态规划,前端经典「N皇后」问题两种解法

前端

前言

在我的上一篇文章《前端电商 sku 的全排列算法很难吗?学会这个套路,彻底掌握排列组合。》中,我详细讲解了排列组合的递归回溯解法,相信看过的小伙伴们对这个套路已经有了一定程度的掌握(没看过的同学快回头学习~)。

在这篇文章中,我们将继续学习另一个经典算法问题——N皇后问题。这是一道 LeetCode 上难度为 hard 的题目,听起来很吓人,但其实只要掌握了正确的解题思路,并不难理解。

问题

N皇后问题是这样的:在一个 N x N 的棋盘上摆放 N 个皇后,使得任意两个皇后互不攻击。

也就是说,皇后不能放在同一行、同一列或同一对角线上。

比如,在一个 4 x 4 的棋盘上,我们可以这样摆放 4 个皇后:

_ _ _ Q
_ _ Q _
Q _ _ _
_ Q _ _

在这个摆放方案中,任意两个皇后都不在同一行、同一列或同一对角线上,因此这是一个合法的解。

解法一:递归回溯

我们首先来看一下如何使用递归回溯来解决 N 皇后问题。

递归回溯是一种经典的算法思想,它通过不断尝试不同的可能性,并在遇到死胡同时回溯到上一个状态来寻找新的可能性。

在 N 皇后问题中,我们可以使用递归回溯来枚举所有可能的皇后摆放方案,并检查每个方案是否合法。如果一个方案合法,我们就将它保存下来;如果一个方案不合法,我们就回溯到上一个状态继续枚举。

function solveNQueens(n) {
  const result = [];
  const board = new Array(n).fill(0).map(() => new Array(n).fill('.'));

  function isSafe(row, col) {
    for (let i = 0; i < row; i++) {
      if (board[i][col] === 'Q') {
        return false;
      }
    }

    for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
      if (board[i][j] === 'Q') {
        return false;
      }
    }

    for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
      if (board[i][j] === 'Q') {
        return false;
      }
    }

    return true;
  }

  function backtrack(row) {
    if (row === n) {
      result.push(board.map(row => row.join('')));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (isSafe(row, col)) {
        board[row][col] = 'Q';
        backtrack(row + 1);
        board[row][col] = '.';
      }
    }
  }

  backtrack(0);

  return result;
}

在上面的代码中,solveNQueens 函数接受一个参数 n,表示棋盘的边长。函数返回一个数组,其中包含所有合法的皇后摆放方案。

isSafe 函数检查一个皇后摆放方案是否合法。如果一个皇后摆放方案合法,那么它返回 true;否则,它返回 false

backtrack 函数使用递归回溯来枚举所有可能的皇后摆放方案。如果一个皇后摆放方案合法,那么它将这个方案保存下来,并继续枚举下一个皇后摆放方案;否则,它回溯到上一个状态继续枚举。

解法二:动态规划

除了递归回溯,我们还可以使用动态规划来解决 N 皇后问题。

动态规划是一种经典的算法思想,它通过将问题分解成更小的子问题,并存储子问题的解,来解决大问题。

在 N 皇后问题中,我们可以将问题分解成 N 个子问题:在第 i 行摆放一个皇后,使得任意两个皇后互不攻击。

我们定义一个状态 dp[i][j],表示在第 i 行的前 j 列摆放一个皇后,使得任意两个皇后互不攻击。

如果 dp[i][j]true,则表示在第 i 行的前 j 列摆放一个皇后是合法的;否则,则表示在第 i 行的前 j 列摆放一个皇后是不合法的。

我们还可以定义一个状态 path[i][j],表示在第 i 行的前 j 列摆放一个皇后时,这个皇后摆放在第 j 列。

如果 path[i][j]true,则表示在第 i 行的第 j 列摆放了一个皇后;否则,则表示在第 i 行的第 j 列没有摆放皇后。

function solveNQueens(n) {
  const result = [];
  const dp = new Array(n).fill(0).map(() => new Array(n).fill(false));
  const path = new Array(n).fill(0).map(() => new Array(n).fill(false));

  function isSafe(row, col) {
    for (let i = 0; i < row; i++) {
      if (path[i][col]) {
        return false;
      }
    }

    for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
      if (path[i][j]) {
        return false;
      }
    }

    for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
      if (path[i][j]) {
        return false;
      }
    }

    return true;
  }

  function backtrack(row) {
    if (row === n) {
      result.push(board.map(row => row.join('')));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (isSafe(row, col)) {
        path[row][col] = true;
        backtrack(row + 1);
        path[row][col] = false;
      }
    }
  }

  backtrack(0);

  return result;
}

在上面的代码中,solveNQueens 函数接受一个参数 n,表示棋盘的边长。函数返回一个数组,其中包含所有合法的皇后摆放方案。

isSafe 函数检查一个皇后摆放方案是否合法。如果一个皇后摆放方案合法,那么它返回 true;否则,它返回 false

backtrack 函数使用动态规划来枚举所有可能的皇后摆放方案。如果一个皇后摆放方案合法,那么它将这个方案保存下来,并继续枚举下一个皇后摆放方案;否则,它回溯到上一个状态继续枚举。

总结

在这篇文章中,我们学习了如何使用递归回溯和动态规划来解决 N 皇后问题。这两种解法都是经典的算法思想,在许多其他问题中也有广泛的应用。

希望通过这篇文章,你能对递归回溯和动态规划有更深入的理解,并能够将它们应用到自己的编程实践中。