圖:Graph=(V,E)
V:頂點(數據元素)的有窮非空集合;
E:邊的有窮集合。
算法
無向圖:每條邊都是無方向的數組
有向圖:每條邊都是有方向的網絡
徹底圖:任意兩個點都有一條邊相連
spa
稀疏圖:有不多邊或弧的圖。
稠密圖:有較多邊或弧的圖。
網:邊/弧帶權的圖。
鄰接:有邊/弧相連的兩個頂點之間的關係。
存在(vi, vj),則稱vi和vj互爲鄰接點;
存在<vi, vj>,則稱vi鄰接到vj, vj鄰接於vi
關聯(依附):邊/弧與頂點之間的關係。
存在(vi, vj)/ <vi, vj>, 則稱該邊/弧關聯於vi和vj
3d
頂點的度:與該頂點相關聯的邊的數目,記爲TD(v)
指針
在有向圖中, 頂點的度等於該頂點的入度與出度之和。
頂點 v 的入度是以 v 爲終點的有向邊的條數, 記做 ID(v)
頂點 v 的出度是以 v 爲始點的有向邊的條數, 記做OD(v)
code
路徑:接續的邊構成的頂點序列。
路徑長度:路徑上邊或弧的數目/權值之和。
迴路(環):第一個頂點和最後一個頂點相同的路徑。
簡單路徑:除路徑起點和終點能夠相同外,其他頂點均不相同的路徑。
簡單迴路(簡單環):除路徑起點和終點相同外,其他頂點均不相同的路徑。
blog
連通圖(強連通圖)
排序
在無(有)向圖G=( V, {E} )中,若對任何兩個頂點 v、u 都存在從v 到 u 的路徑,則稱G是連通圖(強連通圖)。遞歸
權與網:圖中邊或弧所具備的相關數稱爲權。代表從一個頂點到另外一個頂點的距離或耗費。帶權的圖稱爲網。
子圖:設有兩個圖G=(V,{E})、G1=(V1,{E1}),若V1屬於V,E1 屬於 E,則稱 G1是G的子圖。 例:(b)、(c) 是 (a) 的子圖
連通份量(強連通份量)
無向圖G 的極大連通子圖稱爲G的連通份量。 極大連通子圖意思是:該子圖是 G 連通子圖,將G 的任何不在該子圖中的頂點加入,子圖再也不連通。
有向圖G 的極大強連通子圖稱爲G的強連通份量。極大強連通子圖意思是:該子圖是G的強連通子圖,將D的任何不在該子圖中的頂點加入,子圖再也不是強連通的。
極小連通子圖:該子圖是G 的連通子圖,在該子圖中刪除任何一條邊,子圖再也不連通。 生成樹:包含無向圖G 全部頂點的極小連通子圖。生成森林:對非連通圖,由各個連通份量的生成樹的集合。
CreateGraph(&G,V,VR)
初始條件:V是圖的頂點集,VR是圖中弧的集合。
操做結果:按V和VR的定義構造圖G。
DFSTraverse(G)
初始條件:圖G存在。
操做結果:對圖進行深度優先遍歷。
BFSTraverse(G)
初始條件:圖G存在。
操做結果:對圖進行廣度優先遍歷。
創建一個頂點表(記錄各個頂點信息)和一個鄰接矩陣(表示各個頂點之間關係)。
設圖 A = (V, E) 有 n 個頂點,則圖的鄰接矩陣是一個二維數組 A.Edge[n][n],定義爲:
分析1:無向圖的鄰接矩陣是對稱的;
分析2:頂點i 的度=第 i 行 (列) 中1 的個數;
特別:徹底圖的鄰接矩陣中,對角元素爲0,其他1。
注:在有向圖的鄰接矩陣中,
第i行含義:以結點vi爲尾的弧(即出度邊);
第i列含義:以結點vi爲頭的弧(即入度邊)。
分析1:有向圖的鄰接矩陣多是不對稱的。
分析2:頂點的出度=第i行元素之和
頂點的入度=第i列元素之和
頂點的度=第i行元素之和+第i列元素之和
缺點:n個頂點須要n*n個單元存儲邊;空間效率爲O(n2)。 對稀疏圖而言尤爲浪費空間。
//用兩個數組分別存儲頂點表和鄰接矩陣 #define MaxInt 32767 //表示極大值,即∞ #define MVNum 100 //最大頂點數 typedef char VerTexType; //假設頂點的數據類型爲字符型 typedef int ArcType; //假設邊的權值類型爲整型 typedef struct{ VerTexType vexs[MVNum]; //頂點表 ArcType arcs[MVNum][MVNum]; //鄰接矩陣 int vexnum,arcnum; //圖的當前點數和邊數 }AMGraph;
(1)輸入總頂點數和總邊數。
(2)依次輸入點的信息存入頂點表中。
(3)初始化鄰接矩陣,使每一個權值初始化爲極大值。
(4)構造鄰接矩陣。
Status CreateUDN(AMGraph &G){ //採用鄰接矩陣表示法,建立無向網G cin>>G.vexnum>>G.arcnum; //輸入總頂點數,總邊數 for(i = 0; i<G.vexnum; ++i) cin>>G.vexs[i]; //依次輸入點的信息 for(i = 0; i<G.vexnum;++i) //初始化鄰接矩陣,邊的權值均置爲極大值 for(j = 0; j<G.vexnum;++j) G.arcs[i][j] = MaxInt; for(k = 0; k<G.arcnum;++k){ //構造鄰接矩陣 cin>>v1>>v2>>w; //輸入一條邊依附的頂點及權值 i = LocateVex(G, v1); j = LocateVex(G, v2); //肯定v1和v2在G中的位置 G.arcs[i][j] = w; //邊<v1, v2>的權值置爲w G.arcs[j][i] = G.arcs[i][j]; //置<v1, v2>的對稱邊<v2, v1>的權值爲w }//for return OK; }//CreateUDN
int LocateVex(MGraph G,VertexType u) {//存在則返回u在頂點表中的下標;不然返回-1 int i; for(i=0;i<G.vexnum;++i) if(u==G.vexs[i]) return i; return -1; }
對每一個頂點vi 創建一個單鏈表,把與vi有關聯的邊的信息連接起來,每一個結點設爲3個域;
每一個單鏈表有一個頭結點(設爲2個域),存vi信息;每一個單鏈表的頭結點另外用順序存儲結構存儲。
注:鄰接表不惟一,因各個邊結點的鏈入順序是任意的
空間效率爲O(n+2e)。
如果稀疏圖(e<<n2),比鄰接矩陣表示法O(n2)省空間。
TD(Vi)=單鏈表中連接的結點個數
#define MVNum 100 //最大頂點數 typedef struct ArcNode{ //邊結點 int adjvex; //該邊所指向的頂點的位置 struct ArcNode * nextarc; //指向下一條邊的指針 OtherInfo info; //和邊相關的信息 }ArcNode; typedef struct VNode{ VerTexType data; //頂點信息 ArcNode * firstarc; //指向第一條依附該頂點的邊的指針 }VNode, AdjList[MVNum]; //AdjList表示鄰接表類型 typedef struct{ AdjList vertices; //鄰接表 int vexnum, arcnum; //圖的當前頂點數和邊數 }ALGraph;
(1)輸入總頂點數和總邊數。
(2)依次輸入點的信息存入頂點表中,使每一個表頭結點的指針域初始化爲NULL。
(3)建立鄰接表。
Status CreateUDG(ALGraph &G){ //採用鄰接表表示法,建立無向圖G cin>>G.vexnum>>G.arcnum; //輸入總頂點數,總邊數 for(i = 0; i<G.vexnum; ++i){ //輸入各點,構造表頭結點表 cin>> G.vertices[i].data; //輸入頂點值 G.vertices[i].firstarc=NULL; //初始化表頭結點的指針域爲NULL }//for for(k = 0; k<G.arcnum;++k){ //輸入各邊,構造鄰接表 cin>>v1>>v2; //輸入一條邊依附的兩個頂點 i = LocateVex(G, v1); j = LocateVex(G, v2); p1=new ArcNode; //生成一個新的邊結點*p1 p1->adjvex=j; //鄰接點序號爲j p1->nextarc= G.vertices[i].firstarc; G.vertices[i].firstarc=p1; //將新結點*p1插入頂點vi的邊表頭部 p2=new ArcNode; //生成另外一個對稱的新的邊結點*p2 p2->adjvex=i; //鄰接點序號爲i p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2; //將新結點*p2插入頂點vj的邊表頭部 }//for return OK; }//CreateUDG
優勢:空間效率高,容易尋找頂點的鄰接點;
缺點:判斷兩頂點間是否有邊或弧,需搜索兩結點對應的單鏈表,沒有鄰接矩陣方便。
鄰接矩陣與鄰接表表示法的關係
1. 聯繫:鄰接表中每一個鏈表對應於鄰接矩陣中的一行,鏈表中結點個數等於一行中非零元素的個數。
2. 區別:
① 對於任一肯定的無向圖,鄰接矩陣是惟一的(行列號與頂點編號一致),但鄰接表不惟一(連接次序與頂點編號無關)。
② 鄰接矩陣的空間複雜度爲O(n2),而鄰接表的空間複雜度爲O(n+e)。
3. 用途:鄰接矩陣多用於稠密圖;而鄰接表多用於稀疏圖
十字鏈表---用於無向圖
遍歷定義:從已給的連通圖中某一頂點出發,沿着一些邊訪遍圖中全部的頂點,且使每一個頂點僅被訪問一次,就叫作圖的遍歷,它是圖的基本運算。
遍歷實質:找每一個頂點的鄰接點的過程。
圖的特色:圖中可能存在迴路,且圖的任一頂點均可能與其它頂點相通,在訪問完某個頂點以後可能會沿着某些邊又回到了曾經訪問過的頂點。
解決思路:設置輔助數組 visited [n ],用來標記每一個被訪問過的頂點。
初始狀態爲0
i 被訪問,改 visited [i]爲1,防止被屢次訪問
深度優先搜索
廣度優先搜索
圖G爲鄰接矩陣類型
void DFS(AMGraph G, int v){ //圖G爲鄰接矩陣類型 cout<<v; visited[v] = true; //訪問第v個頂點 for(w = 0; w< G.vexnum; w++) //依次檢查鄰接矩陣v所在的行 if((G.arcs[v][w]!=0)&& (!visited[w])) DFS(G, w); //w是v的鄰接點,若是w未訪問,則遞歸調用DFS }
void DFS(ALGraph G, int v){ //圖G爲鄰接表類型 cout<<v; visited[v] = true; //訪問第v個頂點 p= G.vertices[v].firstarc; //p指向v的邊鏈表的第一個邊結點 while(p!=NULL){ //邊結點非空 w=p->adjvex; //表示w是v的鄰接點 if(!visited[w]) DFS(G, w); //若是w未訪問,則遞歸調用DFS p=p->nextarc; //p指向下一個邊結點 } }
用鄰接矩陣來表示圖,遍歷圖中每個頂點都要從頭掃描該頂點所在行,時間複雜度爲O(n2)。
用鄰接表來表示圖,雖然有 2e 個表結點,但只需掃描 e 個結點便可完成遍歷,加上訪問 n個頭結點的時間,時間複雜度爲O(n+e)。
結論:
稠密圖適於在鄰接矩陣上進行深度遍歷;
稀疏圖適於在鄰接表上進行深度遍歷。
簡單概括:
在訪問了起始點v以後,依次訪問 v的鄰接點;
而後再依次訪問這些頂點中未被訪問過的鄰接點;
直到全部頂點都被訪問過爲止。
廣度優先搜索是一種分層的搜索過程,每向前走一步可能訪問一批頂點,不像深度優先搜索那樣有回退的狀況。
所以,廣度優先搜索不是一個遞歸的過程,其算法也不是遞歸的。
【算法思想】
(1)從圖中某個頂點v出發,訪問v,並置visited[v]的值爲true,而後將v進隊。
(2)只要隊列不空,則重複下述處理。
① 隊頭頂點u出隊。
② 依次檢查u的全部鄰接點w,若是visited[w]的值爲false,則訪問w,並置visited[w]的值爲true,而後將w進隊。
void BFS (Graph G, int v){ //按廣度優先非遞歸遍歷連通圖G cout<<v; visited[v] = true; //訪問第v個頂點 InitQueue(Q); //輔助隊列Q初始化,置空 EnQueue(Q, v); //v進隊 while(!QueueEmpty(Q)){ //隊列非空 DeQueue(Q, u); //隊頭元素出隊並置爲u for(w = FirstAdjVex(G, u); w>=0; w = NextAdjVex(G, u, w)) if(!visited[w]){ //w爲u的還沒有訪問的鄰接頂點 cout<<w; visited[w] = true; EnQueue(Q, w); //w進隊 }//if }//while }//BFS
BFS算法效率分析
若是使用鄰接矩陣,則BFS對於每個被訪問到的頂點,都要循環檢測矩陣中的整整一行( n 個元素),總的時間代價爲O(n2)。
用鄰接表來表示圖,雖然有 2e 個表結點,但只需掃描 e 個結點便可完成遍歷,加上訪問 n個頭結點的時間,時間複雜度爲O(n+e)。
最小生成樹
最短路徑
拓撲排序
關鍵路徑
Prim(普里姆)算法
Kruskal(克魯斯卡爾)算法
Prim算法: 歸併頂點,與邊數無關,適於稠密網
Kruskal算法:歸併邊,適於稀疏網
設連通網絡 N = { V, E }
1. 從某頂點 u0 出發,選擇與它關聯的具備最小權值的邊(u0, v),將其頂點加入到生成樹的頂點集合U中
2. 每一步從一個頂點在U中,而另外一個頂點不在U中的各條邊中選擇權值最小的邊(u, v),把它的頂點加入到U中
3. 直到全部頂點都加入到生成樹頂點集合U中爲止
設連通網絡 N = { V, E }
1. 構造一個只有 n 個頂點,沒有邊的非連通圖 T = { V,空集 }, 每一個頂點自成一個連通份量
2. 在 E 中選最小權值的邊,若該邊的兩個頂點落在不一樣的連通份量上,則加入 T 中;不然捨去,從新選擇
3. 重複下去,直到全部頂點在同一連通份量上爲止
典型用途:交通問題。如:城市A到城市B有多條線路,但每條線路的交通費(或所需時間)不一樣,那麼,如何選擇一條線路,使總費用(或總時間)最少?
問題抽象:在帶權有向圖中A點(源點)到達B點(終點)的多條路徑中,尋找一條各邊權值之和最小的路徑,即最短路徑。
(注:最短路徑與最小生成樹不一樣,路徑上不必定包含n個頂點)
兩種常見的最短路徑問題:
1、 單源最短路徑—用Dijkstra(迪傑斯特拉)算法
2、全部頂點間的最短路徑—用Floyd(弗洛伊德)算法
1.初始化:先找出從源點v0到各終點vk的直達路徑(v0,vk),即經過一條弧到達的路徑。
2.選擇:從這些路徑中找出一條長度最短的路徑(v0,u)。
3.更新:而後對其他各條路徑進行適當調整:
若在圖中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),
則以路徑(v0,u,vk)代替(v0,vk)。
在調整後的各條路徑中,再找長度最短的路徑,依此類推。
主:鄰接矩陣G[n][n] (或者鄰接表)
輔:
數組S[n]:記錄相應頂點是否已被肯定最短距離
數組D[n]:記錄源點到相應頂點路徑長度
數組Path[n]:記錄相應頂點的前驅頂點
void ShortestPath_DIJ(AMGraph G, int v0){ //用Dijkstra算法求有向網G的v0頂點到其他頂點的最短路徑 n=G.vexnum; //n爲G中頂點的個數 for(v = 0; v<n; ++v){ //n個頂點依次初始化 S[v] = false; //S初始爲空集 D[v] = G.arcs[v0][v]; //將v0到各個終點的最短路徑長度初始化 if(D[v]< MaxInt) Path [v]=v0; //v0和v之間有弧,將v的前驅置爲v0 else Path [v]=-1; //若是v0和v之間無弧,則將v的前驅置爲-1 }//for S[v0]=true; //將v0加入S D[v0]=0; //源點到源點的距離爲0 /*―開始主循環,每次求得v0到某個頂點v的最短路徑,將v加到S集―*/ for(i=1;i<n; ++i){ //對其他n−1個頂點,依次進行計算 min= MaxInt; for(w=0;w<n; ++w) if(!S[w]&&D[w]<min) {v=w; min=D[w];} //選擇一條當前的最短路徑,終點爲v S[v]=true; //將v加入S for(w=0;w<n; ++w) //更新從v0出發到集合V−S上全部頂點的最短路徑長度 if(!S[w]&&(D[v]+G.arcs[v][w]<D[w])){ D[w]=D[v]+G.arcs[v][w]; //更新D[w] Path [w]=v; //更改w的前驅爲v }//if }//for }//ShortestPath_DIJ