圖
圖的基本定義

(學藝不精,圖畫的不好,望見諒)
圖的定義
1.圖的定義
無論多么復雜的圖,都是由頂點和邊構成的。圖G由兩個集合V和E組成,記成G=(V,E),其中V是頂點的有限集合,E是連接V中兩個不同頂點(頂點對)的邊的有限集合,記成E(G)。
2.有向圖
定義:如果表示邊的頂點對(或序偶)是有序的,則稱G為有向圖。

ps:在有向圖中表示邊的頂點對用尖括號括起來,用於表示一條有向邊,如表示從頂點i到頂點j的一條邊,可見<i,j>和<j,i>是兩條不同的邊 。
3.無向圖
定義:如果在圖G中,若<i,j>∈E(G)必有<j,i>∈E(G),即E(G)是對稱的,則用(i,j)代替這兩個頂點對,表示頂點i與頂點j的一條無向邊,則稱G為無向圖。

4.圖的抽象數據類型定義
ADT Graph
{數據對象:
D={ai|1≤i<=n,n>=0,ai為ElemType類型} //ElemType是自定義類型標識符
數據關系:
R= {<ai,aj>|ai、aj∈D,1<=i,j<=n,其中每個元素可以有零個或多個前裝元素,可以有零個或多個后繼元素}
基本運算:
CreateGraph( &g):創建圖,由相關數據構造一個圖g。
DestroyGraph(&g):銷毀圖,釋放圖g占用的存儲空間。
DispGraph(g):輸出圖,顯示圖g的頂點和邊信息。
DFS(g,v):從頂點v出發深度優先遍歷圖g。
BFS(g,v):從頂點v出發廣度優先遍歷圖g。
圖的基本術語
1.端點和鄰接點
在一個無向圖中,若存在一條邊(i,j),則稱頂點 i和頂點j為該邊的兩個端點,並稱它們互為鄰接點,即頂點i是頂點j的一個鄰接點,頂點i也是頂點j的一個鄰接點,邊(i,j)和頂點i、j關聯。關聯於相同兩個端點的兩條或者兩條以上的邊稱為多重邊,在數據結構中討論的圖都是指沒有多重邊的圖。
在一個有向圖中,若存在一條有向邊<i,j>(也稱為弧),則稱此邊是頂點i的一條出邊,同時也是頂點j的一條入邊,i為此邊的起始端點(簡稱為起點),j為此邊的終止端點(簡稱終點),頂點j是頂點i的出邊鄰接點,頂點i是頂點j的入邊鄰接點。
2.頂點的度、入度和出度
在無向圖中,一個頂點所關聯的邊的數目稱為該頂點的度。在有向圖中,頂點的度又分為人度和出度,以頂點j為終點的邊數目,稱為該頂點的入度。以頂點i為點的邊數目,稱為該頂點的出度。一個頂點的入度與出度的和為該頂點的度。
(ps:一個圖中,所有頂點的度之和等於邊數的兩倍。)
3.完全圖
若無相圖中的每兩個頂點之間都存在着一條邊,有向圖中的每兩個頂點之間都存在着方向相反的兩條邊,則稱此圖為完全圖。

有向完全圖:有向圖中的每兩個頂點之間都存在着方向相反的兩條邊。
ps:有向完全圖包含n(n-1)條邊。

無向完全圖:每兩個頂點之間都存在一條邊。

ps:有向完全圖包含n(n-1)/2條邊。
4.稠密圖和稀疏圖
稠密圖:當一個圖接近完全圖時。

稀疏圖:當一個圖含有較少的邊數時。
5.子圖
6.路徑和路徑長度
7.回路或環
若一條路徑上的開始點與結束點為同一個頂點,則此路徑被稱為回路或環。開始點與結束點相同的簡單路經被稱為簡單回路或簡單環。
8.連通、連通圖和連通分量
在無向圖G中,若從頂點i到頂點j有路徑,則稱頂點i和頂點j是連通的。若圖G中的任意兩個頂點都是連通的則稱G為連通圖,否則稱為非連通圖。無向圖G中的極大連通子圖稱為G的連通分量,顯然連通圖的連通分量只有一個(即本身)而非連通圖有多個連通分量。
9.強連通圖和強連通分量
在有向圖G中,若從頂點i到頂點j有路徑,則稱從頂點i到頂點j是連通的,若圖G中的任意兩個頂點i和j都連通,即從頂點i到頂點j和從頂點j到頂點i都存在路徑則稱途徑是強連通圖。
ps:強連通圖只有一個強連通分量(即它本身)。
在一個非常連通圖中找強連通分量的方法如下 :
(1)在圖中找有向環。
(2)拓展該有向環:如果某個頂點到該環中的任何一項頂點都有路徑,並且該環中的任一頂點到這個頂點也有路徑,則加入這個頂點。

10.權和網
圖中的每一條邊都可以附有一個對應的數值,這種與邊相關的數值稱為權,權可以表示從一個頂點到另一個頂點的距離或花費的代價,邊上帶有權的圖稱為帶權圖,也稱為網。

帶權無向圖

帶權有向圖
圖的存儲結構和基本運算算法
鄰接矩陣存儲方法
在鄰接矩陣中判斷圖中,兩個頂點之間是否有邊或者求兩個頂點之間邊的權的執行時間為O(1),所以在需要提取邊權值的算法中,通常采用鄰接矩陣存儲結構。
鄰接表存儲方法
圖的鄰接表是一種順序鏈式存儲相結合的存儲方法。
在鄰接表中有兩種類型的結點,一種是頭結點,其個數恰好為圖中頂點的個數,另一種是邊結點,也就是單鏈表中的結點。對無向圖,這類結點的個數等於邊數的兩倍;對有向圖,這類結點的個數等於邊數。
鄰接表結構體:
typedef struct ANode //邊結點; { int adjvex;//指向該邊的終點編號; struct ANode*nextarc;//指向下一個鄰接點; INfoType info;//保存該邊的權值等信息; }ArcNode; typedef struct //頭結點 { int data;//頂點; ArcNode *firstarc;//指向第一個鄰接點; }VNode; typedef struct { VNode adjlist[MAX];//鄰接表; int n,e;//圖中頂點數n和邊數e; }AdjGraph;
可以看出,對於邊數目較少的稀疏圖,鄰接表比鄰接矩陣更節省存儲空間。
圖的遍歷
圖的遍歷的概念
從給定圖中任意指定的頂點(稱為初始點)出發,按照某種搜索方法沿着圖的邊訪問圖中的所有頂點,使每個頂點僅被訪問一次,這個過程稱為圖的遍歷。
按照搜索方法的不同,圖的遍歷方法有兩種,一種是深度優先遍歷(DFS),另一種叫廣度優先遍歷(BFS)。
深度優先遍歷
深度優先遍歷的過程是從圖中的某個初始點v出發,首先訪問初始點v,然后選擇一個與頂點為相鄰且沒被訪問過的頂點w,以w為初始頂點,再從它出發進行深度優先遍歷 直到圖中與頂點為鄰接的所有頂點都被訪問過為止。容易看出,這是一個遞歸過程。
如:
遍歷結果為A -> B -> G -> E -> C -> D -> H -> F
深度優先算法:
int vsited[MAX]= {0}; //全局數組
void DFS(AdjGraph * G,int v) //深度優先遍歷算法
{ArcNode * p;
visited[v]=1; //置已訪向標記
printf("%d",v); //輸出被訪向頂點的編號
p=G一adjlist[v]. firstarc; //p指向頂點v的第一個鄰接點
while (p!= NULL){
if (visited[p-> adjvex]==0) //若p->adivex頂點未被訪問,遞歸訪問它
DFS(G,p-> adjvex);
p於P->nextarc; //p指向頂點v的下一一個鄰接點
}
廣度優先遍歷
廣度優先遍歷是連通圖的一種遍歷策略。從圖中某個頂點V0出發,並訪問此頂點,然后從V0出發,訪問V0的各個未曾訪問的鄰接點W1,W2,…,Wk;然后,依次從W1,W2,…,Wk出發訪問各自未被訪問的鄰接點,再重復上一步驟,直到全部頂點都被訪問為止。這也是一個遞歸過程。

例如如下圖,廣度優先遍歷得到的答案是A -> B -> C -> F -> D -> H -> E -> G

生成樹和最小生成樹
按照生成樹的定義,n個頂點的連通圖的生成樹有n個頂點、(n-1)條邊。因此,構造最小生成樹的准則有以下3條:
(1)必須只使用該圖中的邊來構造最小生成樹;
(2)必須使用且僅使用(n- 1)條邊來連接圖中的n個頂點;
(3)不能使用產生回路的邊。
ps:求圖的最小生成樹的兩個算法:普里姆算法、克魯斯卡爾算法。

無向圖
深度優先樹和廣度優先樹
由深度優先遍歷得到的生成樹稱為深度優先生成樹。在深度優先遍歷中,如果將每次“前進”(縱向)路過的(將被訪問)頂點和邊都記錄下來,就得到了一個子圖,該子圖為以出發點為根的樹,就是深度優先生成樹。

深度優先樹
相應地,由廣度優先遍歷得到的生成樹稱為廣度優先生成樹。

廣度優先樹
這樣的生成樹由遍歷時訪問過的n個頂點和遍歷時經歷的(n-1)條邊組成。
對於非連通圖,每個連通分量中的頂點集和遍歷時走過的邊一起構成一棵生成樹,各個連通分量的生成樹組成非連通圖的生成森林。
普里姆算法
普里姆算法是一種構造性算法。假設G=(V,E)是一個具有n個頂點的帶權連圖,T=(U,TE)是G的最小生成樹,其中U是T的頂點集,TE是T的邊集,則由G構造從起始點V出發的最小生成樹T的步驟如下:
(1)初始化U={v},以U到其他頂點的所有邊為候選邊。
(2)重復以下步驟(n- 1)次,使得其他(n-1)個頂點被加入到U中。
1)從候選邊中挑選權值最小的邊加人TE,設該邊在V-U中的頂點是k,將k加入u。
2)考查當前U忠的所有頂雷修改候選邊,若(k,j)的權值小於原來和頂點,聯的候選邊,則用(k,j)最代后署作為候選邊。

