回溯法(DFS)
一、基本概念
回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。
回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
二、基本思想
在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。
若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。
回溯法是一種“ 能進則進,進不了則換,換不了則退”的搜索方法。
三、解題步驟
(1)針對所給問題,確定問題的解空間:首先應明確定義問題的解空間,問題的解空間應至少包含問題的一個(最優)解;
(2)確定結點的擴展搜索規則;
(3)以深度優先方式搜索解空間,並在搜索過程中用剪枝函數避免無效搜索。剪枝函數包括約束函數和限界函數。對能否得到問題的可行解的約束稱爲約束函數,對能否得到最優解的約束稱爲限界函數。
四、算法框架
(1)問題框架
設問題的解是一個n維向量(a1,a2,………,an),約束條件是ai(i=1,2,3,…..,n)之間滿足某種條件,記爲f(ai)。
(2)非遞歸回溯框架
int a[n],i;
初始化數組a[];
i = 1;
while(i>0(有路可走) and (未達到目標)) //還未回溯到頭
{
if(i > n) //搜索到葉結點
{
搜索到一個解,輸出;
}
else //處理第i個元素
{
a[i]第一個可能的值;
while(a[i]在不滿足約束條件且在搜索空間內)
{
a[i]下一個可能的值;
}
if(a[i]在搜索空間內)
{
標識佔用的資源;
i = i+1; //擴展下一個結點
}
else
{
清理所佔的狀態空間; //回溯
i = i–1;
}
}
}
(3)遞歸的算法框架
int a[n];
Backtrack(int i)
{
if(i>n)
輸出結果;
else
{
for(j =下界; j <= 上界; j=j+1) //枚舉i所有可能的路徑
}
if(fun(j)) //滿足限界函數和約束條件
{
a[i] = j;
... //其他操作
Backtrack(i+1);
回溯前的清理工作(如a[i]置空值等);
}
}
五、經典例子
n皇后問題
問題描述:
在 n×n 的棋盤上放置彼此不受攻擊的 n 個皇后。按照國際象棋的規則,皇后可以攻擊與之在同一行、同一列、同一斜線上的棋子。設計算法在 n×n 的棋盤上放置 n 個皇后,使其彼此不受攻擊。
僞代碼詳解
(1)約束函數
bool Place(int t) //判斷第 t 個皇后能否放置在第 i 個位置
{
bool ok=true;
for(int j=1;j<t;j++) //判斷該位置的皇后是否與前面 t-1 個已經放置的皇后衝突
{
if(x[t]==x[j]||t-j==fabs(x[t]-x[j]))//判斷列、對角線是否衝突
{
ok=false;
break;
}
}
return ok;
}
(2)按約束條件搜索求解
void Backtrack(int t)
{
if(t>n) //如果當前位置爲 n,則表示已經找到了問題的一個解
{
countn++;
for(int i=1; i<=n;i++) //打印選擇的路徑
cout<<x[i]<<" ";
cout<<endl;
cout<<"----------"<<endl;
}
else
for(int i=1;i<=n;i++) //分別判斷 n 個分支,特別注意 i 不要定義爲全局變量,否則 遞歸調用有問題
{
x[t]=i;
if(Place(t))
Backtrack(t+1); //如果不衝突的話進行下一行的搜索
}
}
旅行商問題
問題描述:
旅行商有 n 個 想去的景點,已知兩個景點之間的距離 dij,爲了節省時間,希望在最短的時間內看遍所有的景點,而且同一個景點只經過一次。怎麼計劃行程,才能在最短的時間內不重複地旅遊完所有景點回到家呢?
和n皇后問題不同,旅行商問題不僅要找可行解,還要找最優解。
void Travelingdfs(int t)
{
if(t>n)
{ //到達葉子結點
//推銷貨物的最後一個城市與住地城市有邊相連並且路徑長度比當前最優值小
//說明找到了一條更好的路徑,記錄相關信息
if(g[x[n]][1]!=INF && (cl+g[x[n]][1]<bestl))
{
for(int j=1;j<=n;j++)
bestx[j]=x[j];
bestl=cl+g[x[n]][1];
}
}
else
{
//沒有到達葉子結點
for(int j=t; j<=n; j++)
{
//搜索擴展結點的所有分支
//如果第t-1個城市與第j個城市有邊相連並且有可能得到更短的路線
if(g[x[t-1]][x[j]]!=INF&&(cl+g[x[t-1]][x[j]]<bestl))
{
//保存第t個要去的城市編號到x[t]中,進入到第t+1層
swap(x[t], x[j]);//交換兩個元素的值
cl=cl+g[x[t-1]][x[t]];
Traveling(t+1); //從第t+1曾的擴展結點繼續搜索
//第t+1層搜索完畢,回溯到第t層
cl=cl-g[x[t-1]][x[t]];
swap(x[t], x[j]);
}
}
}
}
分支限界法(BFS)
一、基本概念
類似於回溯法,也是一種在問題的解空間樹T上搜索問題解的算法,但在一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出T中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函數值達到極大或極小的解,即在某種意義下的最優解。
二、一般過程
由於求解目標不同,導致分支限界法與回溯法在解空間樹T上的搜索方式也不相同。回溯法以深度優先的方式搜索解空間樹T,而分支限界法則以廣度優先或以最小耗費優先的方式搜索解空間樹T。
分支限界法的搜索策略:在擴展結點處,先生成其所有的兒子結點(分支),然後再從當前的活結點表中選擇下一個擴展對點。爲了有效地選擇下一擴展結點,以加速搜索的進程,在每一活結點處,計算一個函數值(限界),並根據這些已計算出的函數值,從當前活結點表中選擇一個最有利的結點作爲擴展結點,使搜索朝着解空間樹上有最優解的分支推進,以便儘快地找出一個最優解。
分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜索問題的解空間樹。問題的解空間樹是表示問題解空間的一棵有序樹,常見的有子集樹和排列樹。在搜索問題的解空間樹時,分支限界法與回溯法對當前擴展結點所使用的擴展方式不同。在分支限界法中,每一個活結點只有一次機會成爲擴展結點。活結點一旦成爲擴展結點,就一次性產生其所有兒子結點。在這些兒子結點中,那些導致不可行解或導致非最優解的兒子結點被捨棄,其餘兒子結點被子加入活結點表中。此後,從活結點表中取下一結點成爲當前擴展結點,並重覆上述結點擴展過程。這個過程一直持續到找到所求的解或活結點表爲空時爲止。
三、回溯法和分支限界法的區別
回溯法深度優先搜索堆棧活結點的所有可行子結點被遍歷後才被從棧中彈出找出滿足約束條件的所有解。
分支限界法廣度優先或最小消耗優先搜索隊列、優先隊列每個結點只有一次成爲活結點的機會找出滿足約束條件的一個解或特定意義下的最優解。
四、常見的兩種分支限界法
(1)隊列式(FIFO)分支限界法。按照隊列先進先出(FIFO)原則選取下一個節點爲擴展節點。
(2)優先隊列式分支限界法。按照優先隊列中規定的優先級選取優先級最高的節點成爲當前擴展節點。
五、算法框架
這裏以優先隊列式分支限界法爲例:
void priorbfs()
{
priority_queue<Node> q; //創建一個優先隊列;
while (!q.empty())
{
livenode = q.top();
q.pop();
if() //判斷是否更新最優解;
{
//更新,記錄最優解;
}
搜索拓展結點的所有分支;
定義新結點 newnode;
q.push(newnode);
}
返回最優解;
}
六、經典例子
旅行商問題
struct Node//定義結點,記錄當前結點的解信息
{
double cl; //當前已經走過的路勁長度
int id; //景點序號
int x[N];//記錄當前路徑
Node() {}
Node(double _cl,int _id)
{
cl = _cl;
id = _id;
}
};
bool operator <(const Node &a, const Node &b)
{
return a.cl>b.cl;
}
數組g中存了景點地圖鄰接矩陣
double Travelingbfs()
{
int t; //當前景點序號t
Node livenode,newnode;
priority_queue<Node> q;
newnode=Node(0,2);
for(int i=1;i<=n;i++)
{
newnode.x[i]=i;
}
q.push(newnode);
while(!q.empty())
{
livenode=q.top();//取出隊頭元素作爲擴展結點livenode
q.pop(); //隊頭元素出隊
t=livenode.id;//當前處理的景點序號
if(t==n) //判斷是否更新最優解
{
if(g[livenode.x[n-1]][livenode.x[n]]!=INF&&g[livenode.x[n]][1]!=INF)
if(livenode.cl+g[livenode.x[n-1]][livenode.x[n]]+g[livenode.x[n]][1]<bestl)
{
bestl=livenode.cl+g[livenode.x[n-]][livenode.x[n]]+g[livenode.x[n]][1];
for(int i=1;i<=n;i++)
{
bestx[i]=livenode.x[i];
}
}
continue;
}
if(livenode.cl>=bestl)
continue;
//擴展
for(int j=t; j<=n; j++)
{
if(g[livenode.x[t-1]][livenode.x[j]]!=INF)
{
double cl=livenode.cl+g[livenode.x[t-1]][livenode.x[j]];
if(cl<bestl)
{
newnode=Node(cl,t+1);
for(int i=1;i<=n;i++)
{
newnode.x[i]=livenode.x[i];
}
swap(newnode.x[t], newnode.x[j]);
q.push(newnode);
}
}
}
}
return bestl;
}
0-1揹包問題
struct Node
{
int cp; //裝入購物車的物品價值
double up; //價值上限
int rw; //揹包剩餘容量
int id; //物品號
bool x[N];
Node() {}
Node(int _cp, double _up, int _rw, int _id)
{
cp = _cp;
up = _up;
rw = _rw;
id = _id;
memset(x, 0, sizeof(x));
}
};
double Bound(Node tnode)
{
double maxvalue=tnode.cp;//已裝入購物車物品價值
int t=tnode.id;//排序號序號
double left=tnode.rw;//剩餘容量
while(t<=n && w[t]<=left)
{
maxvalue+=v[t];
left-=w[t];
t++;
}
if(t<=n)
maxvalue+=double(v[t])/w[t]*left;
return maxvalue;
}
int priorbfs()
{
int t,tcp,trw;
double tup; //t是當前的意思,tup是當前購入購物車物品價值上限
priority_queue<Node> q;
q.push(Node(0, sumv, W, 1));
while(!q.empty())
{
Node livenode, lchild, rchild;//定義3個結點型變量
livenode=q.top();
q.pop();
for(int i=1; i<=n; i++)
{
cout<<livenode.x[i];
}
t=livenode.id;
if(t>n||livenode.rw==0)
{
if(livenode.cp>=bestp)
{
for(int i=1; i<=n; i++)
{
bestx[i]=livenode.x[i];
}
bestp=livenode.cp;
}
continue;
}
if(livenode.up<bestp)
continue;
tcp=livenode.cp;
trw=livenode.rw;
if(trw>=w[t])
{
lchild.cp=tcp+v[t];
lchild.rw=trw-w[t];
lchild.id=t+1;
tup=Bound(lchild);
lchild=Node(lchild.cp,tup,lchild.rw,lchild.id);
for(int i=1;i<=n;i++)
{
lchild.x[i]=livenode.x[i];
}
lchild.x[t]=true;
if(lchild.cp>bestp)
bestp=lchild.cp;
q.push(lchild);
}
rchild.cp=tcp;
rchild.rw=trw;
rchild.id=t+1;
tup=Bound(rchild);
if(tup>=bestp)
{
rchild=Node(tcp,tup,trw,t+1);
for(int i=1;i<=n;i++)
{
rchild.x[i]=livenode.x[i];
}
rchild.x[t]=false;
q.push(rchild);
}
}
return bestp;
}
回溯法與分支限界法的異同
陳小玉老師的算法裏面的總結:
1. 相同點
(1)均需要先定義問題的解空間,確定的解空間組織結構一般是樹或圖。
(2)在問題的解空間樹上搜索問題解。
(3)搜索前均需要確定判定條件,該判斷條件用於判斷擴展生成的結點是否爲可行結點。
(4)搜索過程中必須判斷擴展生成的結點是否滿足判斷條件,如果滿足,則保留該擴展生成的結點,否則捨棄。
2. 不同點
(1)搜索目標:回溯法的求解目標是找出解空間樹中滿足約束條件的所有解,而分支限界法的求解目標是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出的在某種意義下的最優解。
(2)搜索方式不同:回溯法以深度優先的方式搜索解空間樹,而分支限界法是以廣度優先或以最小耗費優先的方式。
(3)擴展方式不同:在回溯法搜索中,擴展結點一次生成一個孩子結點,而在分支限界法搜索中,擴展結點一次生成所有的孩子結點