1.编写蛮力法解决n皇后问题,求出生成解的个数和生成树的节点个数。
2.编写回溯法解决n皇后问题,求出生成解的个数和生成树的节点个数,并且和蛮力法进行比较验证结果。
3.对回溯法和蛮力法进行性能的比较。
4.对回溯法进行性能优化。
1. 编写蛮力法解决n皇后问题,求出生成解的个数和生成树的节点个数。
只要使用一维数组就可以表示排列情况,比如a[2]的值表示第3行摆放的位置,这样做的好处就是不用考虑同一行里面有多个棋子。
蛮力法就是深度遍历所有的节点,一直到叶子节点再判断该方法是不是其中的一个解。N皇后的节点数目是n1+n2+…….+nn个。判断是否为可行解的时候,自上到下一层一层判断,将k层棋子的位置a[k]与上面k-1层的棋子位置a[i]进行比较,判断是否出现在同一列a[k]==a[i],或者在同一对角线上k-i==abs(a[k]-a[i])。这里可以进行判断如果出现以上两种情况就跳出循环不要再判断后面的,减少判断次数。蛮力法的生成解和生成节点数目如下图所示。
1. 编写回溯法解决n皇后问题,求出生成解的个数和生成树的节点个数。
回溯法在于剪枝,在遍历生成树的时候判断当前节点是否为可行的节点,一边生成一边判断,如果当前节点不满足,就不用遍历该节点的子节点了。用一个1维数组来表示解的集合,这样就不用考虑一行内摆放两种棋子的可能。4皇后的回溯法图示如下图所示。回溯法的伪代码如下:
boolQueen(intplace,introw){
for(int i to row-1){ // i代表行
if(a[i]==place || (row-i)==abs(place-a[i]))
return false;
}
return true;
}
voidBackTrack(int row){
for(int i to row-1){ //i代表位置
sum++; //节点数目++
if( Queen(i,row) ){
a[row] = i;
if(row==n-1){
numOfResult++;
}
else
BackTrack(row+1);
}
}
}
比如输出n皇后所有可能的解,可以验证回溯法的正确性。如下图所示,输出了4皇后的两个解,每一行代表一个解,该行的第i个数字表示该解的i行棋子摆放的位置。如下图所示,可以清楚地看出结果是正确的。
图2:回溯法输出4皇后的结果
将回溯法的结果和蛮力法比较(由于蛮力法比较慢所以只运行到10皇后),可以发现结果都是对的,但是蛮力法的生成节点数目却很多。
规模n | 蛮力法的节点数 | 回溯法的节点数 | 解个数 | 效率比 |
4 | 340 | 60 | 2 | 17.6% |
5 | 3905 | 220 | 10 | 5.6% |
6 | 55986 | 894 | 4 | 1.6% |
7 | 960799 | 3584 | 40 | 0.37% |
8 | 19173960 | 15720 | 92 | 0.082% |
9 | 435848049 | 72378 | 352 | 0.0175% |
10 | 11111111110 | 348150 | 724 | 0.003% |
11 | 313842837671 | 1806706 | 2680 | 0.0005% |
12 | 9726655034460 | 10103868 | 14200 | 0.0001% |
13 | 328114698808273 | 59815314 | 73712 | 0.00001% |
14 | 11966776581370170 | 377901398 | 365596 | 0.000003% |
15 | 469172025408063615 | 2532748320 | 2279184 | 0.0000005% |
经过上面的对比,可以清楚地看出回溯法相对于蛮力法效率提升很大。
1. 对回溯法进行优化。
如果仔细看解的详情,发现他们是对称相反的。比如第一个解是135024,第4个解是420531。如下图所示。
因为棋盘是对称的,所以我们只要计算一半的解另外一半就能解出来,效率能提升50%。只要在第一层下子的时候只下该行一半的位置即可。但是对于奇数的n,计算出来的结果会将第一行下在中间位置的解算了两遍。所以要单独处理一下。镜像法解如下图所示,可以清楚看到节点数目减少了一半。