void prim(int start)
{
int sumweight=0;
int i,j,k=0;
for(i=1;i<VNUM;i++) //頂點是從1開始
{
lowcost[i]=edge[start][i];
addvnew[i]=-1; //將所有點至於Vnew之外,V之內,這里只要對應的為-1,就表示在Vnew之外
}
addvnew[start]=0; //將起始點start加入Vnew
adjecent[start]=start;
for(i=1;i<VNUM-1;i++)
{
int min=MAX;
int v=-1;
for(j=1;j<VNUM;j++)
{
if(addvnew[j]!=-1&&lowcost[j]<min) //在Vnew之外尋找最短路徑
{min=lowcost[j];
v=j;}
}
if(v!=-1)
{
printf("%d %d %d\n",adjecent[v],v,lowcost[v]);
addvnew[v]=0; //將v加Vnew中
sumweight+=lowcost[v]; //計算路徑長度之和
for(j=1;j<VNUM;j++)
{if(addvnew[j]==-1&&edge[v][j]<lowcost[j])
{
lowcost[j]=edge[v][j]; //此時v點加入Vnew 需要更新lowcost
adjecent[j]=v;}
}
}
}
printf("the minmum weight is %d",sumweight);
}
ps:普利姆算法適用於稠密圖。
克魯斯卡爾算法
克魯斯卡爾算法是一種按權值的的方法。假設G=(V.E)是一個具有n個頂點的帶權連通無向圖,T=(U,TE)是G的最小生成樹,則構造最小生成樹的步驟如下:
(1)置U的初值為V(即包含有G中的全部頂點),TE的初值為空集(即圖T中的每一個頂點都構成一個分量)。
(2)將圖G中的邊按權值從小到大的順序依次選取,若選取的邊未使生成樹T形成回路,則加人TE,否則舍棄,直到TE中包含(n- 1)條邊為止。
對於帶權連通圖,采用克魯斯卡爾算法構造最小生成樹的過程如下:
(1)將所有邊按權值遞增排序;
(2)圖中邊上的數字表示該邊是第幾小的邊,如1表示是最小的邊,2表示是第2小的邊,依此類推。

