1. 程式人生 > >關於回溯演算法的遞迴與非遞迴解法

關於回溯演算法的遞迴與非遞迴解法

摘要:本文簡要描述了回溯演算法的基本思路,並給出了幾個典型例項的原始碼

關鍵字:回溯,搜尋,非遞迴,全排列,組合,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;
}