學習目標:
1.掌握:圖的基本概念及相關術語和性質
2.熟練掌握:圖的鄰接矩陣和鄰接表兩種存儲表示方法
3.熟練掌握:圖的兩種遍歷方法DFS和BFS
4.熟練掌握:最短路徑算法(Dijkstra算法)
5.掌握:最小生成樹的兩種算法及拓撲排序算法的思想
基本概念儲備:
Graph=(V,E),and graph is generally divided into directed and undirected graph,according to the property of edge。
完全圖:任意兩個點都有一條邊相連。對於有向無向圖其Number of edges can be solved。
稀疏圖:有很少邊或弧的圖。 稠密圖:有較多邊或弧的圖。
網:邊/弧帶權的圖。
路徑:接續的邊構成的頂點序列。 路徑長度:路徑上邊或弧的數目/權值之和。
迴路(環):第一個頂點和最後一個頂點相同的路徑。
簡單路徑:除路徑起點和終點可以相同外,其餘頂點均不相同的路徑。
簡單迴路(簡單環):除路徑起點和終點相同外,其餘頂點均不相同的路徑。
強連通圖:在無(有)向圖G=( V, {E} )中,若對任何兩個頂點 v、u 都存在從v 到 u 的路徑,則稱G是連通圖(強連通圖)。
極大連通子圖 & 極小連通子圖
圖的存儲結構:
順序存儲結構:鄰接矩陣數組表示法
鏈式存儲結構:鄰接錶鏈式表示法
無向圖的鄰接矩陣一定是對稱的.並且對於頂點i而言,它的度等於第i列或者第i行中1的個數。
爲什麼我們用無窮來代表無邊呢?某萌妹如是說:
至於採用鄰接矩陣構造無向網(帶權),我們的算法分四步 1)輸入總頂點數和總邊數 2)依次輸入頂點的信息存入頂點表中 3)初始化鄰接矩陣,權值賦值爲極大值,比如Limit.h的INT_MAX 4)構造鄰接矩陣。
//鄰接矩陣的構造方式:
#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;
Status CreateUDN(AMGraph &G)
{
//採用鄰接矩陣表示法,創建無向網G
scanf(“%d”,&G.vexnum);
scanf(“%d”,&G.arcnum); //輸入總頂點數,總邊數
for(i = 0; i<G.vexnum; ++i)
scanf(“%d”,&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){ //構造鄰接矩陣
scanf(“%d, %d, %d”, &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;
}
然後我們看看鄰接表的鏈式表示方法:
鄰接表的鏈式存儲表示:
算法思想:1)輸入總頂點數和總邊數 2)依次輸入各個點的信息存入頂點表中,每個表頭節點的指針域初始化爲NULL 3)創建鄰接表
#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;
Status CreateUDG(ALGraph &G){
//採用鄰接表表示法,創建無向圖G
scanf(“%d, “%d”, &G.vexnum,&G.arcnum); //輸入總頂點數,總邊數
for(i = 0; i<G.vexnum; ++i){ //輸入各點,構造表頭結點表
scanf(“%d”,&G.vertices[i].data); //輸入頂點值
G.vertices[i].firstarc=NULL; //初始化表頭結點的指針域爲NULL
}//for
for(k = 0; k<G.arcnum;++k){ //輸入各邊,構造鄰接表
scanf(“%d, %d”, &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
十字鏈表:略
圖的遍歷:從已給的連通圖中某一頂點出發,沿着一些邊訪遍圖中所有的頂點,且使每個頂點僅被訪問一次,就叫做圖的遍歷,它是圖的基本運算。
遍歷實質:找每個頂點的鄰接點的過程。
圖的特點:圖中可能存在迴路,且圖的任一頂點都可能與其它頂點相通,在訪問完某個頂點之後可能會沿着某些邊又回到了曾經訪問過的頂點。
如何避免重複訪問?:解決思路:設置輔助數組 visited [n ],用來標記每個被訪問過的頂點。 初始狀態爲0 i 被訪問,改 visited [i]爲1,防止被多次訪問
DFS:深度優先算法
DFS算法設計:
void DFS(AMGraph G, int v) //圖G爲鄰接矩陣類型
{
printf(“%d”, 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爲鄰接表類型
printf(“%d”,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指向下一個邊結點
}
}
BFS:廣度優先搜索
簡單歸納: 在訪問了起始點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
printf(“%d”, 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的尚未訪問的鄰接頂點
printf(“%d”, w); visited[w] = true; EnQueue(Q, w); //w進隊
}//if
}//while
}//BFS
如何求最小生成樹 ?
Prim算法: 歸併頂點,與邊數無關,適於稠密網
Kruskal算法:歸併邊,適於稀疏網
補充:
貪心算法原理:以當前情況爲基礎作最優選擇,而不考慮各種可能的整體情況,所以貪心法不要回溯。 算法優點:因爲省去了爲尋找解而窮盡所有可能所必須耗費的大量時間,因此算法效率高。 注意:貪婪算法的精神就是“只顧如何獲得眼前最大的利益”,有時不一定是最優解。比如找零錢是先用大面值再用小面值讓我們情感上認爲花的鈔票的鈔票數最小;又比如0-1揹包問題.
最短路徑問題:在帶權有向圖中A點(源點)到達B點(終點)的多條路徑中,尋找一條各邊權值之和最小的路徑,即最短路徑。
兩種常見的最短路徑問題:
一、 單源最短路徑—用Dijkstra(迪傑斯特拉)算法 ,一個頂點到其餘各個頂點
二、所有頂點間的最短路徑—用Floyd(弗洛伊德)算法,任意兩點之間
Dijkstra算法:
我們將帶全網N=(V,E)分爲兩個集合,一個是S(已經求出的最短路徑的終點集合),一個是V-S。
1.初始化:先找出從源點v0到各終點vk的直達路徑(v0,vk),即通過一條弧到達的路徑。
2.選擇:從這些路徑中找出一條長度最短的路徑(v0,u)。
3.更新:然後對其餘各條路徑進行適當調整:
若在圖中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),
則以路徑(v0,u,vk)代替(v0,vk)。
在調整後的各條路徑中,再找長度最短的路徑,依此類推。
算法流程:
① 初始化:
● 將源點v0加到S中,即S[v0] = true;
● 將v0到各個終點的最短路徑長度初始化爲權值,即D[i] = G.arcs[v0][vi],(vi∈V − S);
● 如果v0和頂點vi之間有弧,則將vi的前驅置爲v0,即Path[i] = v0,否則Path[i] = −1。
② 選擇下一條最短路徑的終點vk,使得: D[k] = Min{D[i]|vi∈V − S}
③ 將vk加到S中,即S[vk] = true。
④ 更新從v0出發到集合V − S上任一頂點的最短路徑的長度,同時更改vi的前驅爲vk。 若S[i]=false 且 D[k]+G.arcs[k][i]<D[i],則D[i]=D[k]+ G.arcs[k][i]; Path [i]=k;。
⑤ 重複②~④ n − 1次,即可按照路徑長度的遞增順序,逐個求得從v0到圖上其餘各頂點的最短路徑。
上面的描述大可不看,看看算法代碼會更加直觀:
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