演算法與資料結構之帶權圖與圖最小生成樹

吳軍旗發表於2018-07-08

主要介紹有權圖、最小生成樹問題和切分定理、Prim演算法、Krusk演算法等

帶權圖 Weighted Graph

邊上都有自己的權值的帶權圖。

演算法與資料結構之帶權圖與圖最小生成樹

鄰接矩陣表示有權圖

  • 把原來的1/0變成權值。

演算法與資料結構之帶權圖與圖最小生成樹

鄰接表的改造

  • 鄰接表的點變成鍵值對。節點的索引:權值。封裝成Edge類
  • 為了讓鄰接表和鄰接矩陣有一個統一的介面:Edge。i到j的。
  • 沒有邊的地方用空。edge中儲存一個指標。

演算法與資料結構之帶權圖與圖最小生成樹

edge類程式碼實現

// 邊
template<typename Weight>
class Edge{
private:
    int a,b;    // 邊的兩個端點
    Weight weight;  // 邊的權值

public:
    // 建構函式
    Edge(int a, int b, Weight weight){
        this->a = a;
        this->b = b;
        this->weight = weight;
    }
    // 空的建構函式, 所有的成員變數都取預設值
    Edge(){}

    ~Edge(){}

    int v(){ return a;} // 返回第一個頂點
    int w(){ return b;} // 返回第二個頂點
    Weight wt(){ return weight;}    // 返回權值

    // 給定一個頂點, 返回另一個頂點
    int other(int x){
        assert( x == a || x == b );
        return x == a ? b : a;
    }

    // 輸出邊的資訊
    friend ostream& operator<<(ostream &os, const Edge &e){
        os<<e.a<<"-"<<e.b<<": "<<e.weight;
        return os;
    }

    // 邊的大小比較, 是對邊的權值的大小比較
    bool operator<(Edge<Weight>& e){
        return weight < e.wt();
    }
    bool operator<=(Edge<Weight>& e){
        return weight <= e.wt();
    }
    bool operator>(Edge<Weight>& e){
        return weight > e.wt();
    }
    bool operator>=(Edge<Weight>& e){
        return weight >= e.wt();
    }
    bool operator==(Edge<Weight>& e){
        return weight == e.wt();
    }
};
複製程式碼

稠密有權圖程式碼實現

// 稠密圖 - 鄰接矩陣
template <typename Weight>
class DenseGraph{

private:
    int n, m;       // 節點數和邊數
    bool directed;  // 是否為有向圖
    vector<vector<Edge<Weight> *>> g;   // 圖的具體資料

public:
    // 建構函式
    DenseGraph( int n , bool directed){
        assert( n >= 0 );
        this->n = n;
        this->m = 0;
        this->directed = directed;
        // g初始化為n*n的矩陣, 每一個g[i][j]指向一個邊的資訊, 初始化為NULL
        g = vector<vector<Edge<Weight> *>>(n, vector<Edge<Weight> *>(n, NULL));
    }

    // 解構函式
    ~DenseGraph(){

        for( int i = 0 ; i < n ; i ++ )
            for( int j = 0 ; j < n ; j ++ )
                if( g[i][j] != NULL )
                    delete g[i][j];
    }

    int V(){ return n;} // 返回節點個數
    int E(){ return m;} // 返回邊的個數

    // 向圖中新增一個邊, 權值為weight
    void addEdge( int v, int w , Weight weight ){
        assert( v >= 0 && v < n );
        assert( w >= 0 && w < n );

        // 如果從v到w已經有邊, 刪除這條邊
        if( hasEdge( v , w  ) ){
            delete  g[v][w];
            if( v != w && !directed )
                delete g[w][v];
            m --;
        }

        g[v][w] = new Edge<Weight>(v, w, weight);
        if( v != w && !directed )
            g[w][v] = new Edge<Weight>(w, v, weight);
        m ++;
    }

    // 驗證圖中是否有從v到w的邊
    bool hasEdge( int v , int w ){
        assert( v >= 0 && v < n );
        assert( w >= 0 && w < n );
        return g[v][w] != NULL;
    }