void kruskal(MGraph G)
{
int i,j,u1,v1,sn1,sn2,k;
int vset[VertexNum]; //輔助數組,判定兩個頂點是否連通
int E[EdgeNum]; //存放所有的邊
k=0; //E數組的下標從0開始
for (i=0;i<G.n;i++)
{
for (j=0;j<G.n;j++)
{
if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)
{ E[k].u=i;
E[k].v=j;
E[k].w=G.edges[i][j];
k++;}
}
}
heapsort(E,k,sizeof(E[0])); //堆排序,按權值從小到大排列
for (i=0;i<G.n;i++) //初始化輔助數組
{ vset[i]=i;}
k=1; //生成的邊數,最后要剛好為總邊數
j=0; //E中的下標
while (k<G.n)
{
sn1=vset[E[j].u];
sn2=vset[E[j].v]; //得到兩頂點屬於的集合編號
if (sn1!=sn2) //不在同一集合編號內的話,把邊加入最小生成樹
{printf("%d ---> %d, %d",E[j].u,E[j].v,E[j].w);
k++;
for (i=0;i<G.n;i++)
{ if (vset[i]==sn2)
vset[i]=sn1;
}
}
j++;
}
}
ps:克魯斯卡爾算法適用於稀疏圖。
最短路徑
在一個不帶權圖中,若從一頂點到另一頂點存在着條路徑,則稱該路徑長度為該路徑上所經過的邊的數目,它等於該路徑上的頂點數減1。由於從一頂點到另一頂點可能存在着多條路徑,每條路徑上所經過的邊數可能不同,即路徑長度不同,把路徑長度最短(即經過的邊數最少)的那條路徑稱為最短路徑,其長度稱為最短路徑長度或最短距離。
對於帶權圖,考慮路徑上各邊上的權,則把一條路徑上所經邊的權之和定義為該路名為路徑長度。從源點到終點可能有不止一條路徑,把路徑長度最小的那條路徑稱為最短路名其路徑長度(權之和)稱為最短路徑長度。
實際上,只要把不帶權圖上的每條邊看成是權值為1的邊,那么不帶權圖和帶權圖的量短路徑和最短距離的定義就一致了。求圖的最短路徑有兩個方面的問題,即求圖中某一 頂點到其余各頂點的最短路徑材圖中每一對頂點之間的最短路徑。

