所謂N皇后問題,是一個經典的關於回溯法的問題。node
問題描述:在n*n的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后能夠攻擊與之處在同一行或同一列或同一斜線上的棋子。算法
分析:對於每個放置點而言,須要考慮四個方向上是否已經存在皇后。分別是行,列,四十五度斜線和一百三十五度斜線。數組
其中,對於行:每一行只能放一個皇后,直到咱們把最後一個皇后放到最後一行的合適位置。對於列:列相同的約束條件,只需判斷該放置點與已放置好的皇后的j是否相等便可。ide
對於四十五度斜線和一百三十五度斜線:當前棋子和已放置好的棋子不能存在行數差的絕對值等於列數差的絕對值的狀況,若存在則說明兩個棋子在同一條斜線上。函數
首先由比較簡單的四皇后開始分析。spa
對於解空間比較小的狀況,最簡單的固然就是使用暴力搜索法。.net
BruteForceSearchclass nQueen { public: void update(std::vector<std::vector<int>> &is_occupied, int row, int col) { int n = is_occupied.size(); for (int r = row+1; r < n; ++r) { if (0 == is_occupied[r][col]) is_occupied[r][col] = 1; if (col+r-row < n && 0 == is_occupied[r][col+r-row]) is_occupied[r][col+r-row] = 1; if (col+row >= r && 0 == is_occupied[r][col+row-r]) is_occupied[r][col+row-r] = 1; } } void visualize(std::vector<std::vector<int>> &solutions) { int n = solutions.size(); for (int r = 0; r < n; ++r) { int l = solutions[r].size(); for (int c = 0; c < l; ++c) std::cout << solutions[r][c] << " "; std::cout<< std::endl; for (int c = 0; c < l; ++c) { int pos = solutions[r][c]; for (int i = 0; i < l; ++i) { if (i == pos) std::cout << "Q "; else std::cout << "* "; } std::cout << std::endl; } std::cout << std::endl; } std::cout << "Total solutions: " << solutions.size() << std::endl; } // 暴力窮舉式搜索法 void fourQueen() { int n = 4; for (int i = 0; i < n; ++i) { std::vector<int> ret; ret.push_back(i); std::vector<std::vector<int>> is_occupied(n, std::vector<int>(n, 0)); for (int c = 0; c < n; ++c) if (c != i) is_occupied[0][c] = 1; update(is_occupied, 0, i); for (int j = 0; j < n; ++j) { if (0 == is_occupied[1][j]) { ret.push_back(j); update(is_occupied, 1, j); for (int h = 0; h < n; ++h) { if (0 == is_occupied[2][h]) { ret.push_back(h); update(is_occupied, 2, h); for (int k = 0; k < n; ++k) { if (0 == is_occupied[3][k]) { ret.push_back(k); solutions.push_back(ret); break; } } } } } } } visualize(solutions); } public: std::vector<std::vector<int>> is_occupied; std::vector<std::vector<int>> solutions; };對上面的方法稍加觀察便可發現當咱們在一步一步進行 for 循環的時候,其實循環結構很是類似,並且每一層循環都要在還沒有結束的時候,向下一層繼續搜索,這樣就能夠考慮採用遞歸的方式了。3d
乍一看 while 循環好像也不錯,但實際上是行不通的,由於在循環裏面咱們只能將當前行的可行位置搜索完後才能搜索下一行,而這不是咱們想要的方式,也搜索不到相應的解。code
DFS+backtrack For fourQueenclass nQueen { public: void update(std::vector<std::vector<int>> &is_occupied, int row, int col) { int n = is_occupied.size(); for (int r = row+1; r < n; ++r) { if (0 == is_occupied[r][col]) is_occupied[r][col] = 1; if (col+r-row < n && 0 == is_occupied[r][col+r-row]) is_occupied[r][col+r-row] = 1; if (col+row >= r && 0 == is_occupied[r][col+row-r]) is_occupied[r][col+row-r] = 1; } } // 須要新加恢復佔位標誌的函數 void update_reset(std::vector<std::vector<int>> &is_occupied, int row, int col) { int n = is_occupied.size(); for (int r = row+1; r < n; ++r) { if (1 == is_occupied[r][col]) is_occupied[r][col] = 0; if (col+r-row < n && 1 == is_occupied[r][col+r-row]) is_occupied[r][col+r-row] = 0; if (col+row >= r && 1 == is_occupied[r][col+row-r]) is_occupied[r][col+row-r] = 0; } } void visualize(std::vector<std::vector<int>> &solutions) { int n = solutions.size(); for (int r = 0; r < n; ++r) { int l = solutions[r].size(); for (int c = 0; c < l; ++c) std::cout << solutions[r][c] << " "; std::cout<< std::endl; for (int c = 0; c < l; ++c) { int pos = solutions[r][c]; for (int i = 0; i < l; ++i) { if (i == pos) std::cout << "Q "; else std::cout << "* "; } std::cout << std::endl; } std::cout << std::endl; } std::cout << "Total solutions: " << solutions.size() << std::endl; } void solution(int row, std::vector<int> &ret, int n) { if (row >= n) { solutions.push_back(ret); } else { for (int c = 0; c < n; ++c) { if (0 == row) { for (int j = 0; j < n; ++j) { is_occupied[0][j] = (j != c ? 1 : 0); } } if (0 == is_occupied[row][c]) { ret.push_back(c); update(is_occupied, row, c); solution(row+1, ret, n); // 由於要回溯,剛剛向下搜索時改變的標誌位都要恢復成搜索以前的狀態,才能保證按行向右依次進行驗證 // 同時做爲該行結果的ret的最後一位也要彈出,爲下一個解作準備 ret.pop_back(); update_reset(is_occupied, row, c); } } } } void fourQueen() { int n = 4; is_occupied.resize(n); for (int i = 0; i < n; ++i) is_occupied[i].resize(n, 0); std::vector<int> ret; solution(0, ret, n); visualize(solutions); } public: std::vector<std::vector<int>> is_occupied; std::vector<std::vector<int>> solutions; };上面的 is_occupied 數組是對每個格點的狀態都進行了記錄,爲了減小操做量,咱們能夠只記錄放置皇后的地方,而後根據以前的皇后的位置去判斷與當前格點是否會產生衝突。blog
如今其實已經能夠升級到N皇后了,下面看一下遞歸加回溯解法一
Recursive+Backtrack for NQueenclass nQueen { public: void visualize(std::vector<std::vector<int>> &solutions) { int n = solutions.size(); for (int r = 0; r < n; ++r) { int l = solutions[r].size(); for (int c = 0; c < l; ++c) std::cout << solutions[r][c] << " "; std::cout<< std::endl; for (int c = 0; c < l; ++c) { int pos = solutions[r][c]; for (int i = 0; i < l; ++i) { if (i == pos) std::cout << "Q "; else std::cout << "* "; } std::cout << std::endl; } std::cout << std::endl; } std::cout << "Total solutions: " << solutions.size() << std::endl; } // 遞歸加回溯的解法一 bool check(int row, int col, int n) { for (int i = 0; i < row; ++i) { if (1 == is_occupied[i][col]) { return false; } for (int j = 0; j < n; ++j) { if (std::abs(i-row) == std::abs(j-col) && 1 == is_occupied[i][j]) return false; } } return true; } void nQueneRecursively(int row, int n, std::vector<int> &result) { if (row >= n) { solutions.push_back(result); } for (int c = 0; c < n; ++c) { if (check(row, c, n)) { is_occupied[row][c] = 1; result.push_back(c); nQueneRecursively(row+1, n, result); result.pop_back(); is_occupied[row][c] = 0; } } } void nQuene() { int n = 6; is_occupied.resize(n); for (size_t r = 0; r < n; ++r) { is_occupied[r].resize(n, 0); } std::vector<int> result; nQueneRecursively(0, n, result); visualize(solutions); } public: std::vector<std::vector<int>> is_occupied; std::vector<std::vector<int>> solutions; };既然咱們對格點狀態標誌處理以後還要恢復其原來的標誌,以便繼續搜索可能的解,同時此種約束徹底能夠根據記錄的前幾個皇后的位置計算而獲得,因而就有了更簡單的作法
Recursive+Backtrack v2 for NQueenclass nQueen { public: void visualize(std::vector<std::vector<int>> &solutions) { int n = solutions.size(); for (int r = 0; r < n; ++r) { int l = solutions[r].size(); for (int c = 0; c < l; ++c) std::cout << solutions[r][c] << " "; std::cout<< std::endl; for (int c = 0; c < l; ++c) { int pos = solutions[r][c]; for (int i = 0; i < l; ++i) { if (i == pos) std::cout << "Q "; else std::cout << "* "; } std::cout << std::endl; } std::cout << std::endl; } std::cout << "Total solutions: " << solutions.size() << std::endl; } // 遞歸加回溯解法二 // 說明:check2 先在當前行某個格子放置好皇后,再檢驗此時是否無衝突 // nQueenRecursively2 遞歸求解各類擺法,s表示其中一種解法,n是一個固定值, // 題目的要求皇后數 bool check2(std::vector<int> &s, int row) { for (int i = 0; i < row; ++i) { if (std::abs(s[i]-s[row]) == row-i || s[i] == s[row]) return false; } return true; } void nQueenRecursively2(int row, std::vector<int> &s, int n) { if (row >= n) { solutions.push_back(s); } else { for (int i = 0; i < n; ++i) { s[row] = i; if (1 == check2(s, row)) { nQueenRecursively2(row+1, s, n); } } } } void nQuene2() { int n = 6; std::vector<int> s(n); nQueenRecursively2(0, s, n); visualize(solutions); } public: std::vector<std::vector<int>> solutions; };對於此類問題,好像利用廣度優先搜索也能夠完成,下面是其中的一種解法,
分支限界法class Solution { public: struct Node { int level; std::vector<int> path; Node(int n): level(n) {} }; void visualize(std::vector<std::vector<int>> &solutions) { int n = solutions.size(); for (int r = 0; r < n; ++r) { int l = solutions[r].size(); for (int c = 0; c < l; ++c) std::cout << solutions[r][c] << " "; std::cout<< std::endl; for (int c = 0; c < l; ++c) { int pos = solutions[r][c]; for (int i = 0; i < l; ++i) { if (i == pos) std::cout << "Q "; else std::cout << "* "; } std::cout << std::endl; } std::cout << std::endl; } std::cout << "Total solutions: " << solutions.size() << std::endl; } // 分支限界法,其實就是BFS的方法 bool check3(Node q, int row) { for (int j = 0; j < row; ++j) { if (std::abs(row-j)==std::abs(q.path[j]-q.path[row]) || q.path[j]==q.path[row]) return false; } return true; } void nQueen(int n) { Node flag(-1); std::queue<Node> q; q.push(flag); std::vector<int> solve(n, 0); int row = 0; Node currentNode(0); while (!q.empty()) { if (row < n) { for (int k = 0; k < n; ++k) { Node nodetmp(row); for (int i = 0; i < row; ++i) nodetmp.path.push_back(currentNode.path[i]); nodetmp.path.push_back(k); if (check3(nodetmp, row)) q.push(nodetmp); } } currentNode = q.front(); q.pop(); if (-1 == currentNode.level) { ++row; q.push(flag); currentNode = q.front(); q.pop(); } if (n-1 == currentNode.level) { for (int i = 0; i < n; ++i) { solve[i] = currentNode.path[i]; } solutions.push_back(solve); if (row == n-1) ++row; } } visualize(solutions); } public: std::vector<std::vector<int>> solutions; };除此以外,還有一種利用位運算求解的算法
Bit Operation for nQueen Problemclass Solution { public: void nQueen(int k, int ld, int rd) { if (k == max) { ++count; return; } int pos = max & ~(k | ld | rd); while (pos) { int p = pos & (~pos+1); pos -= p; nQueen(k | p, (ld | p) << 1, (rd | p) >> 1); } } public: int count = 0; int max = 1; }; int main (int argc, char *argv[]) { int n = 8; // n is the number of queen Solution solver; solver.max = (solver.max << n) -1; solver.nQueen(0, 0, 0); std::cout << "total solutions: " << solver.cout << std::endl; return 0; }分析該算法時都是基於其二進制形式,其中,
k 記錄當前已經放有皇后的列, 1 表示該列已經放有皇后了, 0 表示還沒有放有皇后。
ld 記錄斜率爲 -1 的方向上是否有皇后, 1 表示有, 0 表示沒有。
rd 記錄斜率爲 1 的方向上是否有皇后, 1 表示有, 0 表示沒有。
pos 記錄當前能夠放置皇后的列, 1 表示能夠放置, 0 表示不能放置。
根據位運算推導, ~pos+1 = -pos ,而後 pos & (-pos) 的意思是取 pos 中二進制形式中最後一位 1 ,在這裏的意思就是要在當前行的該列放置一個皇后。
下一步 pos -= p 實際上就是 pos = pos - (pos & (-pos)) 將 pos 二進制形式中最後一位 1 置位 0 ,在這裏的意思是更新當前能夠放置皇后的列,由於剛剛咱們放置了一個皇后。
遞歸查找下一個皇后放置的位置。
下面以 n=4 時的兩種狀況加以說明
第一種狀況
main 函數中調用時 k = 0, ld = 0, rd = 0,
進入函數體首先 pos = 1111 & ~(0000 | 0000 | 0000) = 1111 ,而後開始 while 循環,
首先 p = pos & (~pos+1) = 0001 ,表示要將第一個皇后棋子放在第一行第一列的位置, 0001 能夠理解爲從右到左依次爲 0,0,0,1,(因爲N皇后問題左和右是對稱的,理解成從左到右和從右到左均可以,只是我習慣從左向右的遍歷方式),
更新 pos = pos - p = 1110 ,表示最左邊一列已經放置了皇后,其餘皇后再放這一列會受到攻擊,以下圖A中第一幅圖第一行所示,o表示放置皇后,x表示被攻擊的位置,
接下來進入遞歸, k = 0001, ld = 0010, rd = 0000 ,此時表示在第二行尋找能夠放置皇后的位置, pos = 1111 & ~(0001 | 0010 | 0000) = 1100 , k=0001 表示最左邊一列不能放, ld=0010 表示從左邊起第二列在斜率爲 -1 的方向上有皇后,也就是咱們剛纔在第一行第一列放置的皇后棋子,此時的 pos 表示從左邊起第三列和第四列能夠放置皇后, p = pos & (~pos+1) = 0100 取最後一位 1 ,表示將棋子放在第三列的位置,以下圖A第一幅圖第二行所示
更新 pos = pos - p = 1000 ,表示在當前行第四列還能夠放置皇后
而後進入下一輪遞歸, k = 0101, ld = 1100, rd = 0010 ,在第三行尋找不衝突的位置, pos = 1111 & ~(0101 | 1100 | 0010) = 0000 ,表示該行已經沒有能夠放皇后的位置了, k=0101 表示第一列和第三列都有棋子攻擊, ld=1100 表示第三列和第四列在斜率爲 -1 的方向上會受到攻擊,由下圖A第二幅圖第三行所示,第三列和第四列恰好是前兩個皇后的斜線攻擊位置, rd=0010 表示第二列在斜率爲 1 的方向上會受到攻擊,由圖可知其在第二個皇后的斜線攻擊位置,至此此种放法不可行,返回到調用處即進行第二列第四行的可行性檢驗……
第二種狀況
當第一行第一列檢查過以後, pos = 1110 ,此時取最後一位 1 , p = pos & (~pos+1) = 0010 ,表示放在第一行第二列的位置,以下圖B第一幅圖第一行所示,更新 pos = pos - p = 1100 ,
接下來進入遞歸,
k = 0000 | 0010 = 0010,ld = (0000 | 0010) << 1 = 0100,rd = (0000 | 0010) >> 1 = 0001開始第二行的遍歷, pos = 1111 & ~(0010 | 0100 | 0001) = 1000 ,此時第二列會受到縱向攻擊,第三列會受到一百三十五度方向的斜線攻擊,第一列會收到四十五度方向的斜線攻擊,進入 while 循環, p = 1000,pos = 0000 ,表示將棋子放在第四列的位置,當前行已沒有其餘能夠放置棋子的位置,以下圖B第二幅圖第二列所示
下一輪遞歸,
k = 0010 | 1000 = 1010, ld = (0100 | 1000) << 1 = 11000, rd = (0001 | 1000) >> 1 = 0100開始第三行的遍歷, pos = 1111 & ~(1010 | 11000 | 0100) = 0001 ,此時該行第二列和第四列都會受到其它已放置的皇后的縱向攻擊,第四列還會受到一百三十五度方向的斜線攻擊,第三列會受到四十五度方向的斜線攻擊,能夠放置皇后的位置只剩第一列, p = 0001 表示放置在第一列,更新 pos = 0000 ,表示該行也已經沒有其餘能夠放置棋子的位置,以下圖B第三幅圖第三行所示
而後進入再一輪遞歸,
k = 1010 | 0001 = 1011, ld = (11000 | 0001) << 1 = 110010, rd = (0100 | 0001) >> 1 = 0010
開始第四行的遍歷, pos = 1111 & ~(1011 | 0010 | 0010) = 0100 ,此時第一列第二列第四列會受到其它已放置的皇后的縱向攻擊,第二列會受到一百三十五度方向斜線攻擊,第二列會受到四十五度方向斜線攻擊,能夠放置皇后的位置只剩第三列, p = 0100,pos = 0000 ,再進入遞歸時 k = 1011 | 0100 = 1111 = max ,完成了一次完整的搜索,表示此時找到了一種解法,而後再一次進行後面的搜索。
參考資料
[2] n皇后問題-回溯法求解
[3] 利用搜索樹來解決N皇后問題
[4] n皇后問題(分支限界法)
[6] 目前最快的N皇后問題算法!!!