    // 顯示圖的資訊
    void show(){

        for( int i = 0 ; i < n ; i ++ ){
            for( int j = 0 ; j < n ; j ++ )
                if( g[i][j] )
                    cout<<g[i][j]->wt()<<"\t";
                else
                    cout<<"NULL\t";
            cout<<endl;
        }
    }

    // 鄰邊迭代器, 傳入一個圖和一個頂點,
    // 迭代在這個圖中和這個頂點向連的所有邊
    class adjIterator{
    private:
        DenseGraph &G;  // 圖G的引用
        int v;
        int index;

    public:
        // 建構函式
        adjIterator(DenseGraph &graph, int v): G(graph){
            this->v = v;
            this->index = -1;   // 索引從-1開始, 因為每次遍歷都需要呼叫一次next()
        }

        ~adjIterator(){}

        // 返回圖G中與頂點v相連線的第一個邊
        Edge<Weight>* begin(){
            // 索引從-1開始, 因為每次遍歷都需要呼叫一次next()
            index = -1;
            return next();
        }

        // 返回圖G中與頂點v相連線的下一個邊
        Edge<Weight>* next(){
            // 從當前index開始向後搜尋, 直到找到一個g[v][index]為true
            for( index += 1 ; index < G.V() ; index ++ )
                if( G.g[v][index] )
                    return G.g[v][index];
            // 若沒有頂點和v相連線, 則返回NULL
            return NULL;
        }

        // 檢視是否已經迭代完了圖G中與頂點v相連線的所有邊
        bool end(){
            return index >= G.V();
        }
    };
};
複製程式碼

main.cpp:

// 測試有權圖和有權圖的讀取
int main() {

    string filename = "testG1.txt";
    int V = 8;
    cout<<fixed<<setprecision(2);

    // Test Weighted Dense Graph
    DenseGraph<double> g1 = DenseGraph<double>(V, false);
    ReadGraph<DenseGraph<double>,double> readGraph1(g1, filename);
    g1.show();
    cout<<endl;

    // Test Weighted Sparse Graph
    SparseGraph<double> g2 = SparseGraph<double>(V, false);
    ReadGraph<SparseGraph<double>,double> readGraph2(g2, filename);
    g2.show();
    cout<<endl;

    return 0;
}
複製程式碼

稀疏有權圖程式碼實現

// 稀疏圖 - 鄰接表
template<typename Weight>
class SparseGraph{

private:
    int n, m;       // 節點數和邊數
    bool directed;  // 是否為有向圖
    vector<vector<Edge<Weight> *> > g;   // 圖的具體資料

public:
    // 建構函式
    SparseGraph( int n , bool directed){
        assert(n >= 0);
        this->n = n;
        this->m = 0;    // 初始化沒有任何邊
        this->directed = directed;
        // g初始化為n個空的vector, 表示每一個g[i]都為空, 即沒有任和邊
        g = vector<vector<Edge<Weight> *> >(n, vector<Edge<Weight> *>());
    }

    // 解構函式
    ~SparseGraph(){
        for( int i = 0 ; i < n ; i ++ )
            for( int j = 0 ; j < g[i].size() ; j ++ )
                delete g[i][j];
    }

    int V(){ return n;} // 返回節點個數
    int E(){ return m;} // 返回邊的個數

    // 向圖中新增一個邊, 權值為weight
    void addEdge( int v, int w , Weight weight){
        assert( v >= 0 && v < n );
        assert( w >= 0 && w < n );

        // 注意, 由於在鄰接表的情況, 查詢是否有重邊需要遍歷整個連結串列
        // 我們的程式允許重邊的出現

        g[v].push_back(new Edge<Weight>(v, w, weight));
        if( v != w && !directed )
            g[w].push_back(new Edge<Weight>(w, v, weight));
        m ++;
    }

    // 驗證圖中是否有從v到w的邊
    bool hasEdge( int v , int w ){
        assert( v >= 0 && v < n );
        assert( w >= 0 && w < n );
        for( int i = 0 ; i < g[v].size() ; i ++ )
            if( g[v][i]->other(v) == w )
                return true;
        return false;
    }

