内容介绍
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将
n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数
n
,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中
'Q'
和'.'
分别代表了皇后和空位。示例 1:
输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如上图所示,4 皇后问题存在两个不同的解法。示例 2:
输入:n = 1 输出:[["Q"]]提示:
1 <= n <= 9
完整代码
int solutionsSize;
char** generateBoard(int* queens, int n) {
char** board = (char**)malloc(sizeof(char*) * n);
for (int i = 0; i < n; i++) {
board[i] = (char*)malloc(sizeof(char) * (n + 1));
for (int j = 0; j < n; j++) board[i][j] = '.';
board[i][queens[i]] = 'Q', board[i][n] = 0;
}
return board;
}
void backtrack(char*** solutions, int* queens, int n, int row, int* columns, int* diagonals1, int* diagonals2) {
if (row == n) {
char** board = generateBoard(queens, n);
solutions[solutionsSize++] = board;
} else {
for (int i = 0; i < n; i++) {
if (columns[i]) {
continue;
}
int diagonal1 = row - i + n - 1;
if (diagonals1[diagonal1]) {
continue;
}
int diagonal2 = row + i;
if (diagonals2[diagonal2]) {
continue;
}
queens[row] = i;
columns[i] = true;
diagonals1[diagonal1] = true;
diagonals2[diagonal2] = true;
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns[i] = false;
diagonals1[diagonal1] = false;
diagonals2[diagonal2] = false;
}
}
}
char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
char*** solutions = malloc(sizeof(char**) * 501);
solutionsSize = 0;
int queens[n];
int columns[n];
int diagonals1[n + n];
int diagonals2[n + n];
memset(queens, -1, sizeof(queens));
memset(columns, 0, sizeof(columns));
memset(diagonals1, 0, sizeof(diagonals1));
memset(diagonals2, 0, sizeof(diagonals2));
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
*returnSize = solutionsSize;
*returnColumnSizes = malloc(sizeof(int*) * solutionsSize);
for (int i = 0; i < solutionsSize; i++) {
(*returnColumnSizes)[i] = n;
}
return solutions;
}
思路详解
一、问题背景
N皇后问题是一个经典的回溯算法问题,要求在一个N×N的棋盘上放置N个皇后,使得它们互不攻击。也就是说,任何两个皇后都不能处于同一行、同一列或同一斜线上。
二、解题思路
-
表示棋盘:
- 使用一维数组
queens
来表示棋盘,数组的索引代表行号,数组中每个元素的值代表在该行中皇后所在的列号。
- 使用一维数组
-
回溯算法:
- 采用回溯法尝试在每一行放置一个皇后,并检查是否与前面的皇后冲突。
- 如果冲突,则回溯到上一个状态,并尝试下一个可能的位置。
-
冲突检测:
- 使用三个布尔数组
columns
、diagonals1
和diagonals2
分别表示列和两个方向的斜线是否已被占用。 columns[i]
表示第i列是否已放置皇后。diagonals1[i]
表示从左上到右下的斜线是否已放置皇后。diagonals2[i]
表示从右上到左下的斜线是否已放置皇后。
- 使用三个布尔数组
三、代码详解
- 生成棋盘:
generateBoard
函数根据queens
数组生成对应的棋盘表示,其中’Q’表示皇后,'.'表示空位。
char** generateBoard(int* queens, int n) {
char** board = (char**)malloc(sizeof(char*) * n);
for (int i = 0; i < n; i++) {
board[i] = (char*)malloc(sizeof(char) * (n + 1));
for (int j = 0; j < n; j++) board[i][j] = '.';
board[i][queens[i]] = 'Q', board[i][n] = 0;
}
return board;
}
- 回溯函数:
backtrack
函数是回溯算法的核心,它尝试在每一行放置皇后,并递归地检查后续行的放置情况。
void backtrack(char*** solutions, int* queens, int n, int row, int* columns, int* diagonals1, int* diagonals2) {
if (row == n) {
char** board = generateBoard(queens, n);
solutions[solutionsSize++] = board;
} else {
for (int i = 0; i < n; i++) {
if (columns[i] || diagonals1[row - i + n - 1] || diagonals2[row + i]) {
continue;
}
queens[row] = i;
columns[i] = true;
diagonals1[row - i + n - 1] = true;
diagonals2[row + i] = true;
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns[i] = false;
diagonals1[row - i + n - 1] = false;
diagonals2[row + i] = false;
}
}
}
- 主函数:
solveNQueens
函数初始化所需的数据结构,并调用backtrack
函数开始回溯过程。
char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
char*** solutions = malloc(sizeof(char**) * 501);
solutionsSize = 0;
int queens[n];
int columns[n];
int diagonals1[n + n];
int diagonals2[n + n];
memset(queens, -1, sizeof(queens));
memset(columns, 0, sizeof(columns));
memset(diagonals1, 0, sizeof(diagonals1));
memset(diagonals2, 0, sizeof(diagonals2));
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
*returnSize = solutionsSize;
*returnColumnSizes = malloc(sizeof(int*) * solutionsSize);
for (int i = 0; i < solutionsSize; i++) {
(*returnColumnSizes)[i] = n;
}
return solutions;
}
四、总结
通过回溯算法,我们可以在N皇后问题中找到所有可能的解决方案。关键在于如何高效地检查冲突,并在找到冲突时回溯到上一个状态。这个算法的时间复杂度是O(N!),因为它需要尝试所有可能的皇后排列。通过使用布尔数组来标记列和斜线,我们能够快速地检查冲突,从而提高算法的效率。
知识点精炼
一、核心概念
- 回溯算法:一种通过试错来寻找问题解决方案的算法,适用于组合问题。
- 冲突检测:在放置皇后时,确保新放置的皇后不与已放置的皇后在同一列或同一斜线上。
- 位运算优化:使用布尔数组来标记列和斜线是否已被占用,提高冲突检测的效率。
二、知识点精炼
-
棋盘表示:
- 使用一维数组
queens
表示棋盘,其中索引代表行,值代表皇后所在的列。
- 使用一维数组
-
冲突检测数组:
columns
数组用于检测列冲突。diagonals1
和diagonals2
数组用于检测两个方向的斜线冲突。
-
回溯函数:
backtrack
函数递归地尝试在每一行放置皇后,并在放置后进行冲突检测。- 如果所有皇后都成功放置,则生成一个解决方案。
-
递归终止条件:
- 当所有行都已放置皇后时,表示找到一个有效解决方案。
-
状态重置:
- 在回溯时,需要将当前行的皇后位置、列标记和斜线标记重置,以便尝试其他可能的放置。
-
解决方案存储:
- 使用
solutions
数组存储所有找到的解决方案。 returnSize
和returnColumnSizes
用于记录和返回解决方案的数量和每行的列数。
- 使用
三、性能优化
- 空间优化:使用布尔数组而不是整型数组来减少空间占用。
- 时间优化:通过提前终止递归路径来减少不必要的计算。
四、实际应用
- 组合问题:N皇后问题是一种典型的组合问题,其解法可以推广到其他类似的组合优化问题。
- 算法竞赛:在算法竞赛中,掌握回溯算法对于解决组合类问题非常有帮助。
五、代码实现要点
- 动态内存分配:在生成棋盘和解决方案数组时,需要动态分配内存。
- 内存释放:在实际应用中,需要确保在适当的时候释放分配的内存,避免内存泄漏。
如何改进算法效率
N皇后问题的回溯算法已经相对高效,因为它避免了不必要的搜索空间。然而,仍有几种方法可以进一步优化算法效率:
-
位运算优化:
- 使用位运算来替代布尔数组进行冲突检测,可以减少内存使用并提高速度。每一位代表一列或一条斜线,通过位掩码(bitmask)来表示皇后的位置和攻击范围。
-
对称性剪枝:
- 由于棋盘的对称性,我们可以只搜索一半的棋盘(例如,只搜索左上角的区域),然后根据对称性生成其他部分的解。
-
迭代加深搜索(IDDFS):
- 迭代加深搜索可以用来找到解的第一个解,而不是所有解。它可以在找到第一个解后立即停止,从而提高效率。
-
分支限界法:
- 在搜索过程中,如果某一列或斜线上已经有皇后,则可以跳过该列或斜线上的其他位置,减少搜索空间。
以下是具体的优化方法:
位运算优化
使用三个整型变量来替代columns
、diagonals1
和diagonals2
数组,分别表示列和两个方向的斜线占用情况。
columnMask
:使用位掩码表示哪些列已经被占用。diag1Mask
:使用位掩码表示从左上到右下的斜线占用情况。diag2Mask
:使用位掩码表示从右上到左下的斜线占用情况。
位运算操作如下:
- 检查列是否被占用:
columnMask & (1 << i)
- 检查斜线是否被占用:
diag1Mask & (1 << (row - i + n - 1))
和diag2Mask & (1 << (row + i))
- 标记列和斜线被占用:
columnMask |= (1 << i)
,diag1Mask |= (1 << (row - i + n - 1))
,diag2Mask |= (1 << (row + i))
- 取消列和斜线标记:
columnMask &= ~(1 << i)
,diag1Mask &= ~(1 << (row - i + n - 1))
,diag2Mask &= ~(1 << (row + i))
对称性剪枝
只搜索棋盘的一部分,然后根据对称性生成其他部分的解。例如,可以只搜索主对角线以上的区域,然后根据对称性得到其他部分的解。
迭代加深搜索(IDDFS)
使用迭代加深搜索来找到第一个解,而不是所有解。这种方法可以在找到第一个解时立即停止搜索,适用于只需要一个解的情况。
分支限界法
在放置皇后时,如果某一列或斜线上已经有皇后,则可以直接跳过该列或斜线上的其他位置,减少搜索空间。
通过这些优化方法,可以显著提高N皇后问题的回溯算法效率,尤其是在解决大规模问题时。然而,需要注意的是,优化后的代码可能会更加复杂,且在某些情况下优化效果可能不如预期显著。在进行优化时,应该权衡代码的可读性和性能。