多维算法思考(二):关于八皇后问题解法的探讨
八皇后问题是隶属于递归算法中的经典例题,正确的理解它是学习递归算法的关键所在。下面我将用三种方法来为大家讲解。
方法一:
1 #include<stdio.h> 2 3 #define N 8 4 int column[N+1];// 同栏是否有皇后,1表示有 5 int rup[2*N+1];// 右上至左下是否有皇后 6 int lup[2*N+1];// 左上至右下是否有皇后 7 int queen[N+1]={0}; 8 int num;// 解答编号 9 void backtrack(int);// 递回求解 10 11 void show(int a[],int); 12 13 int main(void) 14 { 15 int i,m,n; 16 num=0; 17 for(i=1;i<=N;i++) 18 column[i]=1; 19 for(i=1;i<=2*N;i++) 20 rup[i]=lup[i]=1; 21 backtrack(1); 22 return 0; 23 } 24 25 void showAnswer() 26 { 27 int x,y; 28 printf(" 解答%d ",++num); 29 for(y=1;y<=N;y++) 30 { 31 for(x=1;x<=N;x++) 32 { 33 if(queen[y]==x) 34 printf("Q "); 35 else 36 printf("* "); 37 } 38 printf(" "); 39 } 40 } 41 42 void backtrack(int i) 43 { 44 int j; 45 if(i>N) 46 { 47 showAnswer(); 48 } 49 else 50 { 51 for(j=1;j<=N;j++) 52 { 53 if(column[j]==1&&rup[i+j]==1&&lup[i-j+N]==1) 54 { 55 queen[i]=j; 56 // 设定为占用 57 column[j]=rup[i+j]=lup[i-j+N]=0; 58 backtrack(i+1); 59 column[j]=rup[i+j]=lup[i-j+N]=1; 60 } 61 } 62 } 63 }
这种方法整体书写起来较为简单,但理解起来稍有点难度,需对递归流程有特别深刻的了解。为帮助大家学习和理解,我将其代码块核心块单独拿出来整理成例题,例题一,是为了帮助大家了解递归执行的次数。例题二,是为了帮助大家了解递归执行的顺序。
例题一:为了对比起见,我特意用循环也写了一个功能类似的代码,两者结果相同。
#include<stdio.h> #include<stdlib.h> void digui(int,int); void xunhuan(int); int a,b; //递归 int p,q; //循环 int main(void) { int i; printf("递归求法: "); for(i=1;i<=8;i++) { a=0,b=0; digui(1,i); printf("a=%d b=%d ",a,b); } printf(" 循环求法: "); for(i=1;i<=8;i++) { p=1,q=0; xunhuan(i); printf("p=%d q=%d ",p,q); } } void digui(int i,int m) { int j; if(i>m) a++; else { for(j=1;j<=m;j++) { b++; digui(i+1,m); } } } void xuhuan(int m) { int i; for(i=1;i<=m;i++) { p*=n; q+=p; } }
测试结果:
显然,如果不加任何条件,递归执行的次数是相当庞大的,其中a达到 b达到
当i=8时,a突破了千万级别,按数学思考,在不加任何条件的情况下,每一行的8个位置均有可能有一个皇后,刚好为8的8次方。
例题二:(在不考虑左上,左下,右上,右下的情况下,当N=3时,执行的代码如下)
#include<stdio.h> #define N 3 int column[N+1];// 同栏是否有皇后,1表示有 int queen[N+1]={0}; int num;// 解答编号 void backtrack(int);// 递回求解 void show(int a[],int); int main(void) { int i,m,n; num=0; for(i=1;i<=N;i++) column[i]=1; backtrack(1); return 0; } void showAnswer() { int x,y; printf(" 解答%d ",++num); for(y=1;y<=N;y++) { for(x=1;x<=N;x++) { if(queen[y]==x) printf("Q "); else printf("* "); } printf(" "); } } void backtrack(int i) { /* 可通过printf(" %3d ",i)查看执行的顺序 */ int j; if(i>N) showAnswer(); else for(j=1;j<=N;j++) if(column[j]==1) { queen[i]=j; // 设定为占用 column[j]=0; backtrack(i+1); column[j]=1; } }
执行的顺序图如下:(以下backtrack简写为b)
为了更直观的显示我们将showAnswer()稍加改动一下如下:
void showAnswer() { int x,y; printf(" 解答%d ",++num); for(y=1;y<=N;y++) { for(x=1;x<=N;x++) { if(queen[y]==x) printf("%3d",queen[y]); } } printf(" "); }
输出结果如下图:
即为1,2,3的全排列,共有种解法。
方法二:
#include <stdio.h> //检查该点有效返回1,否则返回0 int CheckPosition (int (*chess)[8], int row, int col) { int IsTrue = 1; int i, j; //左上方,行数减,列数减 for (i = row, j = col; IsTrue && i >= 0 && j >= 0; i--, j--) { if (chess[i][j] == 1) IsTrue = 0; } //上方,列数不变 for (i = row, j = col; IsTrue && i >= 0; i--) { if (chess[i][j] == 1) IsTrue = 0; } //右上方,行数减,列数加 for (i = row, j = col; IsTrue && i >= 0 && j < 8; i--, j++) { if (chess[i][j] == 1) IsTrue = 0; } return IsTrue; } void DisChess (int (*chess)[8]) { int i, j; static n = 0; printf("第%d种解法 ", ++n); for (i = 0; i < 8; i++) { for (j = 0; j < 8; j++) { if(chess[i][j]==1) printf("G "); else printf("* "); } printf(" "); } puts(" "); } void EightQueen (int (*chess)[8], int row) { int col; if (row < 8) { for (col = 0; col < 8; col++) { if ((chess[row][col]=CheckPosition(chess, row, col))==1) { EightQueen(chess, row + 1); chess[row][col] = 0; } } } else DisChess(chess); //显示 } int main() { int chess[8][8] = {0}; EightQueen(chess, 0); return 0; }
相比于方法一,方法二更容易理解也更加简单,我们只需把约束条件放在CheckPosition(chess, row, col))函数中,通过循环来实现。但与此同时代码段会相应增加一些。方法一和方法二的主要区别在于数组的选择上。方法一,用的是一维数组,把合乎要求的存放于queen[]数组中,然后输出。方法二,用的是二维数组,首先把数组所有元素均置为0,然后通过约束条件把合乎要求的置为1,然后显示输出。
为了继承方法二的简单和容易理解等优点,同时又缩短代码的长度,方法三对其进行了部分优化代码如下:
方法三:
1 #include<stdio.h> 2 #include<math.h> 3 4 int QueenCount = 8;//皇后数目。 5 int QueenPositions[8];//每行放置皇后的位置。 6 int total = 0;//放置方案的数目。 7 void DisChess(int row); 8 void Print(); 9 10 void main() 11 { 12 DisChess(0); 13 } 14 15 void DisChess(int row)//在第row行上放置一个皇后。 16 { 17 if (row >= QueenCount) 18 { 19 total++;//此时找到了一种放置方案。 20 Print(); 21 } 22 for (int col = 0; col < QueenCount; col++)//在第row行上尝试每一个位置。 23 { 24 bool attack = false; 25 for (int iRow = 0; iRow < row; iRow++) 26 { 27 if (QueenPositions[iRow] == col ||abs(iRow - row) == abs(QueenPositions[iRow] - col)) 28 { 29 attack = true; 30 break; 31 } 32 } 33 if (!attack) 34 { 35 QueenPositions[row] = col; 36 DisChess(row + 1); 37 } 38 } 39 } 40 41 void Print() 42 { 43 int i,j; 44 printf("解法%d为: ",total); 45 for(i=0;i<8;i++) 46 { 47 for(j=0;j<8;j++) 48 { 49 if(QueenPositions[i]==j) 50 printf("Q "); 51 else 52 printf("* "); 53 } 54 printf(" "); 55 } 56 printf(" "); 57 }
介绍了八皇后的所有解法后,那么如何去查看某一种解法呢?这里我们以方法一为例。
首先我们用数组a[1000]保存每一种解法皇后的位置。我们知道方法一中i和j均由1到8,这样我们可以用一个十位数来同时记录皇后的坐标值a[k++]=i*10+j。显示解法函数如下:
1 void show(int a[],int n) 2 { 3 int i,j; 4 for(i=0;i<N;i++) 5 { 6 for(j=0;j<N;j++) 7 { 8 if((i+1==a[n+i]/10)&&(j+1==a[n+i]%10)) 9 printf("Q "); 10 else 11 printf("* "); 12 } 13 printf(" "); 14 } 15 }
其中n为需要显示的解法编号在数组a中的位置(即需要显示的解法编号减去1后在乘以8)