1、圖的定義
圖 是一個頂點集合V和一個頂點間關系的集合E組成,記G=(V,E)
V:頂點的有限非空集合。
E:頂點間關系的有限集合(邊集)。
存在一個結點v,可能含有多個前驅節點和后繼結點。
1頂點(vertex)
上圖中黑色的帶數字的點就是頂點,表示某個事物或對象。由於圖的術語沒有標准化,因此,稱頂點為點、節點、結點、端點等都是可以的。叫什么無所謂,理解是什么才是關鍵。
2邊(edge)
ACM圖的存儲(轉載自劍紫青天,但是他github掛了
對於ACM圖論方面的題目總是免不了首先要建圖存圖,使用合適的存圖方式不但是AC的必要條件,解題事半功倍。
以下主要分析三種常見的存圖方式的優缺點以及代碼實現
- 鄰接矩陣
- 鄰接表
- 鏈式前向星
鄰接矩陣
鄰接矩陣是三種存圖方式中最簡單也最為暴力的一種存圖方式了。
存圖思想
使用一個矩陣來描述一個圖,對於矩陣的第i
行第j
列的值,表示編號為i
的頂點到編號為j
的頂點的權值。
代碼實現
對於鄰接矩陣來說,它的代碼實現都十分簡單,二維數組就可以了。
1 |
#include <stdio.h> |
優點
使用鄰接矩陣來進行建圖存圖有以下優點
-
簡單易學
這個肯定不用多說,哪怕是沒學過線性代數的童鞋也很容易理解這樣的存圖方式。
-
代碼易寫,簡單好操作
上面的代碼實現已經展示了要定義這個數據結構以及實現初始化,增加刪除邊等操作有多么的簡單。
-
對已確定的邊進行操作,效率高
確定邊(已知兩頂點編號)要進行增加或刪除邊(或者說更改邊權)以及查詢邊權等操作,時間復雜度為$O(1)$。
-
易處理重邊
你可以隨時覆蓋掉重邊,可以自己實現存儲最新的邊,權值最大的邊或權值最小的邊等。
當然,如果你非要使用鄰接矩陣存圖還要存重邊也不是不可以。
缺點
鄰接矩陣存圖雖然簡單優雅,但是它的一些缺點卻幾乎是致命的。
-
過高的空間復雜度
對於頂點數
V
,鄰接矩陣存圖的空間復雜度高達$O(V^2)$,頂點數上了一萬可以不用考慮這種存圖方式了。
對於稀疏圖來說,鄰接矩陣存圖內存浪費太嚴重,這也是鄰接矩陣存圖在ACM題目中十分罕見的根本原因。 -
對於不確定邊的查詢效率一般
比如,我找個編號為
1
出發的第一條邊我還要一條條邊判斷是否存在(權值是否為0
)。
鄰接表
鄰接表在三種常用的存圖方式中屬於較為中庸和普遍的存圖方式了,缺點不致命,優點不明顯。
存圖思想
鄰接矩陣對於每個頂點使用定長的數組來存儲以該點出發的邊的情況。第i
個數組的第j
個值存儲的是從頂點i
到頂點j
的邊的權值。
而鄰接表則是對於每個頂點使用不定長的鏈表來存儲以該點出發的邊的情況。因此對於第i
個鏈表的第j
個值實際上存儲的是從編號為i
的頂點出發的第j
條邊的情況。
一般來說,如果有邊權的話,鄰接表的鏈表存儲的是一個結構體,這個結構體存儲該邊的終點以及邊權。
下面給個鄰接表與鄰接矩陣存圖的示例比較。
代碼實現
在ACM題目中,動態的數據結構一般是不被推薦的,因為動態開辟內存比較消耗時間,且寫起來復雜容易出錯。
大部分情況我們使用C++STL里的vector
作為鏈表來實現圖的鄰接表。
1 |
#include <vector> |
優點
-
較為簡單易學
相比鄰接矩陣,無非是數組轉鏈表加上存儲值的意義不同而已,不需要轉太大的彎。
-
代碼易寫,不復雜
代碼實現已經演示過了,較簡單,不容易寫錯。
-
內存利用率較高
-
對不確定邊的操作方便效率也不錯
比如,要遍歷從某點出發的所有邊,不會像鄰接矩陣一樣可能會遍歷到不存在的邊。
缺點
-
重邊不好處理
判重比較麻煩,還要遍歷已有的邊,不能直接判斷。
一般情況下使用鄰接表存圖是會存儲重邊的,不會做重邊的判斷。
所以如果要解決重邊的影響一般不在存邊的情況下做文章。 -
對確定邊的操作效率不高
比如對於給定
i->j
的邊要進行查詢或修改等操作只有通過遍歷這種方式找到了。
鏈式前向星
鏈式前向星是前向星的升級版,因為它可以完美代替前向星,所以就跳過前向星的學習,直接學習鏈式前向星。
存圖思想
這種存圖方式的數據結構主要是邊集數組,顧名思義,圖的邊是用數組來存儲的。
當然想要完美表示圖結構,光有一個邊集數組還不夠,還要有一個數組存儲指向每一個點的第一條邊的“指針”。
而每一條邊都需要存儲接下來一條邊的“指針”,這樣就能夠像類似鄰接表一樣方便遍歷每一個點的所有邊了。
代碼實現
1 |
#include <stdio.h> |
優點
-
內存利用率高
相比
vector
實現的鄰接表而言,可以准確開辟最多邊數的內存,不像vector
實現的鄰接表有爆內存的風險。 -
對不確定邊的操作方便效率也不錯
這點和鄰接表一樣,不會遍歷到不存在的邊。
缺點
-
難於理解,代碼較復雜
這種存圖方式相對於鄰接表來說比較難理解,代碼雖然不是很復雜但是不熟練的話寫起來也不是方便。
-
重邊不好處理
這點與鄰接表一樣,只有通過遍歷判重。
-
對確定邊的操作效率不高
也與鄰接表一樣,不能通過兩點馬上確定邊,只能遍歷查找。
總結
對於鄰接矩陣存圖來說,由於內存消耗的局限性,它的適用范圍比較狹窄,幾乎只能在簡單圖論題目中見到。
鄰接表存圖是最為常見的一種,絕大部分采用C++STL中的vector
實現,一般情況下大部分圖論題目都能使用該存圖方式。
但是鏈式前向星其實是一種較好替代鄰接表來存圖的數據結構,在鄰接表存圖不能使用時可以使用,幾乎可以用於全部圖論題目。
上圖中頂點之間藍色的線條就是邊,表示事物與事物之間的關系。需要注意的是邊表示的是頂點之間的邏輯關系,粗細長短都無所謂的。包括上面的頂點也一樣,表示邏輯事物或對象,畫的時候大小形狀都無所謂。
最短路是什么呢,就是兩個頂點間最短的距離
floyd算法(3重循環的思行代碼
for(int k=1; k<=n; k++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) M[i][j]=min(M[i][j],M[i][k]+M[k][j]);
n條邊m條路
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; const int INF=0x3f3f3f3f; int main() { int n,m,M[100][100]; scanf("%d%d",&n,&m); memset(M,INF,sizeof M); for(int i=1; i<=n; i++) M[i][i]=0; for(int i=1,u,v,w; i<=m; i++) scanf("%d%d%d",&u,&v,&w),M[u][v]=w; for(int k=1; k<=n; k++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) M[i][j]=min(M[i][j],M[i][k]+M[k][j]); for(int i=1; i<=n; i++) { for(int j=1; j<=n; j++) printf("%d ",M[i][j]); printf("\n"); } return 0; }
附錄:矩陣相乘
矩陣A乘以B(15 分)
給定兩個矩陣A和B,要求你計算它們的乘積矩陣AB。需要注意的是,只有規模匹配的矩陣才可以相乘。即若A有Ra行、Ca列,B有Rb行、Cb列,則只有Ca與Rb相等時,兩個矩陣才能相乘。
輸入格式:
輸入先后給出兩個矩陣A和B。對於每個矩陣,首先在一行中給出其行數R和列數C,隨后R行,每行給出C個整數,以1個空格分隔,且行首尾沒有多余的空格。輸入保證兩個矩陣的R和C都是正數,並且所有整數的絕對值不超過100。
輸出格式:
若輸入的兩個矩陣的規模是匹配的,則按照輸入的格式輸出乘積矩陣AB,否則輸出Error: Ca != Rb
,其中Ca
是A的列數,Rb
是B的行數。
輸入樣例1:
2 3
1 2 3
4 5 6
3 4
7 8 9 0
-1 -2 -3 -4
5 6 7 8
輸出樣例1:
2 4
20 22 24 16
53 58 63 28
輸入樣例2:
3 2
38 26
43 -5
0 17
3 2
-11 57
99 68
81 72
輸出樣例2:
Error: 2 != 3
#include<bits/stdc++.h> using namespace std; int A[105][105],B[105][105],C[105][105]; int main() { int a,b; cin>>a>>b; for(int i=0; i<a; i++) for(int j=0; j<b; j++) cin>>A[i][j]; int c,d; cin>>c>>d; for(int i=0; i<c; i++) for(int j=0; j<d; j++) cin>>B[i][j]; if(b!=c) cout<<"Error: "<<b<<" != "<<c; else { cout<<a<<" "<<d<<"\n"; for(int i=0; i<a; i++) for(int j=0; j<d; j++) for(int k=0; k<b; k++) C[i][j]+=A[i][k]*B[k][j]; for(int i=0; i<a; i++) { cout<<C[i][0]; for(int j=1; j<d; j++) cout<<" "<<C[i][j]; cout<<"\n"; } } }