    // 顯示圖的資訊
    void show(){

        for( int i = 0 ; i < n ; i ++ ){
            cout<<"vertex "<<i<<":\t";
            for( int j = 0 ; j < g[i].size() ; j ++ )
                cout<<"( to:"<<g[i][j]->w()<<",wt:"<<g[i][j]->wt()<<")\t";
            cout<<endl;
        }
    }

    // 鄰邊迭代器, 傳入一個圖和一個頂點,
    // 迭代在這個圖中和這個頂點向連的所有邊
    class adjIterator{
    private:
        SparseGraph &G; // 圖G的引用
        int v;
        int index;

    public:
        // 建構函式
        adjIterator(SparseGraph &graph, int v): G(graph){
            this->v = v;
            this->index = 0;
        }

        ~adjIterator(){}

        // 返回圖G中與頂點v相連線的第一個邊
        Edge<Weight>* begin(){
            index = 0;
            if( G.g[v].size() )
                return G.g[v][index];
            // 若沒有頂點和v相連線, 則返回NULL
            return NULL;
        }

        // 返回圖G中與頂點v相連線的下一個邊
        Edge<Weight>* next(){
            index += 1;
            if( index < G.g[v].size() )
                return G.g[v][index];
            return NULL;
        }

        // 檢視是否已經迭代完了圖G中與頂點v相連線的所有頂點
        bool end(){
            return index >= G.g[v].size();
        }
    };
};
複製程式碼

執行結果:

NULL	NULL	0.26	NULL	0.38	NULL	0.58	0.16	
NULL	NULL	0.36	0.29	NULL	0.32	NULL	0.19	
0.26	0.36	NULL	0.17	NULL	NULL	0.40	0.34	
NULL	0.29	0.17	NULL	NULL	NULL	0.52	NULL	
0.38	NULL	NULL	NULL	NULL	0.35	0.93	0.37	
NULL	0.32	NULL	NULL	0.35	NULL	NULL	0.28	
0.58	NULL	0.40	0.52	0.93	NULL	NULL	NULL	
0.16	0.19	0.34	NULL	0.37	0.28	NULL	NULL	

vertex 0:	( to:7,wt:0.16)	( to:4,wt:0.38)	( to:2,wt:0.26)	( to:6,wt:0.58)	
vertex 1:	( to:5,wt:0.32)	( to:7,wt:0.19)	( to:2,wt:0.36)	( to:3,wt:0.29)	
vertex 2:	( to:3,wt:0.17)	( to:0,wt:0.26)	( to:1,wt:0.36)	( to:7,wt:0.34)	( to:6,wt:0.40)	
vertex 3:	( to:2,wt:0.17)	( to:1,wt:0.29)	( to:6,wt:0.52)	
vertex 4:	( to:5,wt:0.35)	( to:7,wt:0.37)	( to:0,wt:0.38)	( to:6,wt:0.93)	
vertex 5:	( to:4,wt:0.35)	( to:7,wt:0.28)	( to:1,wt:0.32)	
vertex 6:	( to:2,wt:0.40)	( to:3,wt:0.52)	( to:0,wt:0.58)	( to:4,wt:0.93)	
vertex 7:	( to:4,wt:0.37)	( to:5,wt:0.28)	( to:0,wt:0.16)	( to:1,wt:0.19)	( to:2,wt:0.34)	

複製程式碼

最小生成樹

最小生成樹是一副連通加權無向圖中一棵權值最小的生成樹。 各個節點之間連通,連通總費用最小。

paste image

相關應用

  • 電纜佈線設計
  • 網路設計
  • 電路設計

最小生成樹主要針對:

  • 針對帶權無向圖
  • 針對連通圖
  • 對於不連通的圖,可以求所有是連通分量的最小生成樹形成最小森林。

步驟

  • 找 V-1 條邊
  • 連線V個頂點
  • 總權值最小

切分定理:Cut Property

切分的定義

把圖中的節點分成兩部分,成為一個切分(cut)

