一、基本概念
回溯法是一種選優搜索法(試探法)。
基本思想:將問題P的狀態空間E表示成一棵高爲n的帶全有序樹T,把求解問題簡化爲搜索樹T。搜索過程採用深度優先搜索。搜索到某一結點時判斷該結點是否包含原問題的解,如果包含則繼續往下搜索,如果不包含則向祖先回溯。
通俗來說,就是利用一個樹結構來表示解空間,然後從樹的根開始深度優先遍歷該樹,到不滿足要求的葉子結點時向上回溯繼續遍歷。
幾個結點:
擴展結點:一個正在產生子結點的結點稱爲擴展結點
活結點:一個自身已生成但未全部生成子結點的結點
死結點:一個所有子結點已全部生成的結點
二、基本步驟
1、分析問題,定義問題解空間。
2、根據解空間,確定解空間結構,得搜索樹。
3、從根節點開始深度優先搜索解空間(利用剪枝避免無效搜索)。
4、遞歸搜索,直到找到所要求的的解。
三、兩類常見的解空間樹
1、子集樹
當問題是:從n個元素的集合S中找出滿足某種性質的子集時,用子集樹。
子集樹必然是一個二叉樹。常見問題:0/1揹包問題、裝載問題。
//遍歷子集樹僞代碼
void backtrack(int t){
if(t>n){
output(x);
} else{
for(int i=0;i<=1;i++){
x[t] = i;
if(legal(t))
backtrack(t+1);
}
}
}
遍歷子集樹時間複雜度:O(2^n)
2、排列樹
當問題是:確定n個元素滿足某種排列時,用排列數。常見問題:TSP旅行商問題,N皇后問題。
//遍歷排列樹僞代碼
void backtrack(int t){
if(t>n){
output(x);
}else{
for(int i=0;i<=1;i++){
swap(x[t],x[i]);
if(legal(t)){
backtrack(t+1);
}
swap(x[t],x[i]);
}
}
}
遍歷排列樹時間複雜度:O(n!)
通俗地講,結合Java集合的概念,選擇哪種樹其實就是看最後所得結果是放入一個List(有序)裏,還是放入一個Set(無序)裏。
四、剪枝函數
剪枝函數能極大提高搜索效率,遍歷解空間樹時,對於不滿足條件的分支進行剪枝,因爲這些分支一定不會在最後所求解中。
常見剪枝函數:
約束函數(對解加入約束條件)、限界函數(對解進行上界或下界的限定)
滿足約束函數的解纔是可行解。
五、求解模式
設問題的解是一個n維向量(a1,a2。。an),約束條件是ai之間滿足某種條件,記爲func(ai)
非遞歸回溯模式
int a[n],i;
//初始化數組a[];
i=1;
//有路可走,並且未達目標
while(i>0){
if(i>n){
//搜索到一個解,輸出;
} else {
//a[i]第一個可能得值
while(a[i]在不滿足約束條件且在搜索空間內){
a[i]下一個可能的值
}
}
}
遞歸回溯模式
一般情況下使用遞歸函數來實現回溯法比較簡單,其中i爲搜索的深度。
int a[n];
func(int i){
if(i>n){
//輸出結果;
}else{
for(j=下界; j<=上界; j=j+1){ //枚舉i所有可能得路徑
if(func(j)){
a[i]=j;
}
}
}
}
常見案例問題
1、0/1揹包問題
2、TSP旅行商問題
3、最優裝載問題
4、N-皇后問題
具體問題可百度詳細內容。