您的位置:首页 > 产品设计 > UI/UE

[LeetCode] [N皇后问题] N-Queens & N-Queens II

2013-12-14 15:41 519 查看
N-Queens

The n-queens puzzle is the problem of placing n queens on ann×n chessboard such that no two queens attack each other.



Given an integer n, return all distinct solutions to the n-queens puzzle.

Each solution contains a distinct board configuration of the n-queens' placement, where
'Q'
and
'.'
both indicate a queen and an empty space respectively.

For example,

There exist two distinct solutions to the 4-queens puzzle:

[
[".Q..",  // Solution 1
"...Q",
"Q...",
"..Q."],

["..Q.",  // Solution 2
"Q...",
"...Q",
".Q.."]
]

问题描述:这是著名的N皇后问题,意思是将N个皇后放在N*N的棋盘上,保证任意两个皇后不会互相攻击。
给定一个整数n,返回所有不同的解决方案,每个解决方案包含一个放置n个皇后的棋盘,棋盘上'Q'代表有皇后,'.'代表该位置为空。

皇后问题是一个著名而古老的问题,是回溯算法的典型例题。在国际象棋中,皇后是最强大的棋子,它可以攻击包括同一行、同一列和同一对角线上的其它成员,因此,两个皇后不会互相攻击的意思就是,棋盘上不会有两个皇后在同一行、同一列和同一对角线上。

1 最简单明了的方法实现N皇后问题

如果是人来放的话,人会如何做呢?要想使任意两个皇后都不会互相攻击,N个皇后中不会有两个放在一行,必定是每行都有一个,因此,可以对N个皇后一行一行地放,在判断当前放的皇后是否会与其它皇后互相攻击时,可以只考虑列和对角线。可以先将第一个皇后放在第一行的第一列,然后再放第二个皇后放在第二行,排除刚刚已经放的第一个皇后可以攻击的范围,它可以放在第二行的第三列,然后依次放后面的皇后。这中间就包含了回溯,当将第i个皇后放在第i行时,它会依次测试第i行的N个位置,如果没有合适的位置,就要改变第i-1个皇后的位置,使它放在下一列。

于是,可以根据上面的思路来设计程序。撇开这个题目本身,要解决的就是N皇后问题,可以先从最原始的办法开始。

数据结构:用包含N*N个元素的二维数组保存这个棋盘,用int型变量来保存总共有多少种解法。

实现函数:

void print_board():打印整个棋盘,当放完第N个皇后时,就说明找到了一个解,然后就可以打印整个棋盘输出这个解。

void place_queen(int i):放第i行的皇后,i从0开始,也就是第0到第N-1个皇后。如果i == N,即已经放完了N个皇后,因为皇后计数是从0到N-1,此时就可以输出一个解了。否则,它会依次测试0到N-1列,将皇后放在j列上,然后判断冲突,看是否刚刚放置的皇后是否与之前放置的皇后有互相攻击的情况,如果没有,则继续放第i + 1行的皇后,如果有互相攻击的情况,就说明不能放在这个位置,将这个位置置空,然后测试下一列。

int is_valid(int n, int col):测试将n行的皇后放在col列是否会互相攻击,这里采用最简单易懂的方式,前面已经提到,不需要考虑行,只需要考虑列和对角线,而且只需要考虑0到n - 1行的皇后是否会与n行的皇后相互攻击。分为三个部分:col列,该位置的西北方向和东北方向。

在主函数中直接调用place_queen(0);从0皇后开始放置。

#include <stdio.h>

#define MAXN	8

int board[MAXN][MAXN];
int sol_cnt;

void print_board()
{
int i = 0, j = 0;

for(i = 0; i < MAXN; ++i) {
for(j = 0; j < MAXN; ++j) {
printf("%d", board[i][j]);
}
printf("\n");
}
printf("\n");
}

void place_queen(int i)
{
int j = 0, k = 0;

if(i == MAXN) {
++sol_cnt;
print_board();
return;
}

for(j = 0; j < MAXN; ++j) {
for(k = 0; k < MAXN; ++k) {
board[i][k] = 0;
}
if(is_valid(i, j)) {
board[i][j] = 1;
place_queen(i+1);
}
}
}

int is_valid(int n, int col)
{
int i = 0, j = 0;

for(j = 0; j < n; ++j) {
if(board[j][col] == 1)
return 0;
}

for(i = n, j = col; i >= 0 && j >= 0; --i, --j) {
if(board[i][j] == 1)
return 0;
}

for(i = n, j = col; i >= 0 && j < MAXN; --i, ++j) {
if(board[i][j] == 1)
return 0;
}

return 1;
}

int main(int argc, char *argv[])
{
place_queen(0);
printf("%d solutions!\n", sol_cnt);

return 0;
}
注意上面place_queen()函数中for循环中,在测试某个位置是否可以放置皇后之前先将该行清0,如果不这样做,假设测试(i, j)位置,可以放置,则在(i, j)放置一个皇后,然后放下一个皇后,当下一个皇后每一列都不能放置时,就要将(i, j)的皇后放置到(i, j + 1),如果此时不清空,会导致多个皇后在一行,而且后面放置皇后时,测试是否可以放置皇后会产生错误判断。

