關於回溯演算法的遞迴與非遞迴解法
摘要:本文簡要描述了回溯演算法的基本思路,並給出了幾個典型例項的原始碼
關鍵字:回溯,搜尋,非遞迴,全排列,組合,N皇后,整數劃分,0/1揹包
回溯是按照某種條件在解空間中往前試探搜尋,若前進中遭到失敗,則回過頭來另擇通路繼續搜尋。
符號宣告:
解空間:[a1,a2,a3,...,an];
x[k]為解空間元素的索引, 0 <= x[k] < n;k為陣列x的索引;
a[x[0~n-1]]表示一組解。
//判斷解空間中的a[x[k]]是否滿足條件
bool CanbeUsed(int k)
{
if(x[k] 不滿足條件) return false;
else return true;
}
演算法描述如下:
(1) k = 0; x[k] = -1;
(2)while( k >= 0 )
a. x[k] = x[k] + 1;
b. while(x[k] < n && ( ! CanbeUsed(k) ))//遍歷解空間,直到找到可用的元素
x[k] = x[k] + 1;
c. if(x[k] > n -1)//x[k]超出瞭解空間a的索引範圍
k = k - 1; //回溯
d. else if( k == n -1)//找到了n - 1個元素
輸出一組解
e. else //當前元素可用,更新變數準備尋找下一個元素
k = k + 1;
x[k] = -1;
回溯的這種實現方式非常適合於在解空間中搜索特定長度的序列!
例項原始碼:
1.回溯之全排列(VC6.0/VS2005)==============================================
////////////////////////////////
//回溯搜尋之全排列
#include<iostream>
#include<string>
using namespace std;
#define N 100
string str;
int x[N];
bool IsPlaced(int n)
{
for(int i = 0; i < n ; ++i)
{
if(x[i] == x[n])
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < str.length(); ++i)
cout<<str[x[i]];
cout<<endl;
}
void Arrange()
{
int k = 0; x[k] = -1;
while(k >= 0)
{
x[k] = x[k] + 1;
while(x[k] < str.length() && !IsPlaced(k))
{
x[k] = x[k] + 1;
}
if(x[k] > str.length() - 1)
{
k = k - 1;
}
else if( k == str.length() - 1)
{
PrintResult();
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
int main()
{
cout<<"input:"<<endl;
while(cin>>str)
{
cout<<str<<" arrange......"<<endl;
Arrange();
cout<<"input:"<<endl;
}
return 0;
}
2.八皇后(N皇后)============================================================
////////////////////////////////////////
//回溯之N皇后問題[ 4<=N<=100]
#include <iostream>
using namespace std;
#define N 8
//用於防置皇后的棋盤
//0表示未放置,1表示已放置
int board[N][N]={
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0
};
int x[N];
//按列擺放
bool CanbePlaced(int k)
{
for(int i = 0; i < k ; ++i)
{
if(x[i] == x[k] || abs(x[i] - x[k]) == abs(i - k))
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < N; ++i)
for(int j = 0; j < N; ++j)
board[i][j] = 0;
for(int i = 0; i < N; ++i)
board[i][x[i]] = 1;
for(int i = 0; i < N; ++i)
{
for(int j = 0; j < N; ++j)
{
if(board[i][j] == 1)
cout<<"* ";
else
cout<<"- ";
}
cout<<endl;
}
cout<<endl;
}
int count = 0;
void NQ()
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < N && !CanbePlaced(k))
x[k] = x[k] + 1;
if(x[k] > N - 1)
{
k = k - 1;
}
else if( k == N - 1)
{
PrintResult();
count ++;
}
else
{
k = k + 1;
x[k] = - 1;
}
}
}
int main()
{
NQ();
cout<<"一共:"<<count<<"組擺放方法"<<endl;
system("pause");
return 0;
}
3.回溯之整數劃分==========================================================
/////////////////////////
//回溯之整數劃分
#include<iostream>
using namespace std;
#define N 100
int x[N];
int result[N];//儲存一組解
int count = 0;//解的組數
int sum(int k)
{
int sum = 0;
for(int i = 0; i <= k; ++i)
sum += result[x[i]];
return sum;
}
//a1>=a2>=...>=an
//a1+a2+...+an = n
bool IsSuit(int n,int k)
{
if(sum(k) > n)
return false;
if(k > 0 && result[x[k]] > result[x[k-1]] )
return false;
return true;
}
void PrintResult(int n,int k)
{
if(sum(k) == n)
{
for(int i = 0; i <= k; ++i)
cout<<result[x[i]]<<" ";
cout<<endl;
count++;
}
}
void SplitInt(int n)
{
//解空間[n,n-1,n-2,...,1]
for(int i = 0; i < n; ++i)
{
result[i] = n - i;
}
for(int m = 1; m <= n; ++m)
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < n && !IsSuit(n,k) )
x[k] = x[k] + 1;
if(x[k] > n - 1)
k = k - 1;
else if( k == m - 1)
{
PrintResult(n,k);
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
}
int main()
{
int num;
while(cin>>num)
{
count = 0;
SplitInt(num);
cout<<"共:"<<count<<"組"<<endl;
}
return 0;
}
4.回溯之組合===============================================================
/////////////////////////////////////////
//回溯之組合
//找出所有從m個元素中選取n(n<=m)元素的組合
#include<iostream>
using namespace std;
#define M 5
#define N 3
char elements[M]={'a','b','c','d','e'};
int x[N];
bool CanbeUsed(int k)
{
for(int i = 0; i < k; ++i)
if(x[i] == x[k])
return false;
if(k > 0 && elements[x[k]] < elements[x[k-1]])
{
return false;
}
return true;
}
void PrintResult()
{
for(int i = 0; i < N; ++i)
{
cout<<elements[x[i]]<< " ";
}
cout<<endl;
}
void Compose()
{
int k = 0;
x[k] = -1;
while( k >= 0 )
{
x[k] = x[k] + 1;
while(x[k] < M && !CanbeUsed(k))
x[k] = x[k] + 1;
if(x[k] > M - 1)
k = k - 1;
else if( k == N - 1)
{
PrintResult();
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
int main()
{
Compose();
system("pause");
return 0;
}
5.回溯之0/1揹包=============================================================
//////////////////////////////////////////////////
//回溯之0/1揹包問題
//.0/1揹包
//一個旅行者有一個最多能用m公斤的揹包,現在有n件物品,它們的重量分別是W1,W2,...,Wn,它們的價值分別為C1,C2,...,Cn.
//若每種物品只有一件求旅行者能獲得最大總價值。
#include<iostream>
using namespace std;
#define M 50
#define N 5
int weight[N] = {10,15,12,19,18};
int value[N] = {5,2,2,1,1};
int x[N]={-1,-1,-1,-1,-1};
int max_weight = 0;
int max_value = 0;
bool CanbeUsed(int k)
{
for(int i = 0; i < k ; ++i)
{
if(x[i] == x[k] )
return false;
}
return true;
}
void CalResult(int k)
{
int totalValue = 0;
int totalWeight= 0;
for(int i = 0 ; i <= k; ++i)
{
totalValue += value[x[i]];
totalWeight += weight[x[i]];
}
if(totalValue > max_value && totalWeight <= M )
{
max_value = totalValue;
max_weight = totalWeight;
cout<< totalWeight << " "<<totalValue<<endl;
}
}
void Bag()
{
//分別計算去1~N個物品的情況
for(int n = 1; n <= N; ++n)
{
int k = 0;
x[k] = -1;
while( k >= 0)
{
x[k] = x[k] + 1;
while(x[k] < n && !CanbeUsed(k))
x[k] = x[k] + 1;
if(x[k] > n - 1)
{
k = k - 1;
}
else if( k == n - 1)
{
CalResult(k);
}
else
{
k = k + 1;
x[k] = -1;
}
}
}
}
int main()
{
Bag();
cout<<"最優解為weight:" << max_weight << ",value:" <<max_value<<endl;
system("pause");
return 0;
}
八皇后問題是一個古老而著名的問題,是回溯演算法的典型例題。該問題是19世紀著名的數學家高斯1850年提出:在8×8格的國際象棋盤上擺放8個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。[英國某著名計算機圖形影象公司面試題]
解析:遞迴實現n皇后問題。
演算法分析:
陣列a、b、c分別用來標記衝突,a陣列代表列衝突,從a[0]~a[7]代表第0列到第7列。如果某列上已經有皇后,則為1,否則為0。
陣列b代表主對角線衝突,為b[i-j+7],即從b[0]~b[14]。如果某條主對角線上已經有皇后,則為1,否則為0。
陣列c代表從對角線衝突,為c[i+j],即從c[0]~c[14]。如果某條從對角線上已經有皇后,則為1,否則為0。
C++程式碼如下:
/* 實現N皇后問題*/
#include <stdio.h>
#define QUEENNUM 8
#define QCROSSNUM (QUEENNUM*2-1)
static char QueenArray[QUEENNUM][QUEENNUM];
static int a[QUEENNUM];
static int b[QCROSSNUM];
static int c[QCROSSNUM];
static int iQueenNum=0;//記錄皇后問題總共有多少中擺法
void Queen(int i);//i為行數
int main()
{
int iLine,iColumn;
for (iLine=0;iLine<QUEENNUM;iLine++)
{
a[iLine]=0;
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
QueenArray[iLine][iColumn]='*';
}
for(iLine=0;iLine<QCROSSNUM;iLine++)
b[iLine] = c[iLine]=0;
Queen(0);
return 0;
}
void Queen(int i)
{
int iColumn;
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
{
if(a[iColumn]==0&&b[i-iColumn+7]==0&&c[i+iColumn]==0)
{
QueenArray[i][iColumn]='@';//皇后標誌
a[iColumn]=1;
b[i-iColumn+7]=1;
c[i+iColumn]=1;
if (i<(QUEENNUM-1))
Queen(i+1);
else
{
int iLine,iColumn;
printf("The %d th state is :/n ",++iQueenNum);
for(iLine=0;iLine<QUEENNUM;iLine++)
{
for(iColumn=0;iColumn<QUEENNUM;iColumn++)
printf("%c",QueenArray[iLine][iColumn]);
printf("/n");
}
printf("/n/n");
}
//後面無論如何也無法放置皇后,則回溯重置
QueenArray[i][iColumn]='*';
a[iColumn]=0;
b[i-iColumn+7]=0;
c[i+iColumn]=0;
}
}
}
這段程式碼與一般的N!之類的遞迴大不相同, 以往都是從大到小的基本遞迴,如N!、打靶等等。這些方法都是採用巢狀方法, 中間沒有迴圈,沒有回溯的出現。八皇后問題顯然不同,中間不但有迴圈,而且還有很嚴謹的回溯。切入點也不同,是設定行數.
程式中,改變QUEENNUM的數值,就能得到N皇后的擺法。遞迴結束後的處理,包括清理本行的皇后,以及相關資料,即列的皇后資訊清除、主從對角線的標誌設定0。回溯法中,回溯後資料清理是有一定深度和難度的。學習的好方法就是多寫寫採用回溯法的遞迴演算法,多嘗試用回溯的方法做一些資料清理工作。
遞迴演算法步驟:
1.方法的選定,基本遞迴、分治法、動態規劃和回溯法的選擇哪種?
2.考慮不滿足什麼樣的條件遞迴結束?
3.考慮滿足條件,並且最後一次呼叫遞迴,如何處理?
4.考慮中間的滿足條件狀態如何處理?如遞迴函式要傳入什麼引數,處理哪些資料?呼叫遞迴函式後,要清理哪些資料,得到的資料如何處理?
基本遞迴法:
一個打靶問題的程式碼如下:
/*10槍打中90環的有多少種可能 ---- by zhaquanmin*/
#include <stdio.h>
long int sum = 0;
int storeArray[10];
void Comput(int score,int num)
{
if(score<0 || score>(num+1)*10)
return ;
if (num==0)
{
storeArray[num]=score;
for(int i=0;i<10;i++)
printf("%d "storeArray[i]);
printf("/n");
++sum;
return ;
}
for(int i=0;i<=10;++i)
{
storeArray[num]=i;
Comput(score-i,num-1);
}
}
int main()
{
Comput(90,9);
cout<<"sum="<<sum<<endl;
return 0;
}