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次的樣子,似乎獨立於問題的規模,這確實是好事情