通过上面的解释,代码应该很好懂了吧?

不过,代码略显臃肿,特别是棋盘要用一个二维数组,而且在测试是否可以放置皇后时,用了三个for循环。那么,是否可以简化呢?

2 简化数据结构实现N皇后问题

由于N个皇后肯定不在同一行和同一列,可以考虑用一个N个成员的一维数组来保存N个皇后的位置。

定义int queens
,queens[i] = j;表示i行的皇后在j列。这样一来,既可以简化place_queen()中清0的for循环,因为不可能有两个在一行了,还可以简化测试是否可以放置皇后的is_valid()。

#include <stdio.h>

#define MAXN	8

int queens[MAXN];
int sol_cnt;

void print_board()
{
int i = 0, j = 0;

for(i = 0; i < MAXN; ++i) {
for(j = 0; j < MAXN; ++j) {
if(queens[i] == j) {
printf("Q");
}
else {
printf("0");
}
}
printf("\n");
}
printf("\n");
}

void place_queen(int i)
{
if(i == MAXN) {
++sol_cnt;
print_board();
return;
}

int j = 0;

for(j = 0; j < MAXN; ++j) {
queens[i] = j;

if(is_valid(i)) {
place_queen(i+1);
}
}
}

int is_valid(int n)
{
int i = 0;

for(i = 0; i < n; ++i) {
if(queens[i] == queens
) {
return 0;
}

if(abs(queens[i] - queens
) == (n - i)) {
return 0;
}
}

return 1;
}

int main(int argc, char *argv[])
{
place_queen(0);
printf("%d solutions!\n", sol_cnt);

return 0;
}
下面依次分析上面的代码的改进之处,以及改进后哪些代码要相应调整。

void print_board():由于没有保存棋盘本身,而是保存了N个皇后的列数,因此,在打印棋盘的时候,要根据该行的皇后在的列数来打印,很好理解。

void place_queen(int i):这里不可能有两个皇后在同一行的情况,不需清0。而且,由于后面int is_valid(int n)要使用i行皇后的列,因此,是先放置皇后,然后测试是否会产生相互攻击。

int is_valid(int n):这个函数的代码是最不易理解的部分。首先,它的参数只有一个,之前有两个,因为queens[i]已经保存了列。其次,前面已经提到,在测试是否会产生攻击时,不需要考虑行,只需要考虑列和对角线,从列来看,因为queens[i]已经保存了列,所以,如果之前已经放置的皇后的列跟queens
相同,那么就说明有两个皇后在同一列,于是产生攻击,从对角线来看,当两个皇后的行的差值与列的差值相同,就说明两个在对角线上,于是产生攻击。abs(queens[i] - queens
)得到的就是皇后i和皇后i的列的差值,由于两个差值的大小关系不定,因此取绝对值,而n
- i得到的就是皇后i和皇后n的行的差值,如果两个值相等,就产生攻击。

OK,讲到这儿,就该回到正题了,就是LeetCode上的这两个题了,上面的代码稍加改造就可以很轻易地得到N-Queens的解了:

class Solution {
public:
vector<int> queens;
vector<vector<int> > sols;

void place_queen(int i, int n)
{
if(i == n) {
sols.push_back(queens);
return;
}

int j = 0;

for(j = 0; j < n; ++j) {
queens[i] = j;

if(is_valid(i))
place_queen(i+1, n);
}
}

bool is_valid(int n)
{
int i = 0, j = 0;

for(i = 0; i < n; ++i) {
if(queens[i] == queens
) {
return false;
}

if(abs(queens[i] - queens
) == (n - i)) {
return false;
}
}
return true;
}

vector<vector<string> > solveNQueens(int n) {
// IMPORTANT: Please reset any member data you declared, as
// the same Solution instance will be reused for each test case.
int i = 0, cnt = 0;
for(i = 0; i < n; ++i) {
queens.push_back(0);
}
place_queen(0, n);

string str;
vector<string> svec;
vector<vector<string> > svvec;

for(vector<vector<int> >::iterator vv_iter = sols.begin();
vv_iter != sols.end(); ++vv_iter) {
svec.clear();
for(vector<int>::iterator v_iter = (*vv_iter).begin();
v_iter != (*vv_iter).end(); ++v_iter) {
str.clear();
for(i = 0; i < n; ++i) {
if(*v_iter == i) {
str.push_back('Q');
}
else {
str.push_back('.');
}
}
svec.push_back(str);
}
svvec.push_back(svec);
}
return svvec;
}
};

代码与之前的类似,只是由于结果要以字符串的形式存储在容器中,在solveQueens()中多了对容器的一些操作。

再来看看N-Queens II:

Follow up for N-Queens problem.

Now, instead outputting board configurations, return the total number of distinct solutions.

这题也是N皇后问题,所不同的是,它只需要得到解的个数。
实际上,解的个数就是前面的sol_cnt,但是使用前面的代码时会超时,难道代码还不够简洁???

在网上看到一个使用位运算的很简单的代码(记录一个很牛的计算N皇后问题解个数的C程序),用这个函数可以完成上面的任务,不会超时,不过还未参透其中的奥秘,之后再好好看看吧!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  algorithm leetcode