八皇后问题之回溯法
八皇后问题
将n个皇后放置在n*n的国际象棋棋盘上,其中没有任何两个皇后处于同一行,同一列或者同一对角线上,以使得的它们不能相互攻击。
问题分析
- 最简答的思路是把问题转化为“从64个格子中选一个子集”,使得“子集中恰好有8个格子,且任意选出两个格子都不在同一行,同一列或者同意对角线上”。这恰好是子集枚举问题。然而,64个格子的子集有2^64个,太大了,则并不是一个很好的模型。
- 我们把思路转化为“从64个格子中选出8个格子”,这是一个明显的组合问题,根据排列组合,有C8 64=4.426*10^9种结果,虽然比第一种要好,但是性能依然不够好。
- 最后我们通过思考,能不能将它装换为一维问题:因为每行每列各放置一个皇后,如果用C[X]表示第X行皇后的列编号,则问题会变成全排列生成问题,而0-7的排列一共只有8!=40320个,枚举的次数不会超过这个值。
如果这样表示的话,那么判断条件应该如何表示呢?
- 我们可以先用二维数组表示一个八宫格,假设每个格子的坐标(x,y)是二维数组的下标,那么格子(x,y)的y-x的值就能够标识出租对角线,所有的主对角线的y-x的值是相等的,同理格子(x,y)的x+y值就能表示出副对角线,那么所有的问题就都解决了。
回溯法
当把问题分成了若干步骤并递归求解时,如果当前步骤没有合理的选择时,则函数将返回上一级递归调用,这种现象叫回溯。正是因为这个原因,递归枚举算法常常被称为回溯法
源码
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 int n = 8; 6 int c[8]={0}; 7 int count = 0; 8 void search(int cur); 9 10 int main(){ 11 search(0); 12 } 13 14 void print(){ 15 printf("\n-----------------\n"); 16 for(int i = 0; i < 8; i++){ 17 for(int j = 0; j < 8; j++){ 18 if(c[i] == j){ 19 printf("|*"); 20 }else{ 21 printf("| "); 22 } 23 } 24 printf("|\n-----------------\n"); 25 } 26 } 27 28 void search(int cur){ 29 if(cur == n){ 30 print(); 31 printf("\n"); 32 }else{ 33 for(int i = 0; i < n; i++){ 34 c[cur] = i; 35 int ok = 1; 36 for(int j = 0; j < cur; j++){ 37 if(c[cur] == c[j] || j - c[j] == cur - c[cur] || j + c[j] == cur + c[cur]){ 38 ok=0; 39 break; 40 } 41 } 42 if(ok){ 43 search(cur+1); 44 } 45 } 46 } 47 }
算法改进
上述算法的枚举的结点数适合很难减少了,但是程序的效率可以继续提高,利用二维数组直接判断当前尝试的皇后所在的列和两个对角线是否已有其他的皇后,注意的问题是,主对角线的表示y-x可能为负值,存取时要加上n。
1 void searchs(int cur){ 2 if(cur == n){ 3 count++; 4 }else{ 5 for(int i = 0; i < n; i++){ 6 if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n]){ 7 c[cur]=i; 8 vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 1; 9 searchs(cur + 1); 10 vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 0; 11 } 12 } 13 } 14 }
上述程序有个极其关键的地方:vis数组的使用。它表示的含义是已经放置的换后占据了哪些列,主对角线和副对角线。一般的,如果在回溯法中修改了辅助全局变量,则一般要把它们及时恢复原状,有多个地方修改,每个地方都要恢复原有的值。
有兴趣的话,可以研究一下n皇后问题,看一看有没有什么规律或者快速解法呢?