- 概念
- 回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。
回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。
許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
- 回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。
- 基本思想
- 在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。
若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。
而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。
- 在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。
- 解題步驟
- (1)針對所給問題,確定問題的解空間:
首先應明確定義問題的解空間,問題的解空間應至少包含問題的一個(最優)解。
(2)確定結點的擴展搜索規則
(3)以深度優先方式搜索解空間,並在搜索過程中用剪枝函數避免無效搜索
- (1)針對所給問題,確定問題的解空間:
- 子集樹,排列數及其他
- 子集樹概念:當所給問題是從n個元素的集合S中找出S滿足的某種性質的子集時,相應的解空間樹稱為子集樹。例如,0-1背包問題,要求在n個物品的集合S中,選出幾個物品,使物品在背包容積C的限制下,總價值最大(即集合S的滿足條件<容積C下價值最大>的某個子集)。
另:子集樹是從集合S中選出符合限定條件的子集,故每個集合元素只需判斷是否(0,1)入選,因此解空間應是一顆滿二叉樹
- 回溯法搜索子集樹的一般算法
void backtrack(int t)//t是當前層數 { if(t>n)//需要判斷每一個元素是否加入子集,所以必須達到葉節點,才可以輸出 { output(x); } else { for(int i=0;i<=1;i++)//子集樹是從集合S中,選出符合限定條件的子集,故每個元素判斷是(1)否(0)選入即可(二叉樹),因此i定義域為{0,1} { x[t]=i;//x[]表示是否加入點集,1表示是,0表示否 if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函數 { backtrack(t+1); } } } }
- 回溯法搜索子集樹的一般算法
- 排列樹概念:當問題是確定n個元素滿足某種性質的排列時,相應的解空間稱為排列樹。排列樹與子集樹最大的區別在於,排列樹的解包括整個集合S的元素,而子集樹的解則只包括符合條件的集合S的子集。
- 回溯法搜索排列數的一般算法:
void backtrack(int t)//t是當前層數 { if(t>n)//n是限定最大層數 { output(x); } else { for(int i=t;i<=n;i++)//排列樹的節點所含的孩子個數是遞減的,第0層節點含num-0個孩子,第1層節點含num-1個孩子,第二層節點含num-2個孩子···第num層節點為葉節點,不含孩子。即第x層的節點含num-x個孩子,因此第t層的i,它的起點為t層數,終點為num,第t層(根節點為空節點,除外),有num-t+1個親兄弟,需要輪num-t+1回 { swap(x[t],x[i]);//與第i個兄弟交換位置,排列樹一條路徑上是沒有重復節點的,是集合S全員元素的一個排列,故與兄弟交換位置后就是一個新的排列 if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函數 { backtrack(t+1); } swap(x[i],x[t]); } } }
- 回溯法搜索排列數的一般算法:
- 非子集樹,非排列數
- 遞歸算法
void backtrack(int t) { if(t>n) { output(x); } else { for(int i=f(n,t);i<=g(n,t);i++) { x[t]=h(i); if(constraint(t)&&bound(t)) { backtrack(t+1); } } } }
- 遞歸算法
- 子集樹概念:當所給問題是從n個元素的集合S中找出S滿足的某種性質的子集時,相應的解空間樹稱為子集樹。例如,0-1背包問題,要求在n個物品的集合S中,選出幾個物品,使物品在背包容積C的限制下,總價值最大(即集合S的滿足條件<容積C下價值最大>的某個子集)。
- 這類題目的解題方法:先根據題意去判斷是什么類型的樹(子集樹,排列數,還是兩者都不是),再去寫代碼。
- 實例詳解
- 裝載問題:有一批共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背包問題。- 解題思路:畫出解題思路圖,得出這個解應該是一個子集樹(0000,0001,0010,0011等等)。采用子集樹的遞歸算法進行求解即可。
- 代碼:
#include "stdafx.h" #include <iostream> using namespace std; template <class Type> class Loading { //friend Type MaxLoading(Type[],Type,int,int []); //private: public: void Backtrack(int i); int n, //集裝箱數 *x, //當前解 *bestx; //當前最優解 Type *w, //集裝箱重量數組 c, //第一艘輪船的載重量 cw, //當前載重量 bestw, //當前最優載重量 r; //剩余集裝箱重量 }; template <class Type> void Loading <Type>::Backtrack (int i); 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> void Loading <Type>::Backtrack (int i)// 搜索第i層結點 { if (i > n)// 到達葉結點 { if (cw>bestw) { for(int j=1;j<=n;j++) { bestx[j]=x[j];//更新最優解 bestw=cw; } } return; } r-=w[i]; if (cw + w[i] <= c) // 搜索左子樹 { x[i] = 1; cw += w[i]; Backtrack(i+1); cw-=w[i]; } if (cw + r > bestw) { x[i] = 0; // 搜索右子樹 Backtrack(i + 1); } r+=w[i]; } template<class Type> Type MaxLoading(Type w[], Type c, int n, int bestx[])//返回最優載重量 { Loading<Type>X; //初始化X X.x=new int[n+1]; X.w=w; X.c=c; X.n=n; X.bestx=bestx; X.bestw=0; X.cw=0; //初始化r X.r=0; for (int i=1;i<=n;i++) { X.r+=w[i]; } X.Backtrack(1); delete []X.x; return X.bestw; }
#include "stdafx.h" #include <iostream> using namespace std; template <class Type> class Loading { //friend Type MaxLoading(Type[],Type,int,int []); //private: public: void Backtrack(int i); int n, //集裝箱數 *x, //當前解 *bestx; //當前最優解 Type *w, //集裝箱重量數組 c, //第一艘輪船的載重量 cw, //當前載重量 bestw, //當前最優載重量 r; //剩余集裝箱重量 }; template <class Type> void Loading <Type>::Backtrack (int i); 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> void Loading <Type>::Backtrack (int i)// 搜索第i層結點 { if (i > n)// 到達葉結點 { if (cw>bestw) { for(int j=1;j<=n;j++) { bestx[j]=x[j];//更新最優解 bestw=cw; } } return; } r-=w[i]; if (cw + w[i] <= c) // 搜索左子樹 { x[i] = 1; cw += w[i]; Backtrack(i+1); cw-=w[i]; } if (cw + r > bestw) { x[i] = 0; // 搜索右子樹 Backtrack(i + 1); } r+=w[i]; } template<class Type> Type MaxLoading(Type w[], Type c, int n, int bestx[])//返回最優載重量 { Loading<Type>X; //初始化X X.x=new int[n+1]; X.w=w; X.c=c; X.n=n; X.bestx=bestx; X.bestw=0; X.cw=0; //初始化r X.r=0; for (int i=1;i<=n;i++) { X.r+=w[i]; } X.Backtrack(1); delete []X.x; return X.bestw; }
-
0-1背包問題:有n件物品和一個容量為c的背包。第i件物品的價值是v[i],重量是w[i]。求解將哪些物品裝入背包可使價值總和最大。所謂01背包,表示每一個物品只有一個,要么裝入,要么不裝入。
- 解題思路:這個問題存在一種最有解,用回溯法也可以解出來。他的類型應該是子集樹。要么選入,要么不選入。在搜索狀態空間樹時,只要左子節點是可一個可行結點,搜索就進入其左子樹。對於右子樹時,先計算上界函數,以判斷是否將其減去,剪枝啦啦!
上界函數bound():當前價值cw+剩余容量可容納的最大價值<=當前最優價值bestp。
為了更好地計算和運用上界函數剪枝,選擇先將物品按照其單位重量價值從大到小排序,此后就按照順序考慮各個物品。 - 代碼:
#include<iostream> #include<vector> #include<cstdlib> using namespace std; struct Node { int w; int p; }; vector<Node> v;//物品 vector<int> x;//當前方案 vector<int> bestx;//存儲最佳方案 int num;//物品數 int c;//背包容量 int maxP;//最大價值 int random(int start,int end) { return start+rand()%(end-start); } void storage(int cp) { if(cp>maxP) { maxP=cp; for(int i=0;i<num;i++) { bestx[i]=x[i]; } } } void knapsack(int cw,int t,int cp) { if(t>=num) { return; } else { if(cw<=c) { for(int i=0;i<=1;i++) { x[t]=i; if(i==1) { cw+=v[t].w; cp+=v[t].p; } if(cw<=c) { storage(cp); knapsack(cw,t+1,cp); } } } } } int main() { maxP=-1; cin>>num>>c; for(int i=0;i<num;i++) { Node temp; temp.w=random(1,20); temp.p=random(1,100); v.push_back(temp); x.push_back(0); bestx.push_back(0); } knapsack(0,0,0); cout<<maxP<<endl; }
- 解題思路:這個問題存在一種最有解,用回溯法也可以解出來。他的類型應該是子集樹。要么選入,要么不選入。在搜索狀態空間樹時,只要左子節點是可一個可行結點,搜索就進入其左子樹。對於右子樹時,先計算上界函數,以判斷是否將其減去,剪枝啦啦!
- 旅行售貨員問題:某售貨員要到若干城市去推銷商品,已知各城市之間的路程(旅費),他要選定一條從駐地出發,經過每個城市一遍,最后回到駐地的路線,使總的路程(總旅費)最小。
- 解題思路:這個題目分析之后他的解空間是一個排列數,如圖所示。
- 代碼:
#include<iostream> #include<vector> #include<climits> #include<algorithm> #include<cstdlib> using namespace std; vector< vector<int> > v; vector<int> x; int num; int costbest; int random(int s,int e) { return s+rand()%(e-s); } int countDis() { int cost=0; for(int i=1;i<x.size();i++) { cost+=v[x[i-1]][x[i]]; } return cost+v[x[0]][x[x.size()-1]]; } void Bttsp(int firstcity,int t) { if(t>=num) { int costx=countDis(); if(costx<costbest) { costbest=costx; } } else { for(int i=t;i<num;i++) { if(x[0]==firstcity) { swap(x[t],x[i]); Bttsp(firstcity,t+1); swap(x[t],x[i]); } } } } int main() { costbest=INT_MAX; cin>>num; for(int i=0;i<num;i++) { vector<int> temp; for(int j=0;j<num;j++) { temp.push_back(0); } v.push_back(temp); x.push_back(i); } for(int i=0;i<num;i++) { for(int j=i+1;j<num;j++) { int temp=random(1,50); v[i][j]=temp; v[j][i]=temp; cout<<"v["<<i<<"]["<<j<<"]="<<temp<<endl; } } Bttsp(0,0); cout<<costbest<<endl; }
- 類似的還有圓排列問題。
- 裝載問題:有一批共n個集裝箱要裝上2艘載重量分別為c1和c2的輪船,其中集裝箱i的重量為wi,且
- LeetCode實例分析
-
Combinations:Given two integers n and k,return all possible combinations of k numbersout of 1 ... n. For example, If n = 4 and k =2, a solution is: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
- 題目分析:給你兩個整數 n和k,從1-n中選擇k個數字的組合。比如n=4,那么從1,2,3,4中選取兩個數字的組合,包括圖上所述的四種。leetcode給出的算法框架:
class Solution {
public:
vector<vector<int> > combine(int n, int k) {
}
};
他所返回的是一個二維數組,用來存儲所有組合。這一類問題的套路教學:
現在進行套路教學:要求返回vector<vector<int> >,那我就給你一個vector<vector<int> >,因此 (1) 定義一個全局vector<vector<int> > result; (2) 定義一個輔助的方法(函數)void backtracking(int n,int k, vector<int>){} n k 總是要有的吧,加上這兩個參數,前面提到vector<int>是數字的組合,也是需要的吧,這三個是必須的,沒問題吧。(可以嘗試性地寫參數,最后不需要的刪除)
(3) 接着就是我們的重頭戲了,如何實現這個算法?對於n=4,k=2,1,2,3,4中選2個數字,我們可以做如下嘗試,加入先選擇1,那我們只需要再選擇一個數字,注意這時候k=1了
(此時只需要選擇1個數字啦)。當然,我們也可以先選擇2,3 或者4,通俗化一點,我們可以選擇(1-n)的所有數字,這個是可以用一個循環來描述?每次選擇一個加入我們的鏈表list中,
下一次只要再選擇k-1個數字。那什么時候結束呢?當然是k<0的時候啦,這時候都選完了 - 完整代碼:
class Solution { public: vector<vector<int> > res; vector<vector<int> > combine(int n, int k) { //vector<vector<int> > res; vector<int> temp; backtrack(n, k, temp, 1); return res; } void backtrack(int n, int k, vector<int> temp, int start){ if (k < 0) return ; else if (k == 0){//所有的找完之后將這個數組進入二維數組中 res.push_back(temp); } else{ for (int i=start; i<=n; i++){ temp.push_back(i); backtrack(n, k-1, temp, i+1);//回溯法找到滿足的元素 temp.pop_back();//回溯,彈出上一個滿足的元素 } } } };
- 題目分析:給你兩個整數 n和k,從1-n中選擇k個數字的組合。比如n=4,那么從1,2,3,4中選取兩個數字的組合,包括圖上所述的四種。leetcode給出的算法框架:
- 實例分析二:給你一個正數數組candidate[],一個目標值target,尋找里面所有的不重復組合,讓其和等於target,給你[2,3,6,7] 2+2+3=7 ,7=7,所以可能組合為[2,2,3],[7])
- 套路實現:基本框架
class Solution { public: vector<vector<int> > res; vector<vector<int> > combinationSum(vector<int> &candidates, int target) { int size = candidates.size(); vector<int> temp; backtrack(candidates, terget, temp, 1); } void backtrack(vector<int> &candidates, int target, int temp, int){ } };
- 然后就是分析算法了:對於所有可能的組合。我們回溯的進行記錄,首先拿出一個數num,target就變成target-num,然后再從所有的數中挑出可以的數字。極限條件是target<0,輸出結果條件是target=0;由於這個數字的選取是可以重復的所以下一個數也是重當前位置開始,但是不能使重復的排列,所以還必須有start參數,記錄開始的位置多次從頭開始遍歷循環。
- 代碼實現:
class Solution { public: vector<vector<int> > res; vector<vector<int> > combinationSum(vector<int> &candidates, int target) { vector<vector<int> > res; sort(candidates.begin(),candidates.end()); vector<int> temp; backtrack(res,candidates,temp,target,0); return res; } void backtrack(vector<vector<int> > &res,vector<int> &candidates, vector<int> &temp,int target,int start){ if (target < 0) return; else if (target == 0){ res.push_back(temp); //return ; } else{ for(int i=start;i<candidates.size();i++){ temp.push_back(candidates[i]); backtrack(res,candidates,temp,target-candidates[i],i); temp.pop_back(); } } } };
- 套路實現:基本框架
-