题目
题目:
在一个国际象棋棋盘上放八个皇后,使得任何两个皇后之间不相互攻击,求出所有 的布棋方法。上机运行并检验结果。
思考:将此题推广到 N 皇后的情况,检验在 N 比较大的情况下,比方说 N=16 的时候,你的程序能否快速的求出结果,如果不能,思考有什么方法能够优化算法。
准备工作
我用queen数组表示皇后放置的位置,attack数组来表示皇后的攻击范围,处在皇后攻击范围内的格子标上1,被标记格子处不再可以放置其他皇后。Solve数组用来储存每一种结果。
同时为了方便表示皇后可以移动的八个方向,我利用到了两个方向数组。
static const int dx[] = { -1,1,0,0,-1,-1,1,1 };
static const int dy[] = { 0,0,-1,1,-1,1,-1,1 };
解题思路
这题我采取的是回溯法来解决八皇后问题。
从第一行开始放置皇后,并标记queen数组,以及皇后攻击范围内的格子。
然后依次往下放置皇后,并做标记,若遇到某一行没有合适的位置再放置皇后了说明这个放置方案是不合理的,我们需要回溯到上一步(恢复queen数组和attack数组)执行其他的放置方案。
直到k==n(k表示当前所在行)递归结束,说明我们找到了一组解。
等全部解都找完后,返回solve。
实验结果
思考
随着皇后数的增加我发现程序会执行的越来越慢,到了16皇后的时候就已经运行不出结果了,分析可以发现我这个算法的时间复杂度很高,中间做了很多次重复的操作,所以这个算法是有待优化的。
我在网上查阅相关资料发现,位运算法处理n皇后问题,是当前一种较快的处理方法,除此之外还有回溯+剪枝、对称剪枝等等。
未优化源代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//自定义函数,实现放置皇后之后,对attack数组更新
void put_queen(int x, int y, vector<vector<int>>& attack) {
//方向数组,方便之后对8个方向进行标记
static const int dx[] = { -1,1,0,0,-1,-1,1,1 };
static const int dy[] = { 0,0,-1,1,-1,1,-1,1 };
attack[x][y] = 1;//放置皇后的位置标记为1
//循环标记攻击位置
for (int i = 1; i < attack.size(); i++) {//从皇后位置向1到n-1个距离延伸
for (int j = 0; j < 8; j++) {//遍历8个方向
int nx = x + i * dx[j];
int ny = y + i * dy[j];
if (nx >= 0 && nx < attack.size() && ny >= 0 && ny < attack.size()) {
attack[nx][ny] = 1;//标记为1
}
}
}
}
//回溯发求解N皇后的递归函数
//k表示当前处理的行
//n表示N皇后问题
//queen存储皇后的位置
//attack标记皇后的攻击位置
//solve存储
void backtrack(int k, int n, vector<string>& queen, vector<vector<int>>& attack, vector<vector<string>>& solve) {
if (k == n) {//找到一组解
solve.push_back(queen);
return;
}
//遍历0至n-1列,在循环中,回溯试探皇后放置位置
for (int i = 0; i < n; i++) {
if (attack[k][i] == 0) {//判断当前第k行第i列是否可以放置皇后
vector<vector<int>>tmp = attack;//备份attack数组
queen[k][i] = 'Q';//标记该位置放置皇后
put_queen(k, i, attack);//更新attack数组
backtrack(k + 1, n, queen, attack, solve);//递归试探k+1行皇后位置
attack = tmp;//恢复attack数组
queen[k][i] = '.';//恢复queen数组
}
}
}
//内层vector保存了一种解法
//string中Q为皇后的位置
//外层vector保存了所有解法
vector<vector<string>>solveNQueens(int n) {
vector<vector<string>>solve;//存储最后结果
vector<vector<int>>attack;//标记皇后的攻击范围
vector<string>queen;//保存皇后的位置
//使用循环初始化attack和queen数组
for (int i = 0; i < n; i++) {
attack.push_back((std::vector<int>()));
for (int j = 0; j < n; j++) {
attack[i].push_back(0);
}
queen.push_back("");
queen[i].append(n, '.');//给字符串赋值
}
backtrack(0, n, queen, attack, solve);
return solve;
}
void test1() {
vector<vector<string>>result;
int i;
cin >> i;
result = solveNQueens(i);
cout << i << "皇后共有" << result.size() << "种解法:" << endl;
int count = 1;
for (vector<vector<string>>::iterator it = result.begin(); it != result.end(); it++) {
cout << "解法" << count++ << ":" << endl;
for (vector<string>::iterator sit = it->begin(); sit != it->end(); sit++) {
cout << *sit << endl;
}
cout << endl;
}
}
//时间复杂度O(n^n)
int main() {
test1();
return 0;
}
二进制优化代码
说明:这段代码是我一个打acm同学写的,我是菜菜我写不好
#include <iostream>
#include <cstdio>
#include <ctime>
using namespace std;
int N;
int upper_limit; // 111...1111 n bits
int ans = 0;
void test(int row, int ld, int rd) {
// 如果row的皇后还没有放满
if (row != upper_limit) {
int pos = upper_limit & ~(row | ld | rd);//(row|ld|rd)表示不能放的位置,取反后表示能放的位置,和upper_limit取&操作,就提取出所有可以放置皇后的位置
// while 循环枚举所有为1的位置,然后去放置皇后
while (pos != 0) {
// 也可以写成x & (x^(x-1))
int p = pos & (-pos); //lowbit求最后一个1在哪
pos = pos ^ p; // 将p二进制为1的位置在pos中置为0
// (ld | p) << 1 更新ld的下一层不能放的位置
// (rd | p) >> 1 更新rd的下一层不能放的位置
test(row | p, (ld | p) << 1, (rd | p) >> 1);// row | p 把p二进制为1的位置放上皇后
}
}
else ans++;
}
int main() {
ans = 0;
cin >> N;
upper_limit = (1 << N) - 1;
clock_t start, finish;
double duration;
start = clock();
test(0, 0, 0);
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("Time is %lf\n", duration);
printf("共有多少种情况: %d\n", ans);
return 0;
}