計算機考研之數據結構-圖

數據結構-圖

概念

定義

  • 圖G由點集V和邊集E組成,記爲G=(V,E)。
  • 點集不能爲空,邊集能夠爲空。
  • |V|,\(V=v_1,\cdots,v_n\)表示圖點的個數,也稱爲圖的
  • |E|,\(E=\{(u,v),u\in V,v\in V\}\)表示圖邊的個數。

有向圖算法

  • 是點的有序對,記作<v,u>
  • <v,u>中,v 爲弧尾,w 爲弧頭,稱點 v 到點 u 的弧,或 v 鄰接到 u。

無向圖數組

  • 是點的無序對,記作(v,u)(u,v)
  • (v,u)中,稱 v 和 u 互爲鄰接。

分類

簡單圖,圖G知足:數據結構

  • 不存在重複邊。
  • 不存在點到自身的邊。

多重圖,非簡單圖即爲多重圖。spa

屬性

路徑,點 u 到 點 v 的路是,u,a,b,c,d,...,v 的一個點序列。
路徑長度,路徑上邊的個數
迴路(環),路徑中,第一個點和最後一個點相同。
簡單路徑,路徑中,點序列不重複。
簡單迴路,迴路中,點序列不重複。
距離,點 u 到 點 v 的最短路徑。若不存在則路徑爲無窮大(∞)。指針

