N皇后问题
问题定义
- 棋盘: 一个n×n的网格。
- 皇后: 一种特殊棋子,可以攻击同一行、同一列或两条对角线上的任何棋子。
- 目标: 在棋盘上放置n个皇后,使得它们之间没有任何一个能够攻击到对方。
问题难点
- 确保皇后之间不在同一行或列。
- 避免皇后在对角线上相互攻击。
题目分析
问题概述
在n×n的棋盘上放置n个皇后,要求它们互不攻击。我们使用一个一维数组来存储皇后的坐标。
坐标存储
- 使用一个长度为n的整型数组
path[]
来存储皇后的坐标。 - 数组的下标表示行,数组的值表示列。
皇后坐标示例
- 第0个皇后坐标:(0, path[0])
- 第1个皇后坐标:(1, path[1])
- …
- 第i个皇后坐标:(i, path[i])
数组选择理由
- 使用一维数组
path[]
可以体现深度优先搜索的思想。 - 一维数组方便进行回溯操作。
主函数设计
编写 main
函数,输入棋盘大小 n
并初始化 path[]
数组。
int main() {
int n;
scanf("%d", &n);
// 动态分配内存,存储皇后放置的路径
int* path = (int*)malloc(sizeof(int) * n);
// 注意:需要在使用完毕后释放内存
printf("\n最终结果为:%d种", cout); // 此处应为算法执行后的输出结果
free(path); // 释放分配的内存
return 0;
}
函数规划
1. choose
函数
- 从第0行开始,逐行选择放置新皇后的坐标。
2. judge
函数
- 判断新皇后是否可以放置在特定坐标,确保不与已放置的皇后冲突。
3. prt
函数(可选)
- 打印棋盘格局,传入
path[]
和n
即可。
上述代码中的cout
应替换为具体的算法执行后的输出结果变量,以及malloc
后的内存需要在使用完毕后通过free
函数释放。
函数设计概述
为了解决n皇后问题,我们需要设计以下三个核心函数:
1. choose
函数
- 目的: 从棋盘的第一行开始,逐行放置皇后。
- 过程: 选择每一行中可以放置皇后的列。
2. judge
函数
- 目的: 判断新皇后是否可以放置在当前考虑的列上。
- 条件: 确保新皇后不会与已放置的皇后在同一行、列或对角线上。
3. prt
函数(可选)
- 目的: 打印出所有皇后的放置位置,展示棋盘的格局。
- 输入: 路径数组
path[]
和棋盘大小n
。
函数效果预览
完成所有函数设计后,将能够实现以下效果:
- 逐行放置皇后,直到所有皇后都放置完毕或找到所有可能的解决方案。
- 通过
judge
函数确保每次放置都符合规则。 - 使用
prt
函数可视化解决方案,便于理解和验证。
prt
函数实现
首先,我们从最简单的打印函数 prt
开始。这个函数负责打印出棋盘上皇后的位置。
函数参数
int* path
: 指向存储皇后列坐标的数组的指针。int n
: 棋盘的大小。
函数逻辑
- 通过两个嵌套循环遍历棋盘的每一行和每一列。
- 根据
path[i]
的值判断当前位置是否有皇后,并打印相应的字符。
代码实现
void prt(int* path, int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (path[i] == j) // 如果当前位置有皇后
printf("e "); // 打印 "e"
else
printf("0 "); // 否则打印 "0"
}
printf("\n");
}
printf("\n\n");
}
choose
函数实现
接下来是 choose
函数,它负责在棋盘上逐行放置皇后。
函数逻辑
- 从第0行开始,逐行尝试放置皇后。
- 遇到三种情况:
- 当前格子符合条件,记录皇后位置,进入下一行。
- 当前格子不符合条件,尝试同一行的下一个格子。
- 如果一行内所有格子都不符合条件,回溯至上一行,尝试下一个可能的位置。
首先,在第0行choose到坐标(0,0),发现符合条件,放置。
放置后触发情况1,进入下一行的choose,发现坐标(1,0)不可以,因为两个皇后在一列。
触发情况2,我们让第一行的皇后向右移动,可还是不行。
再次触发情况2,再次移动,发现可以,于是触发情况1,到下一行进行choose
但是这时候我们发现,下一行的皇后无论放到哪一列都与旧的皇后矛盾,不是同列就是对角线
回溯法的应用
回溯法的精髓
在n皇后问题中,如果某一步发现当前路径已经无法继续往下走,我们就需要回溯到上一步,这是回溯法的核心思想。
剪枝定义
剪枝是一种算法优化策略,它通过去除搜索树中不必要的或无用的分支来减少搜索的时间和空间复杂度。
应用场景
在回溯算法中,剪枝是一种常用的优化手段。
剪枝操作
- 当确定某一种情况不可能出现解时,算法会直接返回,不再继续深入搜索。
剪枝的好处
- 当第三行已经没有合适的位置放置皇后时,继续搜索第四行的位置是没有意义的。
- 通过回溯,计算机避免了在错误路径上浪费时间,这是一种有效的剪枝策略。
避免无效搜索
- 计算机不会继续考虑那些看似暂时符合规则,但实际上已经不符合条件子解空间的情况。
我们回溯到上一行,那么上一行便进入情况2(这个格子不符合条件),皇后向右
发现可行,于是进入情况1,再次进入下一行搜索,搜索到一个位置。
继续进入下一行,发现再次全都不行,进入情况3,开始回溯。
如此循环往复,直到回溯到第0行向右移动,接着搜索,直到一个正确的结果出现了。
choose
函数设计
choose
函数是回溯算法的核心,负责在棋盘上逐行放置皇后。
函数参数
int* path
: 已放置皇后的列坐标数组。int line
: 当前正在选择的行。int n
: 棋盘的总行数。
函数逻辑
- 基本情况: 如果所有行都放置了皇后,打印棋盘格局并返回。
- 逐列遍历: 对当前行的每一列进行遍历,尝试放置皇后。
- 条件判断: 使用
judge
函数判断新皇后是否与旧皇后冲突。
- 流程:
- 遍历每一列,逐列尝试放置皇后。
- 对于每一行,检查是否可以放置皇后:
- 情况1:如果当前位置可行(即没有与其他皇后相互攻击),则记录皇后的位置
path[line] = column
,然后递归调用choose(path, line + 1, n)
继续放置下一行的皇后。 - 情况2:如果当前位置不可行,皇后向右移动到下一列
column++
。 - 情况3:如果所有列都尝试完毕,无法放置皇后,则回溯到上一行
return false
。
- 情况1:如果当前位置可行(即没有与其他皇后相互攻击),则记录皇后的位置
示例
- 假设
choose(path, 1, 4)
成功放置了第一个皇后在(1, 2)
的位置,即path[1] = 2
。 - 然后,递归调用
choose(path, 2, 4)
继续放置第二个皇后。
代码实现
int choose(int* path, int line, int n) {
// 基本情况:所有皇后都放置完毕
if (line == n) {
prt(path, n);
return 1; // 表示找到一个解决方案
}
// 遍历当前行的所有列
for (int column = 0; column < n; column++) {
// 如果当前列可以放置皇后
if (judge(path, line, column)) {
path[line] = column; // 记录皇后位置
choose(path, line + 1, n); // 递归调用,选择下一行
}
}
// 如果当前行无法放置皇后,则回溯
return 0; // 表示当前路径不可行
}
注意事项
judge
函数是判断新皇后是否与已放置的皇后冲突的关键。choose
函数通过递归实现,每找到一个解决方案就打印一次棋盘格局。
判断函数 judge
judge
函数用于判断新放置的皇后是否与已放置的皇后冲突。
函数目的
- 验证新皇后的位置
(line, column)
是否与已放置的皇后位置冲突。
参数说明
path[]
:存储已放置皇后的坐标数组。line
:新皇后的行号。column
:新皇后的列号。
皇后坐标表示
- 第0个皇后坐标:
(0, path[0])
- 第1个皇后坐标:
(1, path[1])
- …
- 第i个皇后坐标:
(i, path[i])
判断逻辑
- 遍历已放置的皇后:检查每个已放置的皇后与新皇后的位置关系。
- 冲突判断:
- 同行冲突:由于新皇后是在前
line-1
行放置的,所以无需检查同行。 - 列冲突:检查新皇后的列号
column
是否与已放置的皇后列号相同。 - 对角线冲突:通过一种巧妙的方法判断对角线是否冲突。
- 同行冲突:由于新皇后是在前
函数返回
- 如果所有已放置的皇后与新皇后均不冲突,返回
true
。 - 如果存在任何冲突,返回
false
。
冲突判断方法
- 对角线冲突:通过计算两个皇后的行和列的差值是否相等来判断对角线冲突。
示例
- 假设
path = [0, 2, 4]
,新皇后的位置为(3, 3)
。 - 遍历
path
中的每个皇后,检查与(3, 3)
是否冲突。 - 如果没有冲突,
judge
函数返回true
。 - 如果存在冲突,
judge
函数返回false
。
对于(x1,y1)和(x2,y2),若
,则二点处于对角线上。可以分类讨论证明。至于绝对值,利用#include<math.h>中的abs函数,
int judge(int* path, int line, int column) {
// 遍历已放置的皇后,检查是否与新皇后冲突
for (int i = 0; i < line; i++) {
// 如果新皇后与已放置的皇后在同一列上
if (path[i] == column) {
// 返回false,表示存在列冲突
return false;
}
// 如果新皇后与已放置的皇后在同一对角线上
if (abs(path[i] - column) == abs(i - line)) {
// 返回false,表示存在对角线冲突
return false;
}
// 如果当前皇后与新皇后不冲突,则继续检查下一个皇后
else {
continue;
}
}
// 如果遍历完所有已放置的皇后后没有发现冲突
// 则返回true,表示新皇后可以放置在指定位置
return true;
}
完整代码:
#include<stdio.h> // 引入标准输入输出库
#include<math.h> // 引入数学库,用于使用绝对值函数abs
#include<malloc.h> // 引入内存分配库,用于动态内存分配
// 计数器,用来存储最终有多少种放置皇后的方案
int cout = 0;
// 打印函数,用于打印棋盘上皇后的放置结果
void prt(int* path, int n) {
for (int i = 0; i < n; i++) { // 遍历每一行
for (int j = 0; j < n; j++) { // 遍历每一列
if (path[i] == j) // 如果当前位置是皇后,则打印"e"
printf("e ");
else // 否则打印"0"
printf("0 ");
}
printf("\n"); // 每行结束后换行
}
printf("\n\n"); // 打印完一个结果后空两行
}
// 判断函数,用于判断新皇后是否与已有皇后冲突
int judge(int* path, int line, int column) {
for (int i = 0; i < line; i++) { // 遍历已放置的皇后
// 如果新皇后与老皇后在同一列或对角线上,则返回false
if ((path[i] == column) || ((abs(path[i] - column)) == (abs(i - line))))
return false;
}
// 如果遍历完所有已放置的皇后都没有冲突,则返回true
return true;
}
// 选择函数,用于递归选择皇后的位置
int choose(int* path, int line, int n) {
if (line == n) { // 如果已经放置完所有皇后,则打印结果并计数
prt(path, n);
cout++;
return 0;
}
for (int column = 0; column < n; column++) { // 尝试在当前行的每一列放置皇后
if (judge(path, line, column)) { // 如果当前位置不冲突
path[line] = column; // 记录皇后的位置
choose(path, line + 1, n); // 递归放置下一行的皇后
}
}
return 0;
}
// 主函数,程序的入口点
int main() {
int n; // 棋盘大小
scanf("%d", &n); // 从标准输入读取棋盘大小
// 动态分配内存,大小为n个整数,用来存储皇后的放置路径
int* path = (int*)malloc(sizeof(int) * n);
// 从第0行开始选择皇后的位置,棋盘大小为n
choose(path, 0, n);
// 打印最终的皇后放置方案数量
printf("\n最终结果为:%d种", cout);
return 0; // 程序结束
}
总结
题目思路总结
- 起始点: 从棋盘的第一行开始放置皇后。
- 逐行放置: 按顺序逐行放置皇后,并进行冲突检查。
- 冲突与回溯: 如果当前行无法放置皇后,回溯至上一行重新尝试。
- 合法解的发现: 当所有行都成功放置了皇后,即找到了一个合法解。
- 记录方式: 使用数组记录每行皇后的列位置。
具体实现策略
- 从第一行开始,逐行尝试放置皇后。
- 在放置每个皇后时,检查与已放置皇后的冲突情况。
- 若发生冲突,回溯至上一行重新放置。
- 所有行放置完毕,记录一个解决方案。
回溯法与剪枝总结
- 回溯法: 一种通过试错枚举所有可能解的算法。
- 剪枝: 在回溯过程中,通过特定条件避免无效搜索,优化搜索树。
- 优化目的: 减少搜索的时间和空间消耗,提升算法效率。
- 剪枝重要性: 作为回溯法中的重要优化手段,显著提高算法性能。