所謂N皇后問題,是一個經典的關於回溯法的問題。
問題描述:在n*n的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。
分析:對於每一個放置點而言,需要考慮四個方向上是否已經存在皇后。分別是行,列,四十五度斜線和一百三十五度斜線。
其中,對於行:每一行只能放一個皇后,直到我們把最后一個皇后放到最后一行的合適位置。對於列:列相同的約束條件,只需判斷該放置點與已放置好的皇后的j是否相等即可。
對於四十五度斜線和一百三十五度斜線:當前棋子和已放置好的棋子不能存在行數差的絕對值等於列數差的絕對值的情況,若存在則說明兩個棋子在同一條斜線上。
首先由比較簡單的四皇后開始分析。
對於解空間比較小的情況,最簡單的當然就是使用暴力搜索法。
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 循環的時候,其實循環結構非常相似,而且每一層循環都要在尚未結束的時候,向下一層繼續搜索,這樣就可以考慮采用遞歸的方式了。
乍一看 while 循環好像也不錯,但其實是行不通的,因為在循環里面我們只能將當前行的可行位置搜索完后才能搜索下一行,而這不是我們想要的方式,也搜索不到相應的解。
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 數組是對每一個格點的狀態都進行了記錄,為了減少操作量,我們可以只記錄放置皇后的地方,然后根據之前的皇后的位置去判斷與當前格點是否會產生沖突。
現在其實已經可以升級到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 = pos - (pos & (-pos)) 將 pos 二進制形式中最后一位 1 置位 0,在這里的意思是更新當前可以放置皇后的列,因為剛剛我們放置了一個皇后。
遞歸查找下一個皇后放置的位置。
帶結果可視化的版本如下
class Solution { 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; } // 計算一個整數的二進制形式中有多少個比特位為1 int count1Bit(int n) { int countOneBit = 0; while (n) { ++countOneBit; n = n & (n-1); } return countOneBit; } void nQueenRecursively(int k, int ld, int rd) { if (k == max) { ++count; solutions.push_back(s); return; } int pos = max & ~(k | ld | rd); int index = count1Bit(k); while (pos) { int p = pos & (~pos+1); pos -= p; // 根據p是2的多少次冪判斷當前放置的位置 s[index] = (p==1 ? 0 : 1+(int)log2(p>>1)); nQueenRecursively(k | p, (ld | p) << 1, (rd | p) >> 1); } } void nQueen(int n) { max = (max << n) - 1; s.resize(n, -1); nQueenRecursively(0, 0, 0); visualize(solutions); } public: int count = 0; int max = 1; std::vector<int> s; std::vector<std::vector<int>> solutions; }; int main (int argc, char *argv[]) { int n = 8; // n is the number of queen Solution solver; solver.nQueen(n); return 0; }下面以 n=4 時的兩種情況加以說明
第一種情況
Step 1 main 函數中調用時 k = 0, ld = 0, rd = 0
Step 2 進入函數體首先 pos = 1111 & ~(0000 | 0000 | 0000) = 1111 ,然后開始 while 循環
Step 3 首先 p = pos & (~pos+1) = 0001 ,表示要將第一個皇后棋子放在第一行第一列的位置, 0001 可以理解為從右到左依次為 0,0,0,1,(由於N皇后問題左右是對稱的,理解成從左到右和從右到左都可以,只是我習慣從左向右遍歷)
Step 4 更新 pos = pos - p = 1110 ,表示最左邊一列已經放置了皇后,其他皇后再放這一列會受到攻擊,如下圖A中第一幅圖第一行所示,o 表示放置皇后,x 表示被攻擊的位置
Step 5 接下來進入遞歸, 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第一幅圖第二行所示
Step 6 更新 pos = pos - p = 1000 ,表示在當前行第四列還可以放置皇后
Step 7 進入下一輪遞歸, k = 0101, ld = 1100, rd = 0010 ,在第三行尋找不沖突的位置,pos = 1111 & ~(0101 | 1100 | 0010) = 0000 表示該行已經沒有可以放皇后的位置了, k=0101 表示第一列和第三列都有棋子攻擊,
ld=1100 表示第三列和第四列在斜率為 -1 的方向上會受到攻擊,由下圖A第二幅圖第三行所示,第三列和第四列剛好是前兩個皇后的斜線攻擊位置,
rd=0010 表示第二列在斜率為 1 的方向上會受到攻擊,由圖可知其在第二個皇后的斜線攻擊位置,至此此種放法不可行
Step 8 返回到調用處即進行第二列第四行的可行性檢驗……
第二種情況
Step 1 當第一行第一列檢查過之后, pos = 1110 ,此時取最后一位 1 , p = pos & (~pos+1) = 0010 ,表示放在第一行第二列的位置,如下圖B第一幅圖第一行所示
Step 2 更新 pos = pos - p = 1100 ,接下來進入遞歸
k = 0000 | 0010 = 0010 ld = (0000 | 0010) << 1 = 0100 rd = (0000 | 0010) >> 1 = 0001Step 3 開始第二行的遍歷, pos = 1111 & ~(0010 | 0100 | 0001) = 1000 ,此時第二列會受到縱向攻擊,第三列會受到一百三十五度方向的斜線攻擊,第一列會收到四十五度方向的斜線攻擊
Step 4 進入 while 循環, p = 1000,pos = 0000 ,表示將棋子放在第四列的位置,當前行已沒有其他可以放置棋子的位置,如下圖B第二幅圖第二列所示,下一輪遞歸,
k = 0010 | 1000 = 1010 ld = (0100 | 1000) << 1 = 11000 rd = (0001 | 1000) >> 1 = 0100Step 5 開始第三行的遍歷, pos = 1111 & ~(1010 | 11000 | 0100) = 0001 ,此時該行第二列和第四列都會受到其它已放置的皇后的縱向攻擊,第四列還會受到一百三十五度方向的斜線攻擊,第三列會受到四十五度方向的斜線攻擊
Step 6 可放置皇后的位置只剩第一列,p = 0001 表示放置在第一列,更新 pos = 0000 ,表示該行也已經沒有其他可以放置棋子的位置,如下圖B第三幅圖第三行所示,然后進入再一輪遞歸
k = 1010 | 0001 = 1011 ld = (11000 | 0001) << 1 = 110010 rd = (0100 | 0001) >> 1 = 0010Step 7 開始第四行的遍歷, pos = 1111 & ~(1011 | 0010 | 0010) = 0100 ,此時第一列第二列第四列會受到其它已放置的皇后的縱向攻擊,第二列會受到一百三十五度方向斜線攻擊,第二列會受到四十五度方向斜線攻擊
Step 8 可以放置皇后的位置只剩第三列, p = 0100,pos = 0000 ,再進入遞歸時 k = 1011 | 0100 = 1111 = max ,完成了一次完整的搜索,表示此時找到了一種解法,然后再一次進行后面的搜索。
參考資料
[2] n皇后問題-回溯法求解
[3] 利用搜索樹來解決N皇后問題
[4] n皇后問題(分支限界法)
[6] 目前最快的N皇后問題算法!!!