所谓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皇后问题算法!!!