1、回溯法
(1)描述:回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法。
(2)原理: 回溯法在問題的解空間樹中,按深度優先策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任意一點時,先判斷該結點是否包含問題的解。如果肯定不包含,則跳過對該結點為根的子樹的搜索,逐層向其祖先結點回溯;否則,進入該子樹,繼續按深度優先策略搜索。
回溯法的基本做法是搜索,或是一種組織得井井有條的,能避免不必要搜索的窮舉式搜索法。這種方法適用於解一些組合數相當大的問題。有許多問題,當需要找出它的解集或者要求回答什么解是滿足某些約束條件的最佳解時,往往要使用回溯法。
(3)問題的解空間
問題的解向量:回溯法希望一個問題的解能夠表示成一個n元式(x1,x2,…,xn)的形式。
顯約束:對分量xi的取值限定。
隱約束:為滿足問題的解而對不同分量之間施加的約束。
解空間:對於問題的一個實例,解向量滿足顯式約束條件的所有多元組,構成了該實例的一個解空間。
注意:同一個問題可以有多種表示,有些表示方法更簡單,所需表示的狀態空間更小(存儲量少,搜索方法簡單)。
(4)生成問題狀態的基本方法
擴展結點:一個正在產生兒子的結點稱為擴展結點。
活結點:一個自身已生成但其兒子還沒有全部生成的節點稱做活結點。
死結點:一個所有兒子已經產生的結點稱做死結點。
深度優先的問題狀態生成法:如果對一個擴展結點R,一旦產生了它的一個兒子C,就把C當做新的擴展結點。在完成對子樹C(以C為根的子樹)的窮盡搜索之后,將R重新變成擴展結點,繼續生成R的下一個兒子(如果存在)。
寬度優先的問題狀態生成法:在一個擴展結點變成死結點之前,它一直是擴展結點。
回溯法:為了避免生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函數(bounding function)來處死那些實際上不可能產生所需解的活結點,以減少問題的計算量。具有限界函數的深度優先生成法稱為回溯法。
(5)回溯法的基本思想
基本思想:
用回溯法解題的一個顯著特征是在搜索過程中動態產生問題的解空間。在任何時刻,算法只保存從根結點到當前擴展結點的路徑。如果解空間樹中從根結點到葉結點的最長路徑的長度為h(n),則回溯法所需的計算空間通常為O(h(n))。而顯式地存儲整個解空間則需要O(2h(n))或O(h(n)!)內存空間。
解題步驟:
1)針對所給問題,定義問題的解空間;
2)確定易於搜索的解空間結構;
3)以深度優先方式搜索解空間,並在搜索過程中用剪枝函數避免無效搜索。
常用剪枝函數:用約束函數在擴展結點處剪去不滿足約束的子樹;用限界函數剪去得不到最優解的子樹。
遞歸回溯:
回溯法對解空間作深度優先搜索,因此,在一般情況下用遞歸方法實現回溯法。
迭代回溯:
采用樹的非遞歸深度優先遍歷算法,可將回溯法表示為一個非遞歸迭代過程。
子集樹:當所給的問題是從n個元素的集合S中找出滿足某種性質的子集時,相應的解空間稱為子集樹。例如,那個物品的0-1背包問題所相應的解空間樹就是一顆子集樹。這類子集問題通常有2^n個葉節點,其節點總個數為2^(n+1)-1。遍歷子集樹的任何算法均需要O(2^n)的計算時間。

用回溯法遍歷子集樹的一般算法可描述如下:
排列樹:當所給問題是確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。排列樹通常有n!個葉子節點。因此遍歷排列樹需要O(n!)的計算時間。

用回溯法遍歷排列樹的一般算法可描述如下:
問題描述:有一批共n個集裝箱要裝上2艘載重量分別為c1和c2的輪船,其中集裝箱i的重量為wi,且
,裝載問題要求確定是否有一個合理的裝載方案可將這些集裝箱裝上這2艘輪船。如果有,找出一種裝載方案。
例如:當n=3,c1=c2=50,且w=[10,40,40]時,則可以將集裝箱1和2裝到第一艘輪船上,而將集裝箱3裝到第二艘輪船上;如果w=[20,40,40],則無法將這3個集裝箱都裝上輪船。
基本思路: 容易證明,如果一個給定裝載問題有解,則采用下面的策略可得到最優裝載方案。
(1)首先將第一艘輪船盡可能裝滿;
(2)將剩余的集裝箱裝上第二艘輪船。
將第一艘輪船盡可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近C1。由此可知,裝載問題等價於以下特殊的0-1背包問題。

