1.定義:
回溯算法是一種在窮舉查找基礎上的增強變形。主要是在嘗試搜索的過程中,每次只構造解的一個分量,當發現部分構造解滿足求解條件時,就接受下一個分量所做的第一個合法選擇;當發現部分構造解不滿足求解條件時,就回溯返回,嘗試另外的路徑。這種走不通就回頭的算法稱為回溯算法。
主要思想:通過對所做的選擇構造一顆狀態空間樹,按照深度優先的策略,從根開始深度搜索狀態空間樹。當搜索到某一結點,先判斷該節點是否可以構成完整解,如果可以就繼續向下搜索;否則就逐層返回回溯,嘗試其他路徑。(隱式圖的深度優先搜索)
核心思想:個人以為,回溯法的核心是
狀態空間樹的構造(可能是隱式構造的),一旦確立了狀態空間樹,就可以直接得出問題的解。狀態空間樹的構造思想是:按照深度優先的方式,如果當前節點的下一個分量所構造的部分解可以導致完整解,就生成節點的子女,並添加下一個分量的第一個合法選擇;否則就回溯到該節點的父母,考慮部分解的下一個分量選擇。
2.回溯算法的一般步驟(解決某些約束下的最優解):
(1)確定問題的解空間(包含所有解的一顆狀態空間樹),確定完全解的形式,以及部分構造解無法構成完全解的剪枝規則。(
ps:問題的解空間(狀態空間樹)是在DFS的過程中動態生成的)
(2)確定節點(分量)的擴展規則,即下一個節點的選擇規則。 (
ps:如曼哈頓問題中,按照字母順序選擇下一個分量)
(3)以深度優先的方式搜索解空間,並在搜索的過程中用
剪枝函數判斷該節點是否可以生成完全解。如果可以,則進入該節點的子樹下一步搜索(構造下一個分量);如果不能,則跳過該節點的子樹(不再生成下一個分量),逐層回溯。(
ps:剪枝函數包括約束函數和界限函數,分別剪去不滿足約束的節點和不能得到最優解的函數)
3.回溯的算法框架:
(1)遞歸的方式:
int x[n]; void backtrack(int t) { if(t>n) //到達葉子節點,輸出結果,x是可行解 output(x); else { for i = 1 to k //該節點的子節點(分量的所有下一個分量) { x[t] = value(i); //取出子節點的值 if(constraint(t) && bount(t) ) //剪枝函數:判斷約束和界限 backtrack(t+1); //可以生成完全解,繼續遞歸下去 } } }
特點:思路簡單,設計簡單,但算法時間效率很差。
(2)遞推的方式:
void backtrack() { int t=1; while(t>0) { if(existSubNode(t)) //存在子節點:該結點還有可以構造的節點(下一個分量) { for i = 1 to k { x[t] = value(i); //相當於在此處建立一個結點 if(constraint(t) && bount(t) ) //剪枝函數判斷約束和界限 { if(isResult(t) ) //得到了一個結果,輸出 output(x); else //還沒有得到結果,繼續向下搜索 t++; } else { eraseSubNode(t) //該結點無法構成完全解,故刪去該結點,並設該結點不可再作為子節點。 } } } else { eraseSubNode(t); //該結點沒有子結點,也不能完全解,所以刪去該結點。 t--; //進行回溯 } } }
特點:設計復雜,但算法時間效率很高。
4.用回溯的思想實現背包問題:
用回溯算法解決01背包問題,步驟:
(1)問題的解空間是子集樹,節點表示前 t 個物品的存放狀態,樹枝的值表示第 t 個物品有沒有放入背包。完全解的形式是01組成的N個數,避免無法構造完全解的約束剪枝規則是:當前背包的物品重量CurWeight加新增的物品重量不能超過背包的承重C(CurWeight+w[t]<C)。
當物品數為3時,解空間(狀態空間樹)如下:

(2)節點擴展規則:數字遞增順序,也就是說第t個物品放入背包和沒有放入背包的狀態,即 0 和 1.
(3)回溯搜索:如果搜索到葉子節點,表示一條路徑搜索結束,如果存在更優解則記錄。
如果沒有搜索到葉子節點,則遍歷其子節點,滿足剪枝條件時繼續向下搜索,不滿足時回溯。
#include <iostream> using namespace std; #define N 4 //物品總數 #define C 5 //背包的承重 int w[N] = {2,1,3,2}; //物品重量 int v[N] = {12,10,20,15}; //物品價值 int CurWeight = 0; //當前總重量 int CurValue = 0; //當前總價值 int x[N] ={0,0,0}; //當前的背包選取情況(1表示選取,0表示不選取) int MaxValue = 0; //最大價值 int MaxPack[N] = {0,0,0}; //最大價值下的背包選取情況(1表示選取,0表示不選取) void backtrack(int t) { if(t >= N) //到達葉子處,說明已經求得一個滿足約束的完全解,接下來判斷是否最優 { if(CurValue > MaxValue) { MaxValue = CurValue; for(int i=0;i<N;i++) MaxPack[i] = x[i]; } } else //沒到葉子處,還要繼續向下搜索下去 { for(int i=0;i<=1;i++) { x[t] = i; //01背包問題中,每個物品的存放狀態是 0 或 1。此處表示第 t 個物品在背包中的狀態為 0 或 1。 if(x[t] == 0) //第 t 個物品沒有放入背包,不進行剪枝判斷 { backtrack(t+1); } else //第 t 個物品放入背包,要進行剪枝判斷 { if(CurWeight + w[t] <= C) //剪枝判斷為滿足約束條件 { CurWeight += w[t]; //當前結點滿足約束,則增加結點的值到當前值中 CurValue += v[t]; backtrack(t+1); //繼續向下搜索 CurWeight -= w[t]; //為了回溯到父節點的狀態,要將子節點新增的內容刪掉 CurValue -= v[t]; } } } } } int main() { backtrack(0); cout<<"the max value pack combination: "; for(int i=0; i<N; i++) cout<<MaxPack[i]<<" "; cout<<"\nthe max value: "<<MaxValue<<endl; }