子圖,有兩個圖 G=(V,E) 和 G'=(V',E'),\(V'\in V,E'\in E\) 則 G' 是 G 的子圖。
生成子圖,子圖知足 V(G')=V(G)。code

生成樹,連通圖中包含全部點的一個極小連通子圖blog

  • 若圖中點爲 n 則其生成樹有 n-1 條邊。

生成森林,非連通圖中全部連通份量的生成樹。排序

帶權圖(網),邊上有數值的圖。事件

無向圖屬性

徹底圖或簡單徹底圖,無向圖中,任意兩個點都存在邊。

  • 無向徹底圖中,n 個點有 n(n-1)/2 條邊。

連通,無向圖中,點 v 到 點 u 之間有路徑存在,則 v,w 是連通的。
連通圖,圖中任意兩點都連通。
連通份量非連通圖中的極大連通子圖爲連通份量。

  • 若一個圖有 n 個點,可是隻有 n-1 條邊,那麼必爲非連通圖。

點的度,與該點相連邊的個數。記爲TD(V)。

  • 無向圖所有點的度之和等於邊數量的兩倍,由於每條邊與兩個點相連。

有向圖屬性

有向徹底圖,在有向圖中,任意兩個點之間都存在方向相反的弧。

  • 有向徹底圖中,n 個點 n(n-1) 條邊。

強連通強連通圖強連通份量,有向圖中與無向圖相對的概念。
出度,入度,出度爲是以點爲起點的弧的數量,記爲 ID(v)。入度是以點爲終點的弧的數量記爲 OD(v)。TD(v)=ID(v)+OD(v)。

  • 有向圖所有點的出度之和與入度之和等於弧的數量。

存儲

鄰接矩陣

概念

鄰接矩陣即便用一個矩陣來記錄點與點之間的鏈接信息。

對於結點數爲 n 的圖 G=(V,E)的鄰接矩陣A 是 nxn 的矩陣。

  • A[i][j]=1,若(vi,vj)或<vi,vj>或(vi,vj)是E(G)中的邊。
  • A[i][j]=1,若(vi,vj)或<vi,vj>或(vi,vj)不是E(G)中的邊。

對帶權圖而言,若頂點vi,vj相連則鄰接矩陣中存着該邊對應的權值,若不相連則用無窮大表示。

  • A[i][j]=\(w_{ij}\),若(vi,vj)或<vi,vj>或(vi,vj)是E(G)中的邊。
  • A[i][j]=0或∞,若(vi,vj)或<vi,vj>或(vi,vj)不是E(G)中的邊。

定義

# define MAXSIZE 
typedef struct {
    int vexs [MAXSIZE];
    int edges[MAXSIZE][MAXSIZE];
    int vexnum, arcnum; // 點和邊的數量
}MGraph;

性質

  1. 無向圖的鄰接矩陣爲對稱矩陣,能夠只用上或下三角。
  2. 對於無向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的度 。
  3. 對於有向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的出度(入度)。
  4. 鄰接矩陣容易肯定點之間是否相連,可是肯定邊的個數須要遍歷。
  5. 稠密圖適合使用鄰接矩陣。

鄰接表

概念

對每一個頂點創建一個單鏈表,而後全部頂點的單鏈表使用順序存儲。
頂點表由頂點域(data)和指向第一條鄰邊的指針(firstarc)構成。
邊表,由鄰接點域(adjvex)和指向下一條鄰接邊的指針域(nextarc)構成。

定義

typedef struct ArcNode{  // 邊結點
    int adjvex; // 邊指向的點
    struct ArcNode *next; //指向的下一條邊
}ArcNode;

typedef struct VNnode{ //頂點節點
    int data;
    ArcNode *first;
}VNode, AdjList[MAX]

typedef struct { //鄰接表
    AdjList vertices;
    int vexnum, arcnum;
} ALGraph;

性質

  1. 若G爲無向圖,則所需的存儲空間爲O(|V|+2|E|),若G爲有向圖,則所需的存儲空間爲O(|V|+|E|)。前者倍數是後者兩倍是由於每條邊在鄰接表中出現了兩次。
  2. 鄰接表法比較適合於稀疏圖。
  3. 點找邊很容易,點找邊不容易。
  4. 鄰接表的表示不惟一

十字鏈表

概念

有向圖的一種表示方式。
十字鏈表中每一個弧和頂點都對應有一個結點。

  • 弧結點:tailvex, headvex, hlink, tlink, info
    • headvex, tailvex 分別指示頭域和尾域。
    • hlink, tlink 鏈域指向弧頭和弧尾相同的下一條弧。
    • info 指向該弧相關的信息。
  • 點結點:data, firstin, firstout
    • 以該點爲弧頭或弧尾的第一個結點。

定義

typedef struct ArcNode{
    int tailvex, headvex;
    struct ArcNode *hlink, *tlink;
    //InfoType info;
} ArcNode;
typedef struct VNode{
    int data;
    ArcNode *firstin, *firstout;
}VNode;
typeder struct{
    VNode xlist[MAX];
    int vexnum, arcnum;
} GLGrapha;

鄰接多重表

概念

鄰接多重表是無向圖的一種鏈式存儲方式。

邊結點:

  • mark 標誌域,用於標記該邊是否被搜索過。
  • ivex, jvex 該邊的兩個頂點所在位置。
  • ilink 指向下一條依附點 ivex 的邊。
  • jlink 指向下一條依附點 jvex 的邊。
  • info 邊相關信息的指針域。

點結點:

  • data 數據域
  • firstedge 指向第一條依附於改點的邊。

鄰接多重表中,依附於同一點的邊串聯在同一鏈表中,因爲每條邊都依附於兩個點,因此每一個點會在邊中出現兩次。

定義

typedef struct ArcNode{
    bool mark;
    int ivex, jvex;
    struct ArcNode *ilink, *jlink;
    // InfoType info;
}ArcNode;
typedef struct VNode{
    int data;
    ArcNode *firstedge;
}VNode;
typedef struct {
    VNode adjmulist[MAX];
    int vexnum, arcnum;
} AMLGraph;

基本操做

  • Adjacent(G,x,y),判斷圖是否存在邊(x,y)或<x,y>。
  • Neighbors,列出圖中與 x 鄰接的邊。
  • InsertVertex(G,x),在圖中插入頂點 x。
  • DeleteVertex(G,x),在圖中刪除頂點 x。
  • AddEdge(G,x,y),若是(x,y)或<x,y>不存在,則添加。
  • RemoveEdge(G,x,y),若是(x,y)或<x,y>存在,則刪除。
  • FirstNeighbor(G,x),求圖中頂點 x 的第一個鄰接點。存在返回頂點號,不存在返回-1。
  • NextNeighbor(G,x,y),返回除x的的下一個鄰接點,不存在返回-1;
  • GetEdgeValue(G,x,y),得到(x,y)或<x,y>的權值。
  • SetEdgeValue(G,x,y),設置(x,y)或<x,y>的權值。

遍歷

廣度優先

廣度優先搜索(BFS)有點相似於二叉樹的層序遍歷算法。從某個頂點 v 開始遍歷與 v 鄰近的 w1,w2,3...,而後遍歷與 w1,w2,3...wi 鄰近的點。

因爲 BFS 是一種分層的搜索算法,因此必需要藉助一個輔助的空間。

//初始化操做
bool visited[MAX];
for(int i=0;i<G.vexnum;i++) visited[i]=FALSE;

void BFSTraverse(Graph G){
    InitQueue(Q);
    for(int i=0;i<G.vexnum;i++){
        if(!visited[i])
            BFS(G, i);
    }
}

void BFS(Graph G, int v){
    visit(v);
    visited[v]=TRUE;
    Enqueue(Q,v);
    while(!isEmpty(Q)){
        Dequeue(Q,v);
        for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
            if(!visited[w]){
                visit[w];
                visited[w]=TRUE;
                EnQueue(Q,w);
            }
        }
    }
}

