使用回溯演算法解決N皇后問題以及間隔排列問題

鴨脖發表於2012-11-01

回溯法(探索與回溯法)是一種選優搜尋法,按選優條件向前搜尋,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。


在程式設計中,最容易出錯的地方,便是迭代和遞迴。那麼回溯便是這兩種方法的結合。按照我的理解,使用回溯法需要心中有結構。回溯這個詞用的很形象,按照人的理解就是往後走,但是計算機演算法中的回溯是一個相對的概念,不向前走便是往回走。最外層是一個迭代迴圈,代表的是上一步,迭代中的程式碼有向前走的遞迴,如果在向前走的過程中,沒有滿足相應的約束函式,那麼便繼續上一步的迭代過程,這便稱為回溯。下面我們結合例項來說一下演算法的核心思想,這是我自己寫的程式碼,所以有些地方可能質量不是很高。


在下面的程式碼中,我都把約束函式定義為Place,回溯函式的名稱可以隨便定義。注意觀察回溯函式的結構,大體都一致。


n皇后問題:

在n×n格的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。n後問題等價於再n×n的棋盤上放置n個皇后,任何2個皇后不妨在同一行或同一列或同一斜線上。


//n-Quene
bool Place(int k,int i,int* x){
for(int j=0;j<k;j++){//decide wether the current queen conflict with other before queens
if(x[j] == i || abs(x[j]-i)==abs(j-k))
return false;
return true;
}
}


void NQueens(int k,int n,int* x){
for(int i=0;i<n;i++){
if(Place(k,i,x)){
x[k] = i;
if(k == n-1){
for(int i=0;i<n;i++) cout<<i<<" ";
cout<<endl;
}else
NQueens(k+1,n,x);
}
}
}
void NQueens(int n,int* x){
NQueens(0,n,x);
}


int main(){
int n;
do{
cout<<"請輸入n的大小:(n不小於3)"<<endl;
cin>>n;
}while(n<3);
int* x = (int*)malloc(sizeof(int)*n);


NQueens(n,x);

}


約束函式的作用是不讓第k個皇后和以前的皇后發生衝突,那麼這裡的約束函式很好寫啦。注意回溯函式。最外層的for迴圈是將這個皇后在所有可能的列上進行try,如果可以放那麼就通過遞迴向前走。一旦退出遞迴,並且沒有達到n-1,那麼就說面前面有個地方走不通了,繼續for迴圈,這樣便很形象的展示出了什麼是回溯。


下面再來看一個例項問題:

給定n個數,從1-n,每個數取兩個行成2n個數。然後將這2n個數進行排列,使得1之間有1個數,....n之間有n個數。列出所有可能的排序結果。

這道題目可以使用上述類似的方法進行解答。



#include <stdio.h>
#include <iostream>
using namespace std;


//constraint function
bool Place(int k,int i,int* x){//to see whether the selected index i is valid
if(x[i] != 0 || x[i+k+1] != 0)
return false;
return true;
}


void refresh(int k,int n,int* x){
for(int j=0;j<2*n;j++){
if(x[j] <= k)
x[j] = 0;
}

}
//backtrack function
void Backtrack(int k,int n,int* x){
for(int i=0;i<2*n-k-1;i++){
refresh(k,n,x);
if(Place(k,i,x)){
x[i] = x[i+k+1] = k;
if(k == 1){//get the answer state
cout<<"============================"<<endl;
for(int j=0;j<2*n;j++){
if(j == 2*n-1)
cout<<x[j]<<endl;
else
cout<<x[j]<<"\t";
}
}else{
Backtrack(k-1,n,x);
}
}
}
}
int main(){
int n;
do{
cout<<"請輸入n的大小:(n不小於3)"<<endl;
cin>>n;
}while(n<3);


cout<<"排序結果如下所示:"<<endl;
int* x = (int*)malloc(sizeof(int)*n*2);//define the array
//int* x = new int[n];
for(int i=0;i<n*2;i++)//initialize the array
x[i] = 0;


Backtrack(n,n,x);


char a; 
cin>>a;
return 0;
}


這裡我的約束函式寫的比較簡單,但是在for迴圈之下,必須加上refresh函式對陣列的狀態進行恢復,否則演算法無法進行,因為我的約束函式是根據陣列的狀態進行判定的。


回溯法在很多情況下都要用到,所以需要好好理解並且靈活的運用。















相關文章