數據結構-圖
概念
定義
- 圖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;
性質
- 無向圖的鄰接矩陣為對稱矩陣,可以只用上或下三角。
- 對於無向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的度 。
- 對於有向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的出度(入度)。
- 鄰接矩陣容易確定點之間是否相連,但是確定邊的個數需要遍歷。
- 稠密圖適合使用鄰接矩陣。
鄰接表
概念
對每個頂點建立一個單鏈表,然后所有頂點的單鏈表使用順序存儲。
頂點表由頂點域(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;
性質
- 若G為無向圖,則所需的存儲空間為O(|V|+2|E|),若G為有向圖,則所需的存儲空間為O(|V|+|E|)。前者倍數是后者兩倍是因為每條邊在鄰接表中出現了兩次。
- 鄰接表法比較適合於稀疏圖。
- 點找邊很容易,點找邊不容易。
- 鄰接表的表示不唯一
十字鏈表
概念
有向圖的一種表示方式。
十字鏈表中每個弧和頂點都對應有一個結點。
- 弧結點: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)
}
最小生成樹
一個連通圖的生成樹是圖的極小連通子圖,即包含圖中所有頂點,且只包含盡可能少的邊的樹。
對於一個帶權的連通圖,生成樹不同,對應的權值也不同,權值最小的那棵生成樹就是最小生成樹。
對於最小生成樹,有如下性質:
- 最小生成樹不唯一,但是對應的權值唯一。
- 邊數為頂點數減一。
構造最小生成樹有多種算法,但是一般會用到以下性質:
若 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。
- 執行:
- 從當前的 dist[]數組中選出最小值 dist[vu]。
- 將 set[vu] 置為TRUE。
- 檢測所有 set[vi]==FALSE 的點。
- 比較 dist[vi] 和 dist[vu]+w 的大小,w 為 <vu,vi>的權值。
- 如果 dist[vu]+w<dist[vi]
- 更新 path[] 並將 vu 加入路徑中
- 直到遍歷完所有的頂點(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 的路徑。
算法
一種比較常用的拓撲排序算法:
- 從DAG圖中選出一個沒有前驅的頂點刪除。
- 從圖中刪除所有以該點為起點的邊。
- 重復1,2。直到圖為空。若不為空則必有環。
最終得到的拓撲排序結果為:1,2,4,3,5。
關鍵路徑
概念
在帶權有向圖中,若權值表示活動開銷則為AOE網。
AOE網的性質:
- 只有頂點的的事件發生后,后繼的頂點的事件才能發生。
- 只有頂點的所有前驅事件發生完后,才能進行該頂點的事件。
源點:AOE 中僅有一個入度為0的頂點。
匯點:AOE 中僅有一個出度為0的頂點。
關鍵路徑:從源點到匯點的所有路徑中路徑長度最大的。
關鍵路徑長度:完成整個工程的最短時間。
關鍵活動:關鍵路徑上的活動。
算法
先定義幾個量:
ve(k)
,事件 vk 最早發生時間。決定了所有從 vj 開始的活動能開工的最早時間。- ve(源點)=0。
- ve(k)=Max{ve(j)+Weight(vj,vk)}。
- 注意從前往后算。
vl(k)
,事件 vk 最遲發生的時間。保證所指向的事件 vi 能在 ve(i)之前完成。- vl(匯點)=ve(匯點)。
- vl(k)=Min{vl(k)-Weight(vj,vk)}。
- 注意從后往前算。
e(i)
,活動 ai 最早開始的時間。- 若邊<vk,vj>表示活動 ai,則有 e(i)=ve(k)。
l(i)
,活動 ai 最遲開始時間。- l(i)=vl(i)-Weight(vk, vj)。
d(i)
,活動完成的時間余量。- d(i)=l(i)-e(i)。
- l(i)=e(i)則為關鍵活動。
求關鍵路徑算法如下:
- 求 AOE 網中所有事件的 ve()
- 求 AOE 網中所有事件的 vl()
- 求 AOE 網中所有活動的 e()
- 求 AOE 網中所有活動的 l()
- 求 AOE 網中所有活動的 d()
- 所有 d()=0的活動構成關鍵路徑
可以求得關鍵路徑為(v1,v3,v4,v6)