拓撲排序
在一個有向圖中找一個拓撲序列的過程叫拓撲排序。
拓撲排序方法如下:
(1)從有向圖中選擇一個沒有前驅(即入度為0)的頂點並且輸出它。
(2)從圖中刪去該頂點,並且刪去從該頂點發出的全部有向邊。
(3)重復上述兩步,直到剩余的圖中不再存在沒有前驅的頂點為止。
這樣操作的結果有兩種: 一種是圖中全部頂點都被輸出,即該圖中所有頂點都在拓撲序列中,這說明圖中不存在回路(即該圖為有向無環圖);另一種就是圖中頂點未被全部輸出,這說明圖中存在回路。

queue
vector
for(int i=0;i<n;i++) //n 節點的總數
if(in[i]
0) q.push(i); //將入度為0的點入隊列
vector
while(!q.empty())
{
int p=q.front(); q.pop(); // 選一個入度為0的點,出隊列
ans.push_back(p);
for(int i=0;i<edge[p].size();i++)
{
int y=edge[p][i];
in[y]--;
if(in[y]
q.push(y);
}
}
if(ans.size()==n)
{
for(int i=0;i<ans.size();i++)
printf( "%d ",ans[i] );
printf("\n");
}
else printf("No\n"); // ans 中的長度與n不相等,就說明無拓撲序列
AOE網和關鍵路徑
圖中入度為0的頂點表示工程的開始事件(如開工儀式),出度為0的頂點表示工程結束事件,稱這樣的有向圖為邊表示活動的網(AOE網)。
利用這樣的AOE網能夠計算完成整個工程預計需要多少時間,並找出影響工程進度的“關鍵活動”,從而為決策者提供修改各活動的預計進度的依據。
在AOE網中,從源點到匯點的所有路徑中具有最大路徑長圖的路徑稱為關鍵路徑,完成整個工程的最短時間就是AOE網中關健路徑的長度,或者說是AOE中一條關鍵路徑上各活動持續時間的總和,把關路徑上的活動稱為關鍵活動。
因此,只要找出AOE網中的所有關鍵活動也就找到了全部關鍵路徑。