用回溯法設計解裝載問題的O(2^n)計算時間算法。在某些情況下該算法優於動態規划算法。
算法設計:
用回溯法解裝載問題時,用子集樹表示其解空間顯然是最合適的。用可行性約束函數可剪去不滿足約束條件
的子樹。在子集樹的第j+1層的結點z處,用cw記當前的裝載重量,即cw=
,則當cw>c1時,以結點z為根的子樹中所有結點都不滿足約束條件,因而該子樹中的解均為不可行解,故可將該子樹剪去。(該約束函數去除不可行解,得到所有可行解)。
可以引入一個上界函數,用於剪去不含最優解的子樹,從而改進算法在平均情況下的運行效率。設z是解空間樹第i層上的當前擴展結點。cw是當前載重量;bestw是當前最優載重量;r是剩余集裝箱的重量,即r=
。定義上界函數為cw+r。在以z為根的子樹中任一葉結點所相應的載重量均不超過cw+r。因此,當cw+r<=bestw時,可將z的右子樹剪去。
- #include "stdafx.h"
- #include <iostream>
- using namespace std;
- template<class Type>
- Type MaxLoading(Type w[ ], Type c, int n, int bestx[ ]);
- int main()
- {
- int n=3,m;
- int c=50,c2=50;
- int w[4]={0,10,40,40};
- int bestx[4];
- m=MaxLoading(w, c, n, bestx);
- cout<<"輪船的載重量分別為:"<<endl;
- cout<<"c(1)="<<c<<",c(2)="<<c2<<endl;
- cout<<"待裝集裝箱重量分別為:"<<endl;
- cout<<"w(i)=";
- for (int i=1;i<=n;i++)
- {
- cout<<w[i]<<" ";
- }
- cout<<endl;
- cout<<"回溯選擇結果為:"<<endl;
- cout<<"m(1)="<<m<<endl;
- cout<<"x(i)=";
- for (int i=1;i<=n;i++)
- {
- cout<<bestx[i]<<" ";
- }
- cout<<endl;
- int m2=0;
- for (int j=1;j<=n;j++)
- {
- m2=m2+w[j]*(1-bestx[j]);
- }
- cout<<"m(2)="<<m2<<endl;
- if(m2>c2)
- {
- cout<<"因為m(2)大於c(2),所以原問題無解!"<<endl;
- }
- return 0;
- }
- template <class Type>
- Type MaxLoading(Type w[],Type c,int n,int bestx[])//迭代回溯法,返回最優載重量及其相應解,初始化根結點
- {
- int i=1;//當前層,x[1:i-1]為當前路徑
- int *x=new int[n+1];
- Type bestw=0, //當前最優載重量
- cw=0, //當前載重量
- r=0; //剩余集裝箱重量
- for (int j=1;j<=n;j++)
- {
- r+=w[j];
- }
- while(true)//搜索子樹
- {
- while(i<=n &&cw+w[i]<=c)//進入左子樹
- {
- r-=w[i];
- cw+=w[i];
- x[i]=1;
- i++;
- }
- if (i>n)//到達葉結點
- {
- for (int j=1;j<=n;j++)
- {
- bestx[j]=x[j];
- }
- bestw=cw;
- }
- else//進入右子樹
- {
- r-=w[i];
- x[i]=0; i++;
- }
- while (cw+r<=bestw)
- { //剪枝回溯
- i--;
- while (i>0 && !x[i])
- {
- r+=w[i];
- i--;
- }
- //從右子樹返回
- if (i==0)
- {
- delete []x;
- return bestw;
- }
- x[i]=0;
- cw-=w[i];
- i++;
- }
- }
- }