paste image

藍色和紅色的部分形成了一個切分。

橫切邊的定義

如果一個邊的兩個端點,屬於切分(Cut)不同的兩邊,這個邊稱為橫切邊(Crossing Edge)。

paste image

切分定理:

給定任意切分,橫切邊中權值最小的邊必然屬於最小生成樹。

Lazy Prim

paste image

  • 所有邊中選取出v-1條。
  • 找出四條橫切邊中最小的
    • 最小堆進行實現
  • 把最小邊加入後。進行新的切分
  • 不斷加入最小橫切邊的點,直到所有頂點都進入(直到最後所有節點被訪問過)。
  • 懶惰:雖然不是橫切邊,但是沒有被扔掉。

Prim演算法程式碼實現

// 使用Prim演算法求圖的最小生成樹
template<typename Graph, typename Weight>
class LazyPrimMST{

private:
    Graph &G;                   // 圖的引用
    MinHeap<Edge<Weight>> pq;   // 最小堆, 演算法輔助資料結構
    bool *marked;               // 標記陣列, 在演算法執行過程中標記節點i是否被訪問
    vector<Edge<Weight>> mst;   // 最小生成樹所包含的所有邊
    Weight mstWeight;           // 最小生成樹的權值

    // 訪問節點v
    void visit(int v){

        assert( !marked[v] );
        marked[v] = true;

        // 將和節點v相連線的所有未訪問的邊放入最小堆中
        typename Graph::adjIterator adj(G,v);
        for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
            if( !marked[e->other(v)] )
                pq.insert(*e);
    }

public:
    // 建構函式, 使用Prim演算法求圖的最小生成樹
    LazyPrimMST(Graph &graph):G(graph), pq(MinHeap<Edge<Weight>>(graph.E())){

        // 演算法初始化
        marked = new bool[G.V()];
        for( int i = 0 ; i < G.V() ; i ++ )
            marked[i] = false;
        mst.clear();

        // Lazy Prim
        visit(0);
        while( !pq.isEmpty() ){
            // 使用最小堆找出已經訪問的邊中權值最小的邊
            Edge<Weight> e = pq.extractMin();
            // 如果這條邊的兩端都已經訪問過了, 則扔掉這條邊
            if( marked[e.v()] == marked[e.w()] )
                continue;
            // 否則, 這條邊則應該存在在最小生成樹中
            mst.push_back( e );

            // 訪問和這條邊連線的還沒有被訪問過的節點
            if( !marked[e.v()] )
                visit( e.v() );
            else
                visit( e.w() );
        }

        // 計算最小生成樹的權值
        mstWeight = mst[0].wt();
        for( int i = 1 ; i < mst.size() ; i ++ )
            mstWeight += mst[i].wt();
    }

    // 解構函式
    ~LazyPrimMST(){
        delete[] marked;
    }

    // 返回最小生成樹的所有邊
    vector<Edge<Weight>> mstEdges(){
        return mst;
    };

    // 返回最小生成樹的權值
    Weight result(){
        return mstWeight;
    };
};
複製程式碼

main函式

int main() {

    string filename = "testG1.txt";
    int V = 8;

    SparseGraph<double> g = SparseGraph<double>(V, false);
    ReadGraph<SparseGraph<double>, double> readGraph(g, filename);

    // Test Lazy Prim MST
    cout<<"Test Lazy Prim MST:"<<endl;
    LazyPrimMST<SparseGraph<double>, double> lazyPrimMST(g);
    vector<Edge<double>> mst = lazyPrimMST.mstEdges();
    for( int i = 0 ; i < mst.size() ; i ++ )
        cout<<mst[i]<<endl;
    cout<<"The MST weight is: "<<lazyPrimMST.result()<<endl;

    cout<<endl;


    // Test Prim MST
    cout<<"Test Prim MST:"<<endl;
    PrimMST<SparseGraph<double>, double> primMST(g);
    mst = primMST.mstEdges();
    for( int i = 0 ; i < mst.size() ; i ++ )
        cout<<mst[i]<<endl;
    cout<<"The MST weight is: "<<primMST.result()<<endl;

    cout<<endl;

    return 0;
}
複製程式碼

