【算法入门】【搜索与回溯算法】【N皇后问题】

这是一篇写给刚入门的同志的题解 无意冒犯大佬

题目描述

在一个nXn的国际象棋棋盘上放置n(n<=12)个皇后,使它们不能互相攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。试求出所有放置方法。

输入                                                                       输出

输入一个数n(n<=12)                                            输出所有的排列方案总数。

样例输入                                                                样例输出

4                                                                             2         

引入

搜索与回溯的理解

搜索?回溯?这是个什么东西?相信不少新手都问过这个问题

简单来说 搜索就是暴力枚举

具体来说 搜索是一种将所有情况一一判断,寻找满条件的答案

回溯又是什么呢

回溯通常用于遍历 这用于解决可由一种状态产生多种状态的题目

产生于搜索,回馈于搜索,搜索的高级产物,搜索的精髓所在(还有后面要学到的剪枝和记忆化)

常用于解决棋盘问题(如过河卒问题,素数环问题),放置问题(如n皇后问题,素数棋盘问题),取放问题(如01背包问题弱化版,机器分配问题)等

回溯的特性

回溯的遍历过程可近似看成一颗树,本质是递归

#include<cstdio>
using namespace std;
int f(int x)
{
	int s=0;
	for(int i=1;i<=x;i++)s+=f(i);
	return s;
}
int main()
{
	printf("%d",f(4));
	return 0;
}

这是一种回溯搜索 也叫递归

遍历过程如下图

34ceb52712fa4251b9b417aed058acc0.png

大脑模拟一遍遍历过程,可以发现,回溯搜索是优先向下遍历深度,再遍历同层的其他状态

由于回溯搜索的这种特性 我们总称回溯搜索为深度优先搜索,简称深搜或dfs(deep first search)

时间复杂度通常较大,请谨慎使用


解题

思考过程

因为上面刚讲 

因为不会打状压

因为这一题的每一层放置状态需要由之前放的点的状态得出,所以我们优先考虑深搜暴力

怎么搜

利用dfs枚举皇后所在位置

最后检查方法是否可行


计算时间复杂度

对于这种做法

每层有n种放法,共有n层,由乘法原理可得算法时间复杂度为O(n^n)

计算机每秒能处理约10^8次数据

很明显,12^12>10^8

所以该方法不可行


重新思考

我们发现,在遍历过程中,有很多一开始就不合法的状态

例如这种

ee78880858ab4de5a82a2e6b9b16f25b.png

还有这种

fb2b9965e9c44ab6a6cac5c2fa356de7.png

利用一些简单的组合数学,我们可以计算出这种一开始就不合法的状态占的比重是非常大的

发现,若一个状态的上一层状态不合法,那么这个状态也一定是不合法的

所以我们需要想办法让不合法状态不能继续产生新的状态

那么我们在遍历时判断该状态是否合法,就可以排除掉由不合法状态产生的不合法状态


计算新方法的时间复杂度

因为放完第x层后,第x+1层最多有(n-x)种状态,所以总状态数约为(n!)

12!<(10^8),所以想法可行



代码实现

首码

判断合法状态需要有之前的皇后方法状态,所以我们遍历时需要记录行列主副对角线

我们是顺层遍历的,所以不需要记录列

行非常好记录

主副对角线怎么记录呢

列一个表观察一下

b90f7e4d4091496b9e96895130ab0991.png

不难发现

在同一条正对角线中,横坐标与纵坐标之差都相等

在同一条副对角线中,横坐标与纵坐标之和都相等

所以得出,已知对角线上一点横坐标可得出纵坐标

得出代码

bool a[20][20],b[20][20],c[20][20];
void dfs(int t)
{
	if(t>n)
	{
		s++;
		return;
	}
	for(int i=1;i<=n;i++)
	{
		if(!a[t][i]&&!b[t][i]&&!c[t][i])
		{
			for(int j=1;j<=n;j++)a[j][t+i-j]=b[j][t-i+j]=c[j][i]=1;
			dfs(t+1);
			for(int j=1;j<=n;j++)a[j][t+i-j]=b[j][t-i+j]=c[j][i]=0;
		}
	}
}

负向越界

你会惊喜地发现,wa掉啦,还有可能是re

理性分析下为什么

数组大小开小了?我开的够了啊?

? !        (1,1)好像没有副对角线!

这就造成了负向越界

那肯定会有人说了        那我加个绝对值不就行了?

似乎确实可行

但是这样的话,一条副对角线就会拐弯了,会将部分原本合法的状态剔除

所以这种情况需要和零进行比较取大者

那我们同理可得,主对角线也需要与n取小者

#define max(a,b) a>=b?a:b
#define min(a,b) a>=b?b:a
bool a[20][20],b[20][20],c[20][20];
void dfs(int t)
{
	if(t>n)
	{
		s++;
		return;
	}
	for(int i=1;i<=n;i++)
	{
		if(!a[t][i]&&!b[t][i]&&!c[t][i])
		{
			for(int j=1;j<=n;j++)a[j][max(0,t+i-j)]=b[j][min(n,t-i+j)]=c[j][i]=1;
			dfs(t+1);
			for(int j=1;j<=n;j++)a[j][max(0,t+i-j)]=b[j][min(n,t-i+j)]=c[j][i]=0;
		}
	}
}

空间,时间优化

自信一发

TLE了 bukaixin

在这里发现,我们的行是已知的,遍历到哪里都一样

所以每一个标记的行都是可以省略的,记录的两个循环就可以省略了

那记录后面的 j 怎么办?

干脆不要了 遍历到哪就拿它当 j ,毕竟j本身就是枚举行的

那么这样,副对角线也不会负向越界了,那就可以把max,min删掉了

但是这样一来 主对角线就会负向越界了

那可能又会有人不长记性

那可能又会有人说了 那我套个abs绝对值一下不就行了?

但是,绝对值等于同一个值的数有两个

那么同理 标记值等于一个数的主对角线也有两条,这会剔除一些原本合法的状态

那既然我们不能用这种方式防止负向越界了

那怎么办?

把min拿回来?

但是取0以后又会使所有越界的合法状态消失

所以这种办法不行

那我们就只能利用加法强行赋个正值了

对于主对角线的标记值,最小值当然是在(n,1)的位置,标记值=1-n

让它加上一个n就一定为正了


想到这里,你就得出正解了

正解代码贴出来了

已在多个oj上ac

code

#include<cstdio>
using namespace std;
int n,s;
bool a[14],b[500],c[500];//a是列 b,c是对角线
void dfs(int t)
{
    if(t>n)//前n行都放完了
    {
        s++;//合法状态数+1
        return;
    }
    for(int i=1;i<=n;i++)
    {
        if(!a[i]&&!b[t-i+n]&&!c[t+i])//判断能否放置皇后
        {
            a[i]=1,b[t-i+n]=1,c[t+i]=1;//放置后记录 所在列 对角线
            dfs(t+1);//前往下一行
            a[i]=0,b[t-i+n]=0,c[t+i]=0;//回溯
        }
    }
}
int main()
{
    scanf("%d",&n);
    dfs(1);//从第一行开始枚举
    printf("%d",sum);
    return 0;
}

点个赞呗 😉

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值