N皇后问题就不再叙述了,Google一下就知道了(这里我们讨论找出一个或几个解,不讨论找出全部解的方法)
N皇后有一个解法是回溯法,这个可以解决,但是效率不是很高。(不过这个方法可以找出所有解)
结合随机方法会更快:随机初始化一部分皇后,使得她们互不冲突,然后再用回溯法,这通常快得多。不过这个方法不能找到所有解,也不能保证一次找到解——如果第一次随机化找不到解,就要再次随机化+回溯。
本文讲一个从人工智能的角度解决该问题的算法——最小冲突算法,这个算法在《人工智能——一种现代方法》(第二版)第五章中出现
简单地说,就是:
(1)初始化N个皇后的一个放置,允许有冲突
(2)考虑某一行的某个皇后,她可能与x个皇后冲突,然后看看将这个皇后移动到这一行的哪个空位能使得与其冲突的皇后个数最少,就移动到那里。(也可以考虑列,是等价的)
(3)不断执行(2),直到没有冲突为止
这个算法也不能找到所有解,有可能也要多次初始化,因为有可能从某个初始状态开始,按照最小冲突准则无论怎么调整都达不到终止条件(即没有冲突)。这里的最小冲突准则有点“贪心”的味道,而贪心算法常常达不到最优解。(不过实际实现时发现1次初始化总是能得到解——当然这个没法证明,只是实验验证)
上面的算法没有说明如何初始化,最简单的就是随便放,这里我们采用每行放一个,同时保证列号不重复的一种随机放法。上面算法也没说明每次执行(2)应该处理哪一行,我们的做法是:从第一行开始直到最后一行,逐个处理,如果这样一轮下来没有终止,就再来一轮……直到终止。
计算每行最小冲突的位置是这个算法比较核心的一步,简单的想法就是对某个位置,遍历所有的皇后,看看与其中几个有冲突;对N个可能的位置都执行这一操作,这就是O(N^2)的复杂度,这样每一轮的调整就是O(N^3),N超过1000就会非常慢了
一个改进的想法是:既然每次只调整了一个皇后的位置,产生的影响并不大,可以记录每个皇后的冲突数,然后当某个皇后移动时,更新这个值。
我们的想法类似这个,不过我们是记录每一行、每一列、每条对角线上的皇后数,有了这个皇后数,计算冲突是个常数时间操作,非常容易。更新也很简单。
#include <cstdio> #include <cstdlib> #include <ctime> #define MAX 1000 //最多可能皇后数 #define swap(a,b) {int t = a; a = b; b = t;} //row[i]表示当前摆放方式下第i行的皇后数,col[i]表示当前摆放方式下第i列的皇后数 int row[MAX]; int col[MAX]; int N; //放置N个皇后在N*N棋盘上 //从左上到右下的对角线上row-col值是相同的,但是这个值有可能是负值,最小为-(N-1), //所以可以做个偏移,统一加上N-1,这样这个值就在[0,2*N-2]范围内,将这个值作为该对角线的编号 //pdiag[i]表示当前摆放方式下编号为i的对角线上的皇后数 int pdiag[2 * MAX];//principal diagonal,主对角线,左上到右下(表示和主对角线平行的2N-1条对角线) //从右上到左下的对角线row+col的值相同,取值范围为[0, 2 * MAX - 2],作为对角线编号 //cdiag[i]表示编号为i的对角线上的皇后数 int cdiag[2 * MAX];//counter diagonal,副对角线 //R[]用来存储皇后放置位置,R[row] = col表示(row,col)处,即“第row行第col列”有个皇后 int R[MAX]; //给定二维矩阵的一个点坐标,返回其对应的左上到右下的对角线编号 int getP(int row, int col) { return row - col + N - 1; } //给定二维矩阵的一个点坐标,返回其对应的右上到左下的对角线编号 int getC(int row, int col) { return row + col; } //返回begin, begin + 1, ... , end - 1 这end - begin个数中的随机的一个 int my_rand(int begin, int end) {//左闭右开[begin, end) return rand() % (end - begin) + begin; } //原地shuffle算法,算法导论中的randomize in place算法 void randomize(int a[], int begin, int end)// 左闭右开 { for(int i = begin; i <= end - 2; i++){ int x = my_rand(i, end); swap(a[i], a[x]); } } //初始化皇后的摆放,同时初始化row,col,pdiag,cdiag数组 void init() { for(int i = 0; i < N; i++){//N queens R[i] = i; } randomize(R, 0, N);//初始化N个皇后对应的R数组为0~N-1的一个排列,即没有任意皇后同列,也没有任何皇后同行 for(int i = 0; i < N; i++){ row[i] = 1;//每行恰好一个皇后 col[i] = 0; } for(int i = 0; i < 2 * N - 1; i++){//N queens pdiag[i] = 0; cdiag[i] = 0; } for(int i = 0; i < N; i++){//N queens col[R[i]]++; pdiag[getP(i, R[i])]++; cdiag[getC(i, R[i])]++; } } bool adjust_row(int row); void print_result(); bool qualify(); int main(int argc, const char *argv[]) { srand((unsigned)time(NULL)); scanf("%d", &N); init(); if (qualify()) {//运气很好,初始化后就满足终止条件 print_result(); return 0; } bool can_terminate = false; while (!can_terminate) { for (int i = 0; i < N; i++) { if(adjust_row(i)) { can_terminate = true; break; } } } print_result(); return 0; } //用最小冲突算法调整第row行的皇后的位置(初始化时每行都有一个皇后,调整后仍然在第row行) //调整过后check一下看看是否已经没有冲突,如果没有冲突(达到终止状态),返回true bool adjust_row(int row) { int cur_col = R[row]; int optimal_col = cur_col;//最佳列号,设置为当前列,然后更新 int min_conflict = col[optimal_col] + pdiag[getP(row, optimal_col)] - 1 + cdiag[getC(row, optimal_col)] - 1;//对角线冲突数为当前对角线皇后数减一 for (int i = 0; i < N; i++) {//逐个检查第row行的每个位置 if (i == cur_col) { continue; } int conflict = col[i] + pdiag[getP(row, i)] + cdiag[getC(row, i)]; if (conflict < min_conflict) { min_conflict = conflict; optimal_col = i; } } if (optimal_col != cur_col) {//要更新col,pdiag,cdiag col[cur_col]--; pdiag[getP(row, cur_col)]--; cdiag[getC(row, cur_col)]--; col[optimal_col]++; pdiag[getP(row, optimal_col)]++; cdiag[getC(row, optimal_col)]++; R[row] = optimal_col; if (col[cur_col] == 1 && col[optimal_col] == 1 && pdiag[getP(row, optimal_col)] == 1 && cdiag[getC(row, optimal_col)] == 1) { return qualify();//qualify相对更耗时,所以只在满足上面基本条件后才检查 } } //当前点就是最佳点,一切都保持不变 return false;//如果都没变的话,肯定不满足终止条件,否则上一次就应该返回true并终止了 //return qualify(); } bool qualify() { for(int i = 0; i < N; i++){//N queens if(col[R[i]] != 1 || pdiag[getP(i, R[i])] != 1 || cdiag[getC(i, R[i])] != 1) { return false; } } return true; } void print_result() { printf("the result is like this:\n"); for(int i = 0; i < N; i++){ printf("%d,", R[i]); } printf("\n"); for (int j = 0; j < N; j++) { for (int k = 0; k < N; k++) { if (R[j] == k) printf("*"); else printf("-"); } printf("\n"); } }
测试结果:
N=500 :立即出结果,
N=5000 : 10~20轮调整,2秒
N=50000 :平均每轮调整要5秒,40次迭代
那本书上也有写,说对于百万量级的N,迭代也就50次的样子,似乎独立于问题的规模,这确实是好事情