圖的儲存與遍歷C++實現

Smartog 發表於 2021-07-06
C++

1、圖的儲存

  • 設點數為n,邊數為m

1.1、二維陣列

  • 方法:使用一個二維陣列 adj 來存邊,其中 adj[u][v] 為 1 表示存在 u到 v的邊,為 0 表示不存在。如果是帶邊權的圖,可以在 adj[u][v] 中儲存u到v的邊的邊權。
  • 複雜度:
    • 查詢是否存在某條邊:\(O(1)\)
    • 遍歷一個點的所有出邊:\(O(n)\)
    • 遍歷整張圖:\(O(n^2)\)
    • 空間複雜度:\(O(n^2)\)

1.2、鄰接表

  • 方法:使用一個支援動態增加元素的資料結構構成的陣列,如 vector< int> adj[n + 1] 來存邊,其中 adj[u] 儲存的是點u的所有出邊的相關資訊(終點、邊權等);

  • 複雜度:

    • 查詢是否存在u到v的邊:\(O(d^+(u))\)(如果事先進行了排序就可以使用二分查詢做到\(O(log(d^+(u)))\) )。
    • 遍歷點u的所有出邊:\(O(d^+(u))\)
    • 遍歷整張圖:O(n+m)。
    • 空間複雜度:O(m)。

1.3、直接存邊

  • 方法:使用一個陣列來存邊,陣列中的每個元素都包含一條邊的起點與終點(帶邊權的圖還包含邊權)。(或者使用多個陣列分別存起點,終點和邊權。)
    struct Edge{
    	int u,v;//邊的端點
    	int w;//權重
    }Edges[MAXN];
    
  • 複雜度:
    • 查詢是否存在某條邊:\(O(m)\)
    • 遍歷一個點的所有出邊:\(O(m)\)
    • 遍歷整張圖:\(O(nm)\)
    • 空間複雜度:\(O(m)\)
  • 由於直接存邊的遍歷效率低下,一般不用於遍歷圖。在Kruskal演算法 中,由於需要將邊按邊權排序,需要直接存邊

1.4、鏈式前向星(本質是用陣列模擬連結串列)

  • 方法:本質是用陣列模擬連結串列,主要有兩個陣列

    Edges[MAXN] 儲存邊的資訊,包括兩個端點、權重、下一條邊在Edges中的索引;
    head[MAXN] head[i]為節點i的第一條出邊在Edges中的序號;
    在插入邊的時候維護一個tot變數記錄總計的邊的個數
    
  • 複雜度:

    • 查詢是否存在u到v的邊:\(O(d^+(u))\)(如果事先進行了排序就可以使用二分查詢做到\(O(log(d^+(u)))\) )。
    • 遍歷點u的所有出邊:\(O(d^+(u))\)
    • 遍歷整張圖:O(n+m)。
    • 空間複雜度:O(m)。
  • 程式碼板子:

    #include<bits/stdc++.h>
    using namespace std;
    const int MAXN = 1e6;
    struct Edge
    {//邊結構體
    	int u,v;//邊的端點;
    	int w;//權重
    	int nxt;//下一條邊在Edge中的索引
    }Edges[MAXN];
    int head[MAXN];//每個節點出邊
    int tot;//總的邊數,隨著邊的增加而增加
    
    void init(int n){
    	tot=0;
    	//初始化head陣列
    	for(int i=0; i<n; i++)
    		head[i]=-1;
    	//memset(head,-1,sizeof(head));
    	//memset是以位元組為單位,初始化記憶體塊
    	//位元組單位的陣列時,可以用memset把每個陣列單元初始化成任何你想要的值
    	//因為一個int型別的變數佔4個位元組,而memset是將每一個位元組初始化成1,
    	//所以一個int型別的變數被初始化成了0x01010101。而這個數是16843009
    	//memset初始化int只能初始化0和-1;  
    }
    
    void addEdge(int u,int v,int w){
    	Edges[tot].u=u;
    	Edges[tot].v=v;
    	Edges[tot].w=w;
    	Edges[tot].nxt=head[u];
    	head[u] = tot;
    	tot++;
    }
    

2、圖的遍歷

  • 基於上述的鏈式前向星實現
    void dfs(int u) {
      //v 可以是圖中的一個頂點
      //也可以是抽象的概念,如 dp 狀態等,這一點很難想
      vis[u] = 1; //標記該節點被訪問過
      for (int i = head[u]; i; i = Edges[i].nxt) {
        if (!vis[Edges[i].v]) {
        //task to do
        dfs(v);
        }
      }
    }
    
  • 每次都嘗試訪問同一層的節點。 如果同一層都訪問完了,再訪問下一層。這樣做BFS 演算法找到的路徑是從起點開始的最短合法路徑。換言之,這條路所包含的邊數最小。在 BFS 結束時,每個節點都是通過從起點到該點的最短路徑訪問的。
  • 基於上述的鏈式前向星實現:
    void bfs(int u){
    	vector<int> d;//記錄到達各個節點的最近距離;
    	vector<int> p;//記錄最短路上的節點
    	vector<int> vis(MAXN,0);//0代表節點未被訪問過
    	queue<int> q;
    	q.push(u);
    	vis[u]=1;//標記訪問
    	d[u]=0;
    	p[u]=-1;
    	while(!q.empty()){
    		u=q.front();
    		q.pop();
    		for(int i=head[u]; i; i=Edges[i].nxt){
    		  int v = Edges[i].v;//到達的點
    		  if(!vis[v]){
    		      q.push(v);
    		      vis[v]=1;
    		      d[v]=d[u]+1;
    		      p[v]=u;//記錄前序節點
    		      //task to do
    		  }
    		}
    	}
    }
    

😄
❤️