算法分析與設計:分支限界法

分支限界法

1. 基本思想

分支是使用廣度優先策略,依次生成擴展結點的所有分支
限界是在結點擴展過程中,計算結點的上界,搜索的同時剪掉某些分支
分支限界法就是把問題的可行解展開,再由各個分支尋找最佳解。
與回溯法類似,分支限界法也是在解空間中搜索得到解;
不同的是,分支限界法會生成所有擴展結點,並捨棄不可能通向最優解的結點,然後根據廣度優先/最小耗費優先,從活結點中選擇一個作爲擴展結點,使搜索向解空間上有最優解的分支推進。

2. 搜索策略

分支限界法首先生成當前擴展結點的所有分支,然後再從所有活結點中選擇一個作爲擴展結點。每一個活結點都要計算限界,根據限界情況判斷是否剪枝,或選擇最有利的結點。
分支限界法有兩種不同的搜索空間樹方式,分別爲廣度優先最小耗費優先,它們對應兩種不同的方法:

  1. 隊列式分支限界法(FIFO)
    常規的廣度優先策略。按照先進先出的原則選取下一個擴展結點,以隊列儲存活結點。
  2. 優先隊列式分支限界法/最小耗費優先分支限界法(LC)
    按照優先隊列中指定的優先級,選取優先級最高的結點作爲下一個擴展結點,以優先隊列儲存。

分支限界法的具體搜索策略如下:

  1. 根結點入隊;
  2. 根據使用的方法(FIFO或LC),令一個活結點出隊,作爲擴展結點
  3. 對擴展結點,生成所有的分支;使用約束條件捨棄不可行的結點/不可能爲最優解的結點,剩餘的結點入隊;
  4. 重複2和3,直到找到要求的解隊列爲空
方法 搜索策略 存儲結點常用結構 結點存儲特性 應用問題
回溯法 深度優先 結點可以多次成爲擴展結點,所有可行子結點都遍歷後才彈出 找出滿足條件的所有解
分支限界法 廣度/LC優先 隊列/優先隊列 結點只能成爲一次擴展結點,剪枝或擴展後立刻出隊 找出條件下的某個/最優解

3. 分支結點選擇

所有界限滿足上界/下界的結點都可以作爲擴展結點。因此,必須有一個分支選擇策略,FIFO法和LC法對應兩種策略:
● 按順序選擇結點作爲下一次的擴展結點。優點是節省空間,缺點是需要計算的分支數較多,時間花費大;
● 每次計算完限界後,找出限界最優的結點,作爲下一次的擴展結點。優點是計算的分支數少,缺點是需要額外空間。

4. 限界函數

限界函數很大程度上決定了算法的效率。同一問題可以設計不同的限界函數。
FIFO分支限界法中,常以約束條件作爲限界函數,滿足約束條件纔可入隊,不滿足約束條件的捨棄。
LC分支限界法中,還可以設計一個啓發函數作爲限界函數。
對於有約束的問題,FIFO法和LC法均可以求解;對於無約束問題, 宜使用LC法。

例題:單源最短路徑

1. 問題描述

給定帶權有向圖G,每邊的權值是一個正實數,表示點到點的路徑距離。給定圖中的一個源點V,求圖G中所有點到源點V的最短路徑。
帶權有向圖

2. 問題分析

除了用Dijkstra算法(貪心)解決該問題外,也可以使用分支限界法。由於要求的是最短的路徑,我們考慮使用優先隊列式分支限界法,以減少計算的分支數。顯然,我們的限界就是源到目的點的路徑長度:若源到同一個頂點有多條路徑,將長路徑的分支全部捨棄,而保存更短路徑的分支。並且由於題目的貪心選擇性質,每次從優先隊列中取最短路徑,最終得到的解也必然是最優的。

爲了避免出隊列可能造成的異常,並能更有規律地處理優先隊列,我們爲最小堆構造一個長度等同於頂點個數的結點數組。數組元素的下標對應頂點的編號;數組元素的編號爲-1時,代表該結點被刪除(出隊列)。
解空間樹

3. 算法設計

  1. 生成根節點的所有分支,全部入隊列並記錄路徑;
  2. 在隊列中選擇路徑最短的分支作爲擴展結點
  3. 逐個生成分支,並判斷分支的路徑是否小於記錄的最短路徑;
  4. 若不小於,捨棄該分支;
  5. 若小於,該分支入隊列;
  6. 生成所有分支後,回到2;
  7. 當隊列爲空時,算法結束。

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)

分支限界法的套路單一,就只寫一道例題了,怎麼可能是因爲這兩天沉迷騎砍呢😀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章