時間複雜度分析:
鄰接表:O(|V|+|E|)
鄰接矩陣:O(|V|^2)

深度優先

//初始化操做
bool visited[MAX];
for(int v=0;v<G.vexnum;v++) visited[v]=FALSE;

void DFSTraverse(Graph G){
    for(int v=0;v<G.vexnum;v++){
        if(!visited[v])
            DFS(G,v);
    }
}

void DFS(Graph G,int v){
    visit(v);
    visited[v]=TRUE;
    for(w=FistNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
        if(!visited[w]) 
            DFS(G,w)
}

最小生成樹

一個連通圖的生成樹是圖的極小連通子圖,即包含圖中全部頂點,且只包含儘量少的邊的樹。
對於一個帶權的連通圖,生成樹不一樣,對應的權值也不一樣,權值最小的那棵生成樹就是最小生成樹。

對於最小生成樹,有以下性質:

  1. 最小生成樹不惟一,可是對應的權值惟一。
  2. 邊數爲頂點數減一。

構造最小生成樹有多種算法,可是通常會用到如下性質:
若 G 是一個帶權連通無向圖,U 是 點集 V 的一個非空子集。若(u,v)其中 u∈U,v∈V-U,是一條具備最小權值的邊,則一定存在一棵包含邊(u,v)的最小生成樹。

通用算法以下:

MST(G){
    T=NULL;
    while T未造成生成樹;
        do 找到一條最小代價邊(u,v)且加入 T 後不會產生迴路;
            T=T∪(u,v)
}

Prim

Prim算法的執行很是相似於尋找圖最短路徑的Dijkstra算法。
從某個頂點出發遍歷選取周圍最短的邊。

//僞代碼描述
void Prim(G,T){
    T=∅;
    U={w}; //w爲任意頂點
    while((V-U)!=∅){
        找到(u,v),u∈U,v∈(V-U),且權值最小;
        T=T∪{(u,t)};
        U=U∪{v}
    }
}

以鄰接矩陣爲例:

void Prim(MGraph G)
{
    int sum = 0;
    int cost[MAXSIZE];
    int vexset[MAXSIZE];
    for(int i=0;i<G.vexnum;i++) cost[i]=G.edges[0][i];
    for(int i=0;i<G.vexnum;i++) vexset[i] = FALSE;
    vexset[0]=TRUE;

    for(int i=1;i<G.vexnum;i++)
    {
        int mincost=INF;
        int minvex;
        int curvex;
        for(int j=0;j<G.vexnum;j++)
        {
            if(vexset[j]==FALSE&&cost[j]<mincost)
            {
                mincost=cost[j];
                minvex=j;
            }
            vexset[minvex]=TRUE;
            curvex = minvex;
        }
        sum+=mincost;
        for(int j=0;j<G.vexnum;j++)
            if(vexset[j]==FALSE&&G.edges[curvex][j]<cost[j])
                cost[j]=G.edges[curvex][j]
    }
}

Prim算法的複雜度爲O(|V|^2)不依賴於|E|,因此適合於邊稠密的圖。

構造過程:

kruskal

kruskal所作的事情跟prim是反過來的,kruskal算法對邊進行排序,依次選出最短的邊連到頂點上。

//僞代碼描述
void Kruskal(V,T){
    T=V;
    numS=n; //連通份量數
    while(nums>1){
        從E選出權值最小的邊(v,u);
        if(v和u屬於T中不一樣的連通份量){
            T=∪{(v,u)};
            nums--;
        }
    }
}

一樣以鄰接矩陣爲例。

typedef struct
{
    int v1,v2;
    int w;
} Road;
Road road[MAXSIZE];
int v[MAXSIZE];
int getRoot(int x)
{
    while(x!=v[x]) x=v[x];
    return x;
}
void Kruskal(MGraph G, Road road[])
{
    int sum=0;
    for(int i=0;i<G.vexnum;i++) v[i]=i;
    sort(road,G.arcnum);
    for(int i=0;i<G.arcnum;i++)
    {
        int v1=getRoot(road[i].v1);
        int v2=getRoot(road[i].v2);
        if(v1!=v2)
        {
            v[v1]=v2;
            sum+=road[i].w;
        }
    }
}

kruskal算法的複雜度爲O(|E|log|E|)適合邊少點多的圖。

構造過程:

最短路徑

最短路徑算法通常會利用最短路徑的一條性質,即:兩點間的最短路徑也包含了路徑上其餘頂點間的最短路徑。

Dijkstra

Dijkstra 算法通常用於求單源最短路徑問題。即一個頂點到其餘頂點間的最短路徑

這裏咱們須要用到三個輔助數組:

  • dist[vi],從 v0 到每一個頂點 vi 的最短路徑長度。
  • path[vi],保存從 v0 到 vi 最短路徑上的前一個頂點。
  • set[],標記點是否被併入最短路徑。

執行過程:

  • 初始化:
    • 選定源點 v0。
    • dist[vi]:若 v0 到 vi 之間若存在邊,則爲邊上的權值,不然爲∞。
    • path[vi]:若 v0 到 vi 之間存在邊,則 path[vi]=v0,不然爲-1。
    • set[v0]=TRUE,其他爲 FALSE。
  • 執行:
    1. 從當前的 dist[]數組中選出最小值 dist[vu]。
    2. 將 set[vu] 置爲TRUE。
    3. 檢測全部 set[vi]==FALSE 的點。
    4. 比較 dist[vi] 和 dist[vu]+w 的大小,w 爲 <vu,vi>的權值。
    5. 若是 dist[vu]+w<dist[vi]
    6. 更新 path[] 並將 vu 加入路徑中
    7. 直到遍歷完全部的頂點(n-1次)

結合圖來理解就是:

void Dijkstra(MGraph G, int v)
{
    int set[MAXSIZE];
    int dist[MAXSIZE];
    int path[MAXSIZE];
    int min;
    int curvex;
    for(int i=0;i<G.vexnum;i++)
    {
        dist[i]=G.edges[v][i];
        set[i]=FALSE;
        if(G.edges[v][i]<INF) path[i]=v;
        else path[i]=-1;
    }
    set[v]=TRUE;path[v]=-1;
    
    for(int i=0;i<G.vexnum-1;i++)
    {
        min=INF;
        for(int j=0;j<G.vexnum;j++)
        {
            if(set[j]==FALSE;&&dist[j]<min)
            {
                curvex=j;
                min=dist[j];
            }
            set[curvex]=TRUE;
        }
        for(int j=0;j<G.vexnum;j++)
        {
            if(set[j]==FALSE&&(dist[curvex]+G.edges[curvex][j])<dist[j])
            {
                dist[j]=dist[u]+G.edges[curvex][j];
                path[j]=curvex;
            }
        }
    }
}

複雜度分析:從代碼能夠很容易看出來這裏有兩層的for循環,時間複雜度爲O(n^2)。
適用性:不適用於帶有負權值的圖。

Floyd

floyd算法是求圖中任意兩個頂點間的最短距離

過程:

  • 初始化一個矩陣A,\(A^{(-1)}\)[i][j]=G.edges[i][j]。
  • 迭代n輪:\(A^{(k)}\)=Min{\(A^{(k-1)}\)[i][j], \(A^{(k-1)}\)[i][k]+\(A^{(k-1)}\)[k][j]}

\(A^{(k)}\)矩陣存儲了前K個節點之間的最短路徑,基於最短路徑的性質,第K輪迭代的時候會求出第K個節點到其餘K-1個節點的最短路徑。

圖解:

void Floyd(MGraph G, int Path[][MAXSIZE])
{
    int A[MAXSIZE][MAXSIZE];
    for(int i=0;i<G.vexnum;i++)
        for(int j=0;j<G.vexnum;j++)
        {
            A[i][j]=G.edges[i][j];
            Path[i][j]=-1;
        }

    for(int k=0;k<G.vexnum;k++)
        for(int i=0;i<G.vexnum;i++)
            for(int j=0;j<G.vexnum;j++)
                if(A[i][j]>A[i][k]+A[k][j])
                {
                    A[i][j]=A[i][k]+A[k][j];
                    Path[i][j]=k;
                }
}

複雜度分析:主循環爲三個for,O(n^3)。
適用性分析:容許圖帶有負權邊,可是不能有負權邊構成的迴路。

拓撲排序

概念

  • DAG,有向無環圖。
  • AOV網,用<Vi,Vj>表示 Vi 先於 Vj 的關係構成的DAG。即每一個點表示一種活動,活動有前後順序。
  • 拓撲排序,知足如下關係的DAG,即求AOV網中可能的活動順序:
    • 每一個頂點只出現一次。
    • 若頂點 A 在頂點 B 以前,則不存在 B 到 A 的路徑。

算法

一種比較經常使用的拓撲排序算法:

  1. 從DAG圖中選出一個沒有前驅的頂點刪除。
  2. 從圖中刪除全部以該點爲起點的邊。
  3. 重複1,2。直到圖爲空。若不爲空則必有環。

最終獲得的拓撲排序結果爲:1,2,4,3,5。

關鍵路徑

概念

在帶權有向圖中,若權值表示活動開銷則爲AOE網
AOE網的性質

  1. 只有頂點的的事件發生後,後繼的頂點的事件才能發生。
  2. 只有頂點的全部前驅事件發生完後,才能進行該頂點的事件。

源點:AOE 中僅有一個入度爲0的頂點。
匯點:AOE 中僅有一個出度爲0的頂點。

關鍵路徑:從源點到匯點的全部路徑中路徑長度最大的。
關鍵路徑長度:完成整個工程的最短時間。
關鍵活動:關鍵路徑上的活動。

算法

先定義幾個量:

  1. ve(k),事件 vk 最先發生時間。決定了全部從 vj 開始的活動能開工的最先時間。
    • ve(源點)=0。
    • ve(k)=Max{ve(j)+Weight(vj,vk)}。
    • 注意從前日後算。
  2. vl(k),事件 vk 最遲發生的時間。保證所指向的事件 vi 能在 ve(i)以前完成。
    • vl(匯點)=ve(匯點)。
    • vl(k)=Min{vl(k)-Weight(vj,vk)}。
    • 注意從後往前算。
  3. e(i),活動 ai 最先開始的時間。
    • 若邊<vk,vj>表示活動 ai,則有 e(i)=ve(k)。
  4. l(i),活動 ai 最遲開始時間。
    • l(i)=vl(i)-Weight(vk, vj)。
  5. d(i),活動完成的時間餘量。
    • d(i)=l(i)-e(i)。
    • l(i)=e(i)則爲關鍵活動。

求關鍵路徑算法以下:

  1. 求 AOE 網中全部事件的 ve()
  2. 求 AOE 網中全部事件的 vl()
  3. 求 AOE 網中全部活動的 e()
  4. 求 AOE 網中全部活動的 l()
  5. 求 AOE 網中全部活動的 d()
  6. 全部 d()=0的活動構成關鍵路徑

能夠求得關鍵路徑爲(v1,v3,v4,v6)

習題

相關文章
相關標籤/搜索