分支限界法
1. 基本思想
分支是使用廣度優先策略,依次生成擴展結點的所有分支。
限界是在結點擴展過程中,計算結點的上界,搜索的同時剪掉某些分支。
分支限界法就是把問題的可行解展開,再由各個分支尋找最佳解。
與回溯法類似,分支限界法也是在解空間中搜索得到解;
不同的是,分支限界法會生成所有擴展結點,並捨棄不可能通向最優解的結點,然後根據廣度優先/最小耗費優先,從活結點中選擇一個作爲擴展結點,使搜索向解空間上有最優解的分支推進。
2. 搜索策略
分支限界法首先生成當前擴展結點的所有分支,然後再從所有活結點中選擇一個作爲擴展結點。每一個活結點都要計算限界,根據限界情況判斷是否剪枝,或選擇最有利的結點。
分支限界法有兩種不同的搜索空間樹方式,分別爲廣度優先和最小耗費優先,它們對應兩種不同的方法:
- 隊列式分支限界法(FIFO)
常規的廣度優先策略。按照先進先出的原則選取下一個擴展結點,以隊列儲存活結點。 - 優先隊列式分支限界法/最小耗費優先分支限界法(LC)
按照優先隊列中指定的優先級,選取優先級最高的結點作爲下一個擴展結點,以優先隊列儲存。
分支限界法的具體搜索策略如下:
- 根結點入隊;
- 根據使用的方法(FIFO或LC),令一個活結點出隊,作爲擴展結點;
- 對擴展結點,生成所有的分支;使用約束條件捨棄不可行的結點/不可能爲最優解的結點,剩餘的結點入隊;
- 重複2和3,直到找到要求的解或隊列爲空。
方法 | 搜索策略 | 存儲結點常用結構 | 結點存儲特性 | 應用問題 |
---|---|---|---|---|
回溯法 | 深度優先 | 棧 | 結點可以多次成爲擴展結點,所有可行子結點都遍歷後才彈出 | 找出滿足條件的所有解 |
分支限界法 | 廣度/LC優先 | 隊列/優先隊列 | 結點只能成爲一次擴展結點,剪枝或擴展後立刻出隊 | 找出條件下的某個/最優解 |
3. 分支結點選擇
所有界限滿足上界/下界的結點都可以作爲擴展結點。因此,必須有一個分支選擇策略,FIFO法和LC法對應兩種策略:
● 按順序選擇結點作爲下一次的擴展結點。優點是節省空間,缺點是需要計算的分支數較多,時間花費大;
● 每次計算完限界後,找出限界最優的結點,作爲下一次的擴展結點。優點是計算的分支數少,缺點是需要額外空間。
4. 限界函數
限界函數很大程度上決定了算法的效率。同一問題可以設計不同的限界函數。
FIFO分支限界法中,常以約束條件作爲限界函數,滿足約束條件纔可入隊,不滿足約束條件的捨棄。
LC分支限界法中,還可以設計一個啓發函數作爲限界函數。
對於有約束的問題,FIFO法和LC法均可以求解;對於無約束問題, 宜使用LC法。
例題:單源最短路徑
1. 問題描述
給定帶權有向圖G,每邊的權值是一個正實數,表示點到點的路徑距離。給定圖中的一個源點V,求圖G中所有點到源點V的最短路徑。
2. 問題分析
除了用Dijkstra算法(貪心)解決該問題外,也可以使用分支限界法。由於要求的是最短的路徑,我們考慮使用優先隊列式分支限界法,以減少計算的分支數。顯然,我們的限界就是源到目的點的路徑長度:若源到同一個頂點有多條路徑,將長路徑的分支全部捨棄,而保存更短路徑的分支。並且由於題目的貪心選擇性質,每次從優先隊列中取最短路徑,最終得到的解也必然是最優的。
爲了避免出隊列可能造成的異常,並能更有規律地處理優先隊列,我們爲最小堆構造一個長度等同於頂點個數的結點數組。數組元素的下標對應頂點的編號;數組元素的編號爲-1時,代表該結點被刪除(出隊列)。
3. 算法設計
- 生成根節點的所有分支,全部入隊列並記錄路徑;
- 在隊列中選擇路徑最短的分支作爲擴展結點
- 逐個生成分支,並判斷分支的路徑是否小於記錄的最短路徑;
- 若不小於,捨棄該分支;
- 若小於,該分支入隊列;
- 生成所有分支後,回到2;
- 當隊列爲空時,算法結束。
4. 算法實現
//單源最短路徑
class Graph{ //帶權有向圖
private:
int n; //頂點個數
int **c; //鄰接矩陣
int *dist; //記錄路徑
public:
void shortestPaths(int);
Graph(); //根據情況構造圖
};
class MinHeapNode{ //最小堆的結點
friend Graph;
private:
int i; //結點對應的頂點編號
int length; //結點記錄的最短路徑
public:
int getI(){ return i; }
void setI(int i){ this->i = i; }
int getLength(){ return length; }
void setLength(int length){ this->length = length; }
};
class MinHeap{ //最小堆(雖然叫堆,但其實並不是用堆實現的)
friend Graph;
private:
int length; //最小堆的長度,等同於頂點個數
MinHeapNode *nodes; //結點數組
public:
MinHeap(int n)
{
this->length = n;
nodes = new MinHeapNode[n];
}
void deleteMin(MinHeapNode&); //令當前節點出隊列,並給出下一個擴展結點
void insertNode(MinHeapNode N) //結點入隊列,將原結點的內容替換即可
{
nodes[N.getI()].setI(N.getI());
nodes[N.getI()].setLength(N.getLength());
}
bool outOfBounds() //檢查隊列爲空
{
for(int i = 0;i < length;i++)
if(nodes[i].getI() != -1)
return false;
return true;
}
};
void MinHeap::deleteMin(MinHeapNode &E)
{
int j = E.getI();
nodes[j].setI(-1);
nodes[j].setLength(-1); //標記爲出隊列
int tmp = INT_MAX;
for(int i = 0;i < length;i++){ //給出路徑最短的結點作爲擴展結點
if(nodes[i].getI() != -1 && nodes[i].getLength() < tmp){
E.setI(i);
E.setLength(nodes[i].getLength());
tmp = nodes[i].getLength();
}
}
}
void Graph::shortestPaths(int start)
{
MinHeap heap = MinHeap(n);
MinHeapNode E = MinHeapNode(); //別問,一開始還加了new,太久不寫C++了
E.i = start;
E.length = 0;
dist[start] = 0; //初始爲源點V,對應編號start
while(true){
for(int j = 0;j < n;j++){ //檢查所有鄰接頂點
if(c[E.i][j] != 0){ //是否鄰接
if(E.length + c[E.i][j] < dist[j]){ //是否滿足限界,當前路徑小於記錄的最短路徑
dist[j] = E.length + c[E.i][j]; //更新
if(/*填入判斷表達式*/){ //沒有鄰接頂點的點不入隊,按情況調整,沒有也可以,但會造成無效開銷
MinHeapNode N = MinHeapNode(); //創建一個新的結點,並令其入隊列
N.i = j;
N.length = dist[j];
heap.insertNode(N);
}
}
}
}
if(heap.outOfBounds()) //隊列爲空,結束
break;
heap.deleteMin(E); //該結點已經生成全部分支,出隊列並取得下一擴展結點
}
}
爲使隊列爲空,while循環總共需要取n個結點;每個結點要對所有結點都進行檢查。因此算法的時間複雜度爲O(n2)。
分支限界法的套路單一,就只寫一道例題了,怎麼可能是因爲這兩天沉迷騎砍呢😀