結果

Test Lazy Prim MST:
0-7: 0.16
7-1: 0.19
0-2: 0.26
2-3: 0.17
7-5: 0.28
5-4: 0.35
2-6: 0.4
The MST weight is: 1.81
複製程式碼

時間複雜度

O(ElogE)
複製程式碼

###Prim演算法優化

Lazy Prim存在的問題

  • 所有的邊都進入最小堆,而已經訪問的節點之間的邊其實是可以忽略的(在最小堆中的邊已經不是橫切邊了)。
  • 只需考慮和節點連線的最小橫切邊。

維護資料結構:

和每個節點相連的最短橫切邊。

Prim演算法優化

程式碼實現 (O(ElogV)

//
// Created by liuyubobobo on 9/26/16.
//

#ifndef INC_05_IMPLEMENTATION_OF_OPTIMIZED_PRIM_ALGORITHM_PRIMMST_H
#define INC_05_IMPLEMENTATION_OF_OPTIMIZED_PRIM_ALGORITHM_PRIMMST_H

#include <iostream>
#include <vector>
#include <cassert>
#include "Edge.h"
#include "IndexMinHeap.h"

using namespace std;


template<typename Graph, typename Weight>
class PrimMST{

private:
    Graph &G;
    vector<Edge<Weight>> mst;

    bool* marked;
    IndexMinHeap<Weight> ipq;
    vector<Edge<Weight>*> edgeTo; //儲存節點的最短橫切邊。
    Weight mstWeight;


    void visit(int v){
        assert( !marked[v] );
        marked[v] = true;

        typename Graph::adjIterator adj(G,v);
        for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){
            int w = e->other(v);
            if( !marked[w] ){
                if( !edgeTo[w] ){
                    edgeTo[w] = e;
                    ipq.insert(w, e->wt());
                }
                else if( e->wt() < edgeTo[w]->wt() ){
                    edgeTo[w] = e;
                    ipq.change(w, e->wt());//最小索引堆才有change操作
                }
            }
        }

    }
public:
    // assume graph is connected
    PrimMST(Graph &graph):G(graph), ipq(IndexMinHeap<double>(graph.V())){

        assert( graph.E() >= 1 );

        marked = new bool[G.V()];
        for( int i = 0 ; i < G.V() ; i ++ ){
            marked[i] = false;
            edgeTo.push_back(NULL);
        }
        mst.clear();

        visit(0);
        while( !ipq.isEmpty() ){
            int v = ipq.extractMinIndex();
            assert( edgeTo[v] );
            mst.push_back( *edgeTo[v] );
            visit( v );
        }

        mstWeight = mst[0].wt();
        for( int i = 1 ; i < mst.size() ; i ++ )
            mstWeight += mst[i].wt();
    }

    ~PrimMST(){
        delete[] marked;
    }

    vector<Edge<Weight>> mstEdges(){
        return mst;
    };

    Weight result(){
        return mstWeight;
    };
};

#endif //INC_05_IMPLEMENTATION_OF_OPTIMIZED_PRIM_ALGORITHM_PRIMMST_H

複製程式碼

Kruskal演算法

定義

Kruskal演算法是一種用來查詢最小生成樹的演算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim演算法和Boruvka演算法等。三種演算法都是貪心演算法的應用。和Boruvka演算法不同的地方是,Kruskal演算法在圖中存在相同權值的邊時也有效。

步驟

  • 新建圖G,G中擁有原圖中相同的節點,但沒有邊
  • 將原圖中所有的邊按權值從小到大排序
  • 從權值最小的邊開始,如果這條邊連線的兩個節點於圖G中不在同一個連通分量中,則新增這條邊到圖G中
  • 重複3,直至圖G中所有的節點都在同一個連通分量中

判斷節點是否構成環

  • 使用Union Find快速判斷環
    • 只需要判斷已經找到的邊的兩個節點的根是否相同,如果相同的話說明已經是連通圖了。

