从递归回溯到动态规划,前端经典「N皇后」问题两种解法
2023-10-23 09:07:58
前言
在我的上一篇文章《前端电商 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 皇后问题。这两种解法都是经典的算法思想,在许多其他问题中也有广泛的应用。
希望通过这篇文章,你能对递归回溯和动态规划有更深入的理解,并能够将它们应用到自己的编程实践中。