1. 程式人生 > >華為機試—N皇后問題(高階題160分:兩種回溯法解決 吐血整理)

華為機試—N皇后問題(高階題160分:兩種回溯法解決 吐血整理)

一、問題描述:
    在n×n格的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。n後問題等價於再n×n的棋盤上放置n個皇后,任何2個皇后不妨在同一行或同一列或同一斜線上。
輸入:
    給定棋盤的大小n (n ≤ 13)
輸出:
    輸出有多少种放置方法。

二、解題思路:
    要解決N皇后問題,其實就是要解決好怎麼放置這n個皇后,每一個皇后與前面的所有皇后不能在同一行、同一列、同一對角線,在這裡我們可以以行優先,就是說皇后的行號按順序遞增,只考慮第i個皇后放置在第i行的哪一列,所以在放置第i個皇后的時候,可以從第1列判斷起,如果可以放置在第1個位置,則跳到下一行放置下一個皇后。如果不能,則跳到下一列...直到最後一列,如果最後一列也不能放置,則說明此時放置方法出錯,則回到上一個皇后向之前放置的下一列重新放置。此即是回溯法的精髓所在。當第n個皇后放置成功後,即得到一個可行解,此時再回到上一個皇后重新放置尋找下一個可行解...如此後,即可找出一個n皇后問題的所有可行解。


三、複雜度分析:
    關於N皇后問題的複雜度問題可以說是眾說紛紜了,自己也改變過好幾次,剛開始以為棋盤是n行n列,所以理所當然應該是n^2,後來發現在每列選擇可否放置的比較上又做了一次迴圈,所以應該是n^3,但想了很久,發現判斷可否放置的時候不是每次都迴圈到n,它是根據皇后i的取值而變化的,所以複雜度應該是1/3 n^3左右,即是小於n^3的。

四、測試程式碼:
    兩個實現方法,一個是遞歸回溯,一個是迭代回溯,思路都一樣,只是形式不同罷了。

遞歸回溯

#include <stdio.h>
#include <math.h>   
#define N 15   
  
int n; //皇后個數   
int sum = 0; //可行解個數   
int x[N]; //皇后放置的列數   
  
/*  
 *判斷函式,判斷第k個皇后是否可以放在某一個位置  
 *如果與之前的皇后出現在同一列或同一對角線則放置失敗,返回0,否則返回1  
*/  
int place(int k)   
{   
    int i;   
    for(i=1;i<k;i++)   
      if(abs(k-i)==abs(x[k]-x[i]) || x[k] == x[i])   
        return 0;   
    return 1;   
}   
  
/*  
 *求解可行解函式,當第t個皇后可以放置在t行的某一位置時,繼續放置下一皇后,直到  
 *所有皇后放置結束,如果某一皇后不能放置,則移向下一列放置,如果這一列都不能放  
 *置或所有皇后放置結束,返回上一皇后重新放置,最終返回所有可行解個數。  
*/  
int queen(int t)   
{   
    if(t>n) //當放置的皇后超過n時,可行解個數加1
      sum++;   
    else  
      for(int i=1;i<=n;i++)   
      {   
          x[t] = i; //標明第t個皇后放在第i列   
          if(place(t)) //如果可以放在某一位置,則繼續放下一皇后   
            queen(t+1);    
      }   
    return sum;   
}   
  
int main()   
{   
    int t;   
    while(~scanf("%d",&n)){   
		t = queen(1);   
		if(n == 0) //如果n=0,則可行解個數為0,這種情況一定不要忽略   
		  t = 0;   
		printf("%d\n",t); 
		sum=0;
	}
    return 0;   
} 


迭代回溯

#include <stdio.h> 
#include <math.h>  
#define N 15   
  
int n;   
int sum = 0;   
int x[N];   
  
int place(int k)   
{   
    int i;   
    for(i=1;i<k;i++)   
      if(abs(k-i)==abs(x[k]-x[i]) || x[k] == x[i])   
        return 0;   
    return 1;   
}   
  
int queen()   
{   
      x[1] = 0;   
      int t=1;   
      while(t>0)   
      {   
          x[t]+=1;   
          while(x[t]<=n && !place(t))   
              x[t]++;   
          if(x[t]<=n)   
            if(t == n) 
			{
				for(int k=1;k<=n;k++)
					printf("%d  ",x[k]);
				printf("\n");
				sum++; 
			}
            else  
              x[++t] = 0;   
          else  
            t--;   
      }   
      return sum;   
}   
  
int main()   
{   
    int t;  
	while(scanf("%d",&n)){ 
		t = queen();   
		printf("%d\n",t);
		sum=0;
	}
    return 0;   
}  

五、測試結果:

六、總結

      迭代回溯的註釋因為和遞歸回溯差不多,所以就不再附註了。在這裡我們可以看到,遞歸回溯非常簡單,結構很清晰,但它有一個潛在的問題存在,即當隨著變數n的增大,遞迴法的複雜度也將成幾何級增長,也有可能會出現重複的情況,所以我們在解決問題時,如果能用迭代法解決,最好還是不要用遞迴法,除非你已經對這個遞迴瞭如指掌了。
      通過這個N皇后問題,我想大概已經把回溯法講得很清楚了吧,回溯法得到的解展開就是一個樹,很多方法都是可以通過回溯法來解決的,效率很高,但如果基數過大的話,回溯法就顯得不是那麼適用了,這也是回溯法的弱勢吧。比如說這個N皇后問題,好像當n>60的時候,回溯法就不能完全地解決問題了,這時可以考慮用概率演算法來解決,它可以解決很大的基數,只不過結果不是很精確而已。所以我們在面對一個問題時,具體是使用什麼演算法還是要結合實際情況來考慮的,目的都是更方便、更準確地解決問題。