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


數據結構-圖

概念

定義

  • 圖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滿足:

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

多重圖,非簡單圖即為多重圖。

屬性

路徑,點 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)。

生成樹,連通圖中包含所有點的一個極小連通子圖

  • 若圖中點為 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)

習題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM