算法 回溯 【详解N皇后问题】

一、问题介绍

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

皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。

二、问题分析 

 N皇后是回溯算法的经典问题。

那么什么是回溯算法呢?

其实回溯算法和深度优先搜索很像,实际上是一个决策树的遍历过程,

一般求:满足条件的路径有哪些,有多少条。

我们需要思考三个问题:

1.路径:已经做出的选择。

2.选择列表:当前可以做的选择。

3.结束条件:到达决策树底层,无法再做选择的条件。

 

回溯算法的基本框架和遍历一颗多叉树非常类似:

result = [];

def backtrack(路径,选择列表)

{

    if 满足结束条件:

    result.add(路径);

    return ;

for 选择 in 选择列表

    {

    做选择;

    backtrack(路径,选择列表);

    撤销选择;

    }

}

核心,就是for循环里的递归:我们在递归调用之前做选择,在递归调用后撤销选择

那么回到我们的N皇后问题:

假设N = 8,也就是棋盘大小为8×8:

这是N = 8的一种放法。

我们先来研究一下三皇后:

根据回溯算法的原理,

首先,我们进行第一次backtrack调用,先在第一行的第一列位置放上第一个皇后;

然后对这个位置进行检查,看看是否有冲突——显然,第一个位置不会有任何冲突。

然后我们进行第二次backtrack函数调用,在第二行第一列位置放上一个皇后。

经过冲突检测,发现有冲突了,因为第一个皇后和第二个皇后连成了一条线,

所以此时产生冲突,也就无法继续调用backtrack将第三行的皇后进行摆放,

那么此时continue,进行第二次放置二行皇后的选择。

因为又冲突了,所以我们将第二行皇后放在第三列。

这时,因为放置成功,所以我们调用第三次backtrack,进入第三行。

因为第三行所有位置都无法放置,导致for循环全部进行完毕。

那么此时,回到了操作第二行的代码阶段。

第二行已经运行完了backtrack,也就是已经递归地将剩下所有行的情况都完成了,

那么我们需要再撤回它,回到第一行的第二轮放置中。

然后继续递归地调用backtrack,以Q在(1,2)为头,完成剩下的搜索。

所以我们的主函数backtrack应该这样来写:

void backtrack(vector<string>& board, int row)
{
	//结束条件,此时已到达决策树底层
	//每一行都成功放置了皇后,记录结果
	if (row == board.size())
	{
		ans.push_back(board);
		return;
	}
	//每一行的长度
	int length = board[row].size();
	//在当前行的每一列都可能放置皇后
	for (int col = 0; col < length; col++)
	{
		//首先进行冲突检测,排除会和其他棋子互相攻击的格子
		if (!isValid(board, row, col))
		{
			continue;
		}
		//开始做选择
		board[row][col] = 'Q';
		//进入下一行放皇后
		backtrack(board, row + 1);
		//撤销当前选择,在当前列的下一列放置新的皇后,
		//相当于换一个首结点,然后继续遍历决策树
		board[row][col] = '.';
	}
}

而对于冲突检测,我们只需要对三个位置检查即可:

上方,左上,左下。

因为本行由于是for循环迭代放置皇后,所以本行左右无需判断;

因为皇后是从上至下放置,所以整个下方无需判断。

bool isValid(vector<string>& board, int row, int col)
{
	//1.检测当前列的上方是否有皇后冲突
	for (int i = 0; i < row; i++)
	{
		if (board[i][col] == 'Q')
		{
			return false;
		}
	}
	//2.检测当前格的左上方是否有皇后冲突
	for (int i = row - 1, j = col - 1;
		i >= 0 && j >= 0;
		i--, j--)
	{
		if (board[i][j] == 'Q')
		{
			return false;
		}
	}
	//3.检测当前格的右上方是否有皇后冲突
	for (int i = row - 1, j = col + 1;
		i >= 0 && j < length;
		i--, j++)
	{
		if (board[i][j] == 'Q')
		{
			return false;
		}
	}
	//只需要检查三个位置。因为backtrack的约束,本行的左右不会有其他皇后
	//而且无需检查皇后下方的格子,因为是从上至下摆放的。
	return true;
}

最后放上总代码:

#include <stdio.h>
#include <iostream>
#include <vector>

using namespace std;

//存放最终结果的容器
vector<vector<string>> ans;

//是否可以在board[row][col]放置皇后?
bool isValid(vector<string>& board, int row, int col)
{
	//1.检测当前列的上方是否有皇后冲突
	for (int i = 0; i < row; i++)
	{
		if (board[i][col] == 'Q')
		{
			return false;
		}
	}
	//2.检测当前格的左上方是否有皇后冲突
	for (int i = row - 1, j = col - 1;
		i >= 0 && j >= 0;
		i--, j--)
	{
		if (board[i][j] == 'Q')
		{
			return false;
		}
	}
	//3.检测当前格的右上方是否有皇后冲突
	for (int i = row - 1, j = col + 1;
		i >= 0 && j < length;
		i--, j++)
	{
		if (board[i][j] == 'Q')
		{
			return false;
		}
	}
	//只需要检查三个位置。因为backtrack的约束,本行的左右不会有其他皇后
	//而且无需检查皇后下方的格子,因为是从上至下摆放的。
	return true;
}

void backtrack(vector<string>& board, int row)
{
	//结束条件,此时已到达决策树底层
	//每一行都成功放置了皇后,记录结果
	if (row == board.size())
	{
		ans.push_back(board);
		return;
	}
	//每一行的长度
	int length = board[row].size();
	//在当前行的每一列都可能放置皇后
	for (int col = 0; col < length; col++)
	{
		//首先进行冲突检测,排除会和其他棋子互相攻击的格子
		if (!isValid(board, row, col))
		{
			continue;
		}
		//开始做选择
		board[row][col] = 'Q';
		//进入下一行放皇后
		backtrack(board, row + 1);
		//撤销当前选择,在当前列的下一列放置新的皇后,
		//相当于换一个首结点,然后继续遍历决策树
		board[row][col] = '.';
	}
}

int solveNQueens(int n)
{
	vector<string> board(n, string(n, '.'));
	backtrack(board, 0);
	return ans.size();
}

int main()
{
	cout << solveNQueens(8);
	return 0;
}

运行截图:

八皇后共有92种解法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值