算法與數據結構之帶權圖與圖最小生成樹

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

帶權圖 Weighted Graph

邊上都有本身的權值的帶權圖。 c++

鄰接矩陣表示有權圖

  • 把原來的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)	

複製代碼

最小生成樹

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

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:

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

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

看完的朋友能夠點個喜歡/關注,您的支持是對我最大的鼓勵。

我的博客番茄技術小棧掘金主頁

想了解更多,歡迎關注個人微信公衆號:番茄技術小棧

番茄技術小棧
相關文章
相關標籤/搜索