程式碼實現

// Kruskal演算法
template <typename Graph, typename Weight>
class KruskalMST{

private:
    vector<Edge<Weight>> mst;   // 最小生成樹所包含的所有邊
    Weight mstWeight;           // 最小生成樹的權值

public:
    // 建構函式, 使用Kruskal演算法計算graph的最小生成樹
    KruskalMST(Graph &graph){

        // 將圖中的所有邊存放到一個最小堆中
        MinHeap<Edge<Weight>> pq( graph.E() );
        for( int i = 0 ; i < graph.V() ; i ++ ){
            typename Graph::adjIterator adj(graph,i);
            for( Edge<Weight> *e = adj.begin() ; !adj.end() ; e = adj.next() )
                //只存一條邊中的一個節點
                if( e->v() < e->w() )
                    pq.insert(*e);
        }

        // 建立一個並查集, 來檢視已經訪問的節點的聯通情況
        UnionFind uf = UnionFind(graph.V());
        while( !pq.isEmpty() && mst.size() < graph.V() - 1 ){

            // 從最小堆中依次從小到大取出所有的邊
            Edge<Weight> e = pq.extractMin();
            // 如果該邊的兩個端點是聯通的, 說明加入這條邊將產生環, 扔掉這條邊
            if( uf.isConnected( e.v() , e.w() ) )
                continue;

            // 否則, 將這條邊新增進最小生成樹, 同時標記邊的兩個端點聯通
            mst.push_back( e );
            uf.unionElements( e.v() , e.w() );
        }

        mstWeight = mst[0].wt();
        for( int i = 1 ; i < mst.size() ; i ++ )
            mstWeight += mst[i].wt();
    }

    ~KruskalMST(){ }

    // 返回最小生成樹的所有邊
    vector<Edge<Weight>> mstEdges(){
        return mst;
    };

    // 返回最小生成樹的權值
    Weight result(){
        return mstWeight;
    };
};


// 測試Kruskal演算法
int main() {

    string filename = "testG1.txt";
    int V = 8;

    SparseGraph<double> g = SparseGraph<double>(V, false);
    ReadGraph<SparseGraph<double>, double> readGraph(g, filename);

    // Test Lazy Prim MST
    cout<<"Test Lazy Prim MST:"<<endl;
    LazyPrimMST<SparseGraph<double>, double> lazyPrimMST(g);
    vector<Edge<double>> mst = lazyPrimMST.mstEdges();
    for( int i = 0 ; i < mst.size() ; i ++ )
        cout<<mst[i]<<endl;
    cout<<"The MST weight is: "<<lazyPrimMST.result()<<endl;

    cout<<endl;


    // Test Prim MST
    cout<<"Test Prim MST:"<<endl;
    PrimMST<SparseGraph<double>, double> primMST(g);
    mst = primMST.mstEdges();
    for( int i = 0 ; i < mst.size() ; i ++ )
        cout<<mst[i]<<endl;
    cout<<"The MST weight is: "<<primMST.result()<<endl;

    cout<<endl;


    // Test Kruskal MST
    cout<<"Test Kruskal MST:"<<endl;
    KruskalMST<SparseGraph<double>, double> kruskalMST(g);
    mst = kruskalMST.mstEdges();
    for( int i = 0 ; i < mst.size() ; i ++ )
        cout<<mst[i]<<endl;
    cout<<"The MST weight is: "<<kruskalMST.result()<<endl;


    return 0;
}
複製程式碼

演算法的更多思考

  • Lazy Prim O( ElogE )
  • Prim O( ElogV )
  • Kruskal O( ElogE )

Vyssotsky’s Algorithm:

  • 根據演算法的具體實現,每次選擇一個邊
  • 此時,圖存在多個最小生成樹
  • 將邊逐漸地新增到生成樹中
  • 一旦形成環,刪除環中權值最大的邊.

-------------------------華麗的分割線--------------------

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人部落格番茄技術小棧掘金主頁

想了解更多,歡迎關注我的微信公眾號:番茄技術小棧

番茄技術小棧

相關文章