超详解N皇后问题,两种解决方法,带你快速复习dfs暴力问题

本篇所有代码均正确已通过测试,请放心食用

题目

一个如下的 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

上面的布局可以用序列 2 4 6 1 3 5 来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子,如下:

行号 1 2 3 4 5 6

列号 2 4 6 1 3 5

这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 3 个解。最后一行是解的总个数。

输入格式
一行一个正整数 n,表示棋盘是 n×n 大小的。

输出格式
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。

输入输出样例
输入 

6


输出 

2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4

说明/提示
【数据范围】
对于 100% 的数据,6≤n≤13。

题目解析

1. 每行每列只能有一个皇后

2. 每条对角线上只能有一个皇后

3. 需要输出前三种摆法以及方案总数,每种摆法由按行从小到大的顺序输出所占列号

需要输出方案总数,且皇后摆放位置有要求,且实验数据范围较小,那么这是一个有条件的排列问题,直接用dfs解决。

分析思路

数据要求:

int n;//规格
bool st[20];//对应列序号是否被选
int total;//解的总数
int a[15];//a[i]记录第i行的皇后在第几列,输出结果时,按顺序访问即可

解析:dfs最经典的数据就是一个布尔类型的数组(根据具体题目定维数),用于标记该元素是否被选,从而生成解空间树的不同分支。

区别于普通的排列dfs问题,此处还用到了一个int型数组a,这是因为最终的要求需要输出前三种方案,无论算法如何,一定需要一个数组存储每行对应占了哪一列,才能方便输出,否则仅仅一个数组st,无法记录每列选择的顺序。

对于列举出的方案,建议读者画出解空间树,dfs()函数中,用for循环横向遍历解空间树的同一层元素,用递归纵向遍历一条分支,触底或者不满足条件时则回溯或剪枝。

那么如何列举或者选择元素呢,这是便用到了bool数组st,以及N皇后的摆放条件。

关键:如何判断皇后摆放的合法性:

  1. dfs()函数中,传参k表示目前已经摆了k个皇后,即当前所在行为k+1,前k行已经被摆好了。因此可保证一行只有一个皇后。
  2. bool st[i]数组用于标识第i列是否被选择,被选择为1,未被选则为0,因此可保证一列上只有一个皇后。
  3. 每条对角线上只有一个皇后:
  • 方法一:观察可得列两个元素若在同一条对角线上,那么行数之差=列数之差
  •  方法二:设置两个bool类型数组,专门标记每条对角线上是否已被占。列数学算式不难发现:每个表格中每条对角线均可用唯一数字标识:主对角线方向的对角线:行号-列号+n;副对角线上方向:行号+列号。读者可参考以下图片自行验证。

主对角线方向的对角线:

12345
1  
2   
3    
4    
5   

副对角线方向上的对角线:

12345
1   
2    
3    
4   
5  

两种方法的代码实现分别如下:

代码实现

方法一:

利用对角线上行列之差相等

#include <iostream>
using namespace std;
#include<math.h>

int n;//规格
bool st[20];//对应列序号是否被选
int total;//解的总数
int a[15];//a[i]记录第i行的皇后在第几列,输出结果时,按顺序访问即可

void print()
{
	if (total < 3)
	{
		for (int i = 1; i <= n; i++)
		{
			cout << a[i] << " ";
		}
		cout << endl;
	}
}

bool Ndiagonal(int c,int k)//传入当前列号,前面已摆好的行数
//判断是否成对角线就是和前面所有列号进行对比
{
	//如果当前行数与对比目标相差k行,那么列数相差k列的话就是在一个对角线上
	for (int i = 1; i <= k; i++)
	{
		if (abs(c - a[i]) == k + 1 - i)
			return false;
	}

	return true;

}

void dfs(int k)//目前已经摆了k个皇后,即当前所在行为k+1,前k行已经被摆好了
{
	if (k == n)
	{
		print();
		total++;
		return;
	}

	//for循环遍历列,每层递归遍历行
	for (int i = 1; i <= n; i++)
	{
		//关键:如何判断皇后摆放的合法性:目前只需要判断是否成对角线即可
		if (!st[i] && Ndiagonal(i,k))
		{
			st[i] = 1;
			a[k + 1] = i;
			dfs(k + 1);
			st[i] = 0;
		}
	}
}

int main()
{
	cin >> n;
	dfs(0);
	cout << total;
}

运行结果:

方法二:

用布尔数组标记每条对角线上的皇后摆放情况,此代码为博主在洛谷上看了其他大牛的题解后自己复现了一遍,发现用数学关系标识不同的对角线的方式十分巧妙,于是写入本篇博客供大家一起学习。

#include <iostream>
using namespace std;

int n;//棋盘规模
//数组范围要尽量大一点!!!
bool c[15],d1[100],d2[100];//c标记列有无被选中,d1标记该主对角线方向的对角线是否被占用,d2标记该副对角线方向的对角线
int a[15];//a[i]记录第i行占用的列数,用于后续打印
int total;

void print()
{
    if (total < 3)
    {
        for (int i = 1; i <= n; i++)
        {
            cout << a[i] << " ";
        }
        cout << endl;
    }
    total++;
}

void queen(int r)//当前所在第r行,即表示行数,又可以计数作为终结条件
{
    if (r > n)
    {
        print();
        return;
    }

    for (int i = 1; i <= n; i++)
    {
        if (!c[i] && !d1[r - i + n] && !d2[i + r])//如果当前位置没被选择,且不与已选择的元素发生冲突
        {
            c[i] = 1;
            d1[r - i + n] = 1;
            d2[i + r] = 1;
            a[r] = i;
            queen(r + 1);
            c[i] = 0;
            d1[r - i + n] = 0;
            d2[i + r] = 0;
        }
    }
}

int main()
{
    cin >> n;
    queen(1);
    cout << total;
}

注意!数组范围要尽量大一点,起初提交未能全部AC,下意识觉得是数组范围问题,果真如此。对于大小未知的数组的范围大家还是尽量写大一点。

运行结果:

相应的解释已经写在代码注释中,除此以外,对于两种方法的dfs()中的for循环里这一段代码我还想额外解释一下,尤其是对于初学者:(两种方式都大同小异,此处以方法一为例,方法二类似)

for (int i = 1; i <= n; i++)
	{
		//关键:如何判断皇后摆放的合法性:目前只需要判断是否成对角线即可
		if (!st[i] && Ndiagonal(i,k))
		{
			st[i] = 1;
			a[k + 1] = i;
			dfs(k + 1);
			st[i] = 0;
		}
	}

调用dfs(k+1)后,回溯的时候为何只复原了st[i]=0,而没有管a[k+1]?

因为:

  1. a[j]的值在此处并不影响皇后位置的选择,即:不影响判断条件
  2. a[j]在之后的for循环中会被复写,所以不会影响未来的数据。

所以,在dfs的回溯过程中,只需要将会影响本次操作的元素复原即可,类似的还有求和操作等(是需要复原的)。

后续会继续更新dfs类型题目的相关题解与知识点技巧总结,欢迎评论区留言与我共同讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值