圖結構和相關問題

圖的定義:圖由頂點和邊組成,每條邊的兩端是圖的兩個頂點。記做G(V,E),V是頂點集,E 爲邊集。

通常圖分爲有向圖和無向圖。
頂點的度是指和該頂點相連的邊的條數。特變的對於有向圖,頂點的出邊條數成爲出度,頂點的蠕變條數成爲入度。頂點和邊均可以由一些屬性,稱爲點權和邊權。c++

圖的存儲:

圖可使用兩種存儲方式:鄰接矩陣和鄰接表。算法

鄰接矩陣:適合頂點數目很少的稠密圖。

設圖G(V,E)的頂點編號爲0-N-1,那麼能夠令二維數組G[N][N]的兩維分別表示圖的頂點標號,及若是G[i][j]的值爲1,表示i和j之間有變。這個二維矩陣被稱爲鄰接矩陣。而且若是存在邊權可讓G[N][N]中存放邊權。
無向圖的鄰接矩陣是對稱矩陣。數組

鄰接表:適合頂點個數較多的稀疏圖。

設圖G(V,E)的頂點標號爲0,1,… ,N-1,每一個頂點都有可能有若干條出邊,若是把同一個頂點的全部出邊放在一個列表中,那麼N個頂點就會有N個列表(沒有出邊,對應空表)。這N個列表被稱爲圖的鄰接表。記做Adj[N]。
鄰接表實現可使用變長數組vector,開一個vector數組Adj[N], N是頂點個數,每一個頂點對應一個變長數組,存儲其出邊。
以下所示:
<Vector<int> Adj[N];>
若是節點還有權值,咱們能夠定義一個結構體:函數

Struct Node{
     Int v;
     Int w;
};

而後vector鄰接表中的元素類型就是Node型的。
<Vector<Node> Adj[N];>測試

圖的遍歷:

是指對圖的全部頂點按必定順序進行訪問,遍歷方法通常有兩種:深度優先搜索(DFS)和廣度優先搜索(BFS)。優化

深度優先搜索以「深度」做爲第一關鍵詞,每次都沿着路徑到不能再前進時才退回到最近的岔路口。

DFS的具體實現,首先介紹兩個概念:
連通份量。在無向圖中,若是兩個頂點之間能夠相互到達(能夠是經過必定路徑間接到達),那麼就稱這兩個頂點連通。若是圖G(V,E)的任意兩個頂點都連通,則通圖G爲連通圖,不然稱G爲非連通圖,且稱其中的極大連通子圖爲連通份量。
強連通份量。在有向圖中,若是倆ing個頂點能夠各自經過一條有向路徑到達另外一個頂點,就稱這兩個頂點強連通。若是圖G(V,E)的任意兩個頂點都強連通,則稱圖G爲強連通圖;不然稱G爲非強連通圖,且稱其中的極大強連通子圖爲強連通份量。
若是要遍歷一個圖就要對全部的連通塊進行遍歷,若是已知的圖是連通圖,則只須要一次DFS遍歷就能夠完成。
DFS的僞代碼:(可使用臨界矩陣和鄰接表實現)spa

DFS(u){//訪問頂點u
    vis[u] = true; //設置u爲已訪問
    for(從u出發能到達的全部頂點v){  //枚舉從u出發能夠到達的全部頂點v
        if(vis[v] == false){
        DFS(v);
    }
}
DFSTrave(G){ //遍歷圖
    for(G 的全部頂點u) //對G的全部頂點u
        if vis[u] == false //若是u未被訪問
            DFS(u); //訪問u所在的連通塊
    
}

廣度優先搜索(BFS)遍歷圖

廣度優先搜索以「廣度」做爲關鍵詞,每次以擴散的方式向外訪問頂點。和樹的遍歷同樣,使用BFS遍歷圖須要使用一個隊列,經過反覆取出隊首頂點,將該頂點可到達的不曾加入過隊列的頂點所有入隊,(而不是未被訪問)直到隊列爲空時遍歷結束。
能夠查看下面的僞代碼,根據思路可使用鄰接表和臨界矩陣進行實現。code

BFS(u){ //遍歷u所在的連通塊
    queue q;//定義隊列q
    將u入隊;
    inq[u] = true;
    while(q 非空){
        取出隊首元素u進行訪問;
        for(從u出發可達到的全部頂點v)
            if( inq[v] == false) { //若是v不曾加入過隊列
                將v入隊;
                inq[v] = true;
            }
    }
}
BFSTrave(G){
    for(G 的全部頂點u)
        if(inq[u] == false){ //若是u不曾加入過隊列
            BFS(u); //遍歷u所在的連通塊
        }
}

最短路徑問題:

最小生成樹(Minimum Spanning Tree,MST):

是在一個給定的無向圖G(V,E)中求一棵樹T,使這棵樹擁有圖G中的全部頂點,且全部邊都來自圖G中,而且知足整棵樹的邊權之和最小。
最小生成樹有三個性質須要掌握:
1. 最小生成樹是樹,所以其邊數等於頂點數減一,且樹內必定不會有環。
2. 對給定的圖G(V,E),其最小生成樹能夠不惟一,但其邊權之和必定是惟一的。
3. 因爲最小生成樹是在無向圖上生成的,所以其根節點能夠是這棵樹上的任意一個結點。通常爲了輸出惟一,會指定一個結點做爲根節點。
經常使用的算法有:Prim(普利姆算法)和 Kruskal算法(克魯斯卡爾算法)排序

Prim算法

僞代碼以下,其時間複雜度爲O(V^2),若是圖用鄰接表實現,可使用堆優化即便用優先級隊列將複雜度下降爲O(VlogV + E):隊列

G爲圖,S是以及加入圖中的頂點集,數組d爲頂點與集合S的最短距離
Prim(G,d[]){
    初始化G[],d[],d[1] = 0;
    for(循環n次)
    {
        u = 使d[u]最小的還未被訪問的頂點的標號;
        記錄u已被訪問;
        for(從 u 出發能到達的全部頂點v){
            if(v 未被訪問 && 以u爲中介點使得v與集合S的最短距離d[v]更優){
                將G[u][v]賦值給d[v];
            }
        }
    }
}

Kruskal算法

僞代碼以下,其時間負責度主要在拍於函數上,是O(ElogE),其中E是圖的邊數。

int kruskal(){
    令最小生成樹的邊權之和爲ans,最小生成樹的當前邊數Num_edge;
    將全部邊按照邊權從小到達排序;
    for(從小到大枚舉全部邊)
    {
        if(當前測試邊的兩個端點在不一樣的連通塊中){ //判斷是否在一個聯通塊中可使用並查集
            將該測試邊加入最小生成樹;
            ans += 測試邊的邊權;
            最小生成樹的當前邊數num_edge+1;
            當前邊數num_edge 等於定點數減一時結束循環;
        }
    }
    return ans;
}

從上面的時間複雜度分析可知,Prim 算法的時間複雜度與V相關,適合稠密圖(頂點少邊多),而kruskal算法的實際複雜度與E的數目有關,適合稀疏圖(頂點多,邊少)。

拓撲排序:

關鍵路徑:

參考內容:《算法筆記》 胡凡 曾磊主編

相關文章
相關標籤/搜索