文章目錄
第七章 圖
圖的定義
定義:圖是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示爲:G(V, E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
注意:
- 圖中的數據元素,稱爲頂點(Vertex)
- 圖結構中不允許沒有頂點
- 任意兩個頂點之間都可能有關係,頂點之間的邏輯關係用邊表示
無向邊:兩個頂點之間的邊沒有方向,就稱這邊爲無向邊(Edge),用無序偶對表示。
如果所有的頂點之間都是無向邊,則這個圖爲無向圖(Undirected graphs)。
有向邊:頂點間的邊有方向,則稱這條邊爲有向邊,也成爲弧(Arc)。
如果所有的邊都是有向邊,則該圖稱爲有向圖(Directed graphs)。
由A到D的有向邊,A是弧尾,D是弧頭,<A, D>表示弧,不可寫成<D, A>
注意:無向邊是小括號(),有向邊是尖括號 <>
簡單圖:若不存在頂點到其自身的邊,且同一條不重複出現,則稱這樣的圖爲簡單圖,下面這兩個都不是:
無向完全圖:如果無向圖中任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖。
n個頂點的無向完全圖有條邊。
有向完全圖:如果有向圖中任意兩個頂點之間都存在方向互爲相反的兩條弧,則稱該圖爲有向完全圖。
n個頂點的有向完全圖有條邊。
稠密圖:很多條邊或弧的圖。
稀疏圖:很少條邊或弧的圖。
權:有些圖的邊或弧具有相關數字,這種與圖的邊或弧相關的數叫做權(Weight)。
網:這些權可以表示點之間的距離或耗費,帶權的圖通常稱爲網(Network)。
子圖:A被B包含,則A是B的子圖,下面的右邊都是左邊的子圖。
圖的頂點和邊間關係
無向的鄰接和依附:如果兩個點之間有邊,則稱這兩個頂點互爲鄰接點(Adjacent),即相鄰接。邊依附於頂點,或者說邊和頂點相關聯。
頂點的度:和該頂點相關聯的邊的數目。
邊的數量就是各頂點度數和的一半。
有向的鄰接:如果有弧存在,則稱頂點鄰接到頂點,頂點鄰接自頂點(以頭爲主)。弧和頂點相關聯。
入度:以頂點v爲頭的弧的數目稱爲v的入度(InDegree),記爲ID(v)。
出度:以v爲結尾的弧的數目稱爲v的出度(OutDegree),記爲OD(v)。
度:頂點的度 TD = ID + OD
無向圖的路徑:從頂點到的路徑是一個頂點序列,其中
B到D四種路徑:
有向圖的路徑:路徑也是有向的,B到D只有兩種路徑
有向圖中根結點到任意結點的路徑是唯一的。
路徑的長度:路徑上的邊或弧的數目。
迴路/環:第一個頂點到最後一個頂點相同的路徑。
簡單路徑:序列中頂點不重複出現的路徑。
簡單迴路/環:除了第一個頂點和最後一個頂點之外,其餘頂點不重複出現的迴路,稱爲簡單迴路或簡單環。
途中的粗線都是環,左側是簡單環,右側因爲C頂點重複出現所以不是簡單環。
連通圖相關術語
連通圖:無向圖中,如果頂點之間有路徑,那麼稱這兩個頂點是連通的。如果對於圖中的任意頂點都是連通的,則稱該圖爲連通圖(Connected Graph)。
左邊不是連通圖,右邊是:
連通子圖:無向圖中的極大連通子圖成爲連通分量,要注意:
- 要是子圖
- 子圖要是連通的
- 連通子圖含有極大頂點數
- 具有極大頂點數的連通子圖包含依附於這些頂點的所有邊
下面圖2和圖3是圖1的連通分量,圖4雖然是圖1的子圖,但是不滿足連通子圖的極大的頂點數
非連通圖可以有連通分量
極大連通子圖:包含了原圖和子圖頂點關聯的所有邊
極小連通子圖:包含子圖連通必不可少的邊。
強連通圖:有向圖中,如果每一對頂點間都存在路徑,則稱G十強連通圖。
強連通分量:有向圖中的極大強連通子圖
下面圖1不是強連通圖,圖2是圖1的極大連通子圖
連通圖的生成樹:一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的n各頂點,但只有足以構成一棵樹的n-1條邊。
圖1不是,因爲邊太多了,還可以砍掉一些;
圖2和圖3是;
圖4雖然是n-1條邊,但是不連通。
有向樹:如果一個有向圖恰有一個頂點的入度爲0,其餘頂點的入度均爲1,則是一棵有向樹。
有向圖的生成森林:由若干棵有向數組成,含有圖中全部頂點,但是隻有構成有向樹不相交必須個數的弧。
圖1是有向圖,去掉一些弧後,分解爲兩棵有向樹,右邊的兩棵有向樹就是圖1有向圖的生成森林。
圖的存儲結構
圖的抽象數據類型:
鄰接矩陣
結構:圖的鄰接矩陣存儲方式是用兩個數組來表示圖。一個一維數組存儲頂點信息,一個二維數組(稱爲鄰接矩陣)存儲圖中的邊或弧信息。
nxn,右邊就設爲1,沒有就爲0,這樣不也是浪費很多空間嗎
無向圖:
無向圖的邊數組是一個對稱矩陣
優點:容易判定兩點間有無邊;容易計算度;
有向圖:
有向圖的矩陣並不對稱。
頂點的入度是第列各數之和,出度是第行個數之和。
有權圖:
鄰接矩陣存儲結構:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535 // 用65535表示無窮
typedef struct MGraph
{
VertexType vexs[MAXVEX]; // 頂點表
EdgeType arc[MAXVEX][MAXVEX]; // 鄰接矩陣
int numVertexes, numEdges; // 當前頂點數和邊數
} MGraph;
創建無向網圖:
// 建立無向網圖的鄰接矩陣表示
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
printf("輸入頂點數和邊數:\n");
scanf("%d, %d", &G->numVertexes, &G->numEdges);
for (i=0; i < G->numVertexes; i++) // 讀入頂點信息,建立頂點表
scanf(&G->vexs[i]);
for (i=0; i < G->numVertexes; i++) // 初始化鄰接矩陣
for (j=0; j < G->numEdges; j++)
G->arc[i][j] = INFINITY;
for (k=0; k < G->numEdges; k++) // 建立鄰接矩陣
{
printf("輸入前後下標i,j和權重w");
scanf("%d, %d, %d", &i, &j, &w);
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; // 無向圖,矩陣對稱
}
}
時間複雜度:
創建的複雜度:
初始化鄰接矩陣G->arc的複雜度:
鄰接表
當邊數相對於頂點數較少的時候,鄰接矩陣很浪費存儲空間,如下面的例子:
結構:數組和鏈表相結合的存儲方法稱爲鄰接表。
處理方法:
- 頂點用一個一維數組存儲,每個數據元素還要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息
- 每個頂點的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表存儲,無向圖稱爲頂點的邊表,有向圖則稱爲頂點作爲弧尾的出邊表
無向圖:
adjvex存放鄰接點域,爲結點下標。
有向圖:
有向圖的逆鄰接表,對每個頂點都建立一個鏈接爲爲弧頭的表。
逆的就是左邊爲弧尾,右邊爲弧頭
某個頂點的入度或出度可以通過鏈表長度判斷
對於帶權值的,還可以再加個數據域
結構定義:
typedef char VertexType;
typedef int EdgeType;
typedef struct EdgeNode // 邊表結點
{
int adjvex; // 鄰接點域,存儲頂點對應下標
EdgeType weight;
struct EdgeNode *next;
} EdgeNode;
typedef struct VertexNode // 頂點表結點
{
VertexType data;
EdgeNode *firstedge;
} VertexNode, AdjList[MAXVEX];
typedef struct GraphAdjList
{
AdjList adjList;
int numVertexes, numEdges;
} GraphAdjList;
無向圖的鄰接表:
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode *e;
printf("輸入頂點數和邊數");
scanf("%d, %d", &G->numVertexes, &G->numEdges);
for (i=0; i < G->numVertexes; i++) // 建立頂點表
{
scanf(&G->adjList[i].data); // 輸入頂點信息
G->adjList[i].firstedge = NULL; // 先把邊表設爲空
}
for (k=0; k < G->numEdges; k++) // 建立邊表
{
printf("輸入邊上的頂點序號");
scanf("%d, %d", &i, &j);
// 給i後邊的鏈表添加元素j,使用頭插法
e = (EdgeNode *) malloc(sizeof(EdgeNode)); // 生成邊表結點
e->adjvex = j; // 鄰接序號爲j
e->next = G->adjList[i].firstegde; // e指針指向當前頂點指向的結點
G->adjList[i].firstedge = e; // 將當前頂點指針指向e
// 因爲是無向所以是對稱的,也要個j後面加i
e = (EdgeNode *) malloc(sizeof(EdgeNode));
e->adjvex = i;
e->next = G->adjList[j].firstedge;
G->adjList[j].firstegde = e;
}
}
代碼說明:
- 先建立頂點表結點,同時輸入數據,把指向結點設爲NULL。
- 邊錶鏈表中的每個元素之間是沒有關聯的,每個元素是指向這個鏈表的頂點的鄰接點,所以如果有頂點的鄰接點就用頭插法插到鏈表中。
- 因爲是無向圖,所以是對稱的,要做兩遍插入。
- 時間複雜度爲。
十字鏈表
思想:把鄰接表和逆鄰接表結合起來。
頂點表結點結構:
firstin表示入邊表頭指針,指向該頂點的入邊表中第一個結點;
firstout表示出邊表頭指針,指向出邊表中的第一個結點。
邊表結點結構:
talvex是指弧起點在頂點表的下標;
headvex是指弧終點在頂點表中的下標;
headlink是指入邊表的指針域,指向終點相同的下一條邊;
taillink是指邊表指針域,指向起點相同的下一條邊;
網可以再加個weight域。
加入v0指向v1,然後在v0的指向v1的表的taillink中存放另一個指向v1的頂點?
核心理解:
- 通過firstin再加headlink可以找到該頂點的所有入邊
- 通過firstort再加taillink可以找到該頂點的所有出邊
實現思路:
比如輸入,此時先建立v1指向v0,新建一個邊表結點,讓v1的fisrtout指向新邊表結點,給邊表結點的tailvex和headvex賦值,同時取b0的fisrtin指向,如果爲空就賦值給firstin,如果不爲空就一直鄉下找headlink,直到找到爲空的headlink,將新邊表結點賦值給空的headlink。
優點:
- 求以某頂點爲頭/尾的弧很方便,也很容易得到出度和入度。
- 時間複雜度和鄰接表相同。
鄰接多重表
如果要對邊進行操作,那麼鄰接表就不方便了,如下圖,如果要去掉這條邊,對右邊的表的操作比較繁瑣。
邊表結點結構:
其中ivex和jvex是和某邊相關的兩個頂點在頂點表中的下標。
ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。
首先要記住幾點:
- ilink會指向頂點的另一條邊,而在另一條邊中,頂點肯定就不是了,此時是新邊的,所以ilink指向的結點的就是原來的。
- 如果有個結點,那麼右圖就會有條連線。
流程:
跟上邊的鄰接表差不多,之前的鄰接表是一個頂點連接結點的鏈表,而鄰接多重是採用指向的方法,一個頂點後面只有一個結點,通過結點來指向,這樣對邊操作就很方便,如果要刪除,只需要將6,9的鏈接設爲^即可。
邊集數組
結構:邊集數組是由兩個一維數組組成,一個存儲頂點信息;另一個存貯邊的信息,這個邊數組的每個數據元素由:一條邊的起點下標(begin)、終點下標(end)、權(weight)組成。
這個是最好理解的了,直接暴力存儲
邊數組結構:
JAVA實現鄰接表/矩陣的創建
出錯的點:
- 見了一個類的數組,但是沒有對每個數組元素做類的初始化
- 輸入的下標和頂點表中不匹配,所以多加了個位置
package Graph;
import java.util.Scanner;
class VertexList{
public char data;
public adjNode firstNode;
public int weight;
public VertexList(char data, int weight) {
this.data = data;
this.weight = weight;
firstNode = null;
}
}
class adjNode{
public int data;
public adjNode next;
public adjNode() {
next = null;
}
}
public class Base {
public static void main(String[] args) {
int[][] adjMat = CreAdjMat();
VertexList[] vertexLists = CreaAdjList();
}
private static VertexList[] CreaAdjList() {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入頂點個數和邊數:");
int numVer = scanner.nextInt();
int numEdge = scanner.nextInt();
VertexList[] vertexLists = new VertexList[numVer+1];
System.out.println("num ver" + numVer);
// 初始化頂點表
for (int i = 1; i < numVer+1; i++) {
System.out.println("輸入頂點:");
char data = scanner.next().charAt(0);
System.out.println("輸入頂點權重:");
int weight = scanner.nextInt();
// 忘了初始化了,要先初始化才能賦值
vertexLists[i] = new VertexList(data, weight);
}
// 輸入邊
// 注意下標,這裏我把長度都+1了
for (int i = 0; i < numEdge; i++) {
System.out.println("輸入邊的頂點序號:");
int s = scanner.nextInt();
int e = scanner.nextInt();
// 兩個頂點都接入邊,先從s開始
adjNode node1 = new adjNode();
node1.data = e;
// 用頭插法,原本的放在新的後面
node1.next = vertexLists[s].firstNode;
// 新的插在頂點後
vertexLists[s].firstNode = node1;
// 同樣的方法再處理e
adjNode node2 = new adjNode();
node2.data = s;
node2.next = vertexLists[e].firstNode;
vertexLists[e].firstNode = node2;
}
return vertexLists;
}
private static int[][] CreAdjMat() {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入的頂點數和邊數");
int numVer = scanner.nextInt();
int numEdge = scanner.nextInt();
int[] vertexes = new int[numVer];
int[][] edge = new int[numVer][numVer];
for (int i = 0; i < numVer; i++) {
for (int i1 = 0; i1 < numVer; i1++) {
edge[i][i1] = 0;
}
}
for (int i = 0; i < numEdge; i++) {
System.out.println("輸入邊:");
int v1 = scanner.nextInt();
int v2 = scanner.nextInt();
edge[v1][v2] = 1;
}
return edge;
}
}
圖的遍歷
定義:從圖中某一頂點出發遍歷圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫圖的遍歷(Traversing Graph)。
通常有兩種遍歷方案:一種是深度優先遍歷,一種是廣度優先遍歷。
深度優先遍歷
DFS:深度優先遍歷(Depth First Search),也有稱爲深度優先搜索,簡稱DFS。
類似找鑰匙的時候一個屋一個屋地搜,每個屋徹底搜完再搜下一個,類似於樹的前序遍歷。
右圖過程:
從圖中的某個頂點v出發,訪問此頂點,然後從v的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有路徑相同的頂點都被訪問到。
對於非連通圖,如果經過一次深度優先遍歷後仍有頂點沒有被訪問,則另選途中一個未被訪問的頂點作初始點,重複上述過程,直至圖中所有頂點都被訪問到爲止。
思路:
深度優先就是一條道走到黑,抓到一個輸出一個,所以用遞歸的方法來在一個點的鄰接域中做文章,然後鄰接域中的點再作爲頂點,如此遞歸。每個DFS中標記已經見過。
鄰接矩陣遍歷代碼:
鄰接矩陣就是標記0或1那個
typedef int Boolean;
Boolean visited[MAX]; // 訪問標誌的數組
// 鄰接矩陣的深度優先遞歸算法
// 打印所有和i點相鄰接的沒訪問過頂點
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
printf("%c", G.vexs[i]); // 打印頂點
for (j=0; j<G.numVertexes; j++)
// 和i相鄰接 且 沒訪問過
if (G.arc[i][j]==1 && !visited[j])
DFS(G, j);
}
// 鄰接矩陣的深度遍歷操作
void DFSTraverse(MGraph G)
{
int i;
for (i=0; i<G.numVertexes; i++)
visited[i] = FALSE; // 所有頂點狀態初始化爲未訪問
for (i=0; i<G.numVertexes; i++)
if (!visited[i]) // 對未訪問過的idan調用DFS,如果是連通圖,只會執行一次
DFS(G, i);
}
感覺這個方法比較簡單,相當於粗暴的對每個頂點遍歷,再遍歷頂點的鄰接點,從某點出就是深度的意思嗎?
鄰接表結構的遍歷代碼:
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
printf("%c", GL->adjList[i].data); // 操作
p = GL->adjList[i].firstedge;
while (p)
{
// 思路差不多,順着鏈表一直往下找,同時每次找到的新頂點都做DFS
if (!visited[p->adjvex])
DFS(GL, p->adjvex);
p = p->next;
}
}
void DFSTraverse(GraphAdjList GL)
{
int i;
for (i=0; i<GL->numVertexes; i++)
visited[i] = FALSE; // 初始爲未訪問
for (i=0; i<GL->numVertexes; i++)
if(!visited[i])
DFS(GL, i);
}
兩種結構遍歷的時間複雜度:
- 鄰接矩陣需要訪問矩陣中的所有元素,因此爲
- 鄰接表需要的時間取決於頂點 n 和邊 e 的數量,爲
對於點多邊少的稀疏圖來說,鄰接表的效率更高。
廣度優先遍歷
BFS:廣度優先遍歷(Breadth First Search),又稱爲廣度優先搜索,簡稱BFS。
類似找鑰匙先把每個房間大概看一遍,慢慢擴大範圍,類似於樹的層序遍歷。
重構了一下左邊的圖,變成了右邊,邊和頂點的關係是不變的:
選擇A爲第一層;選擇A的鄰接點BF爲第二層;選擇BF的鄰接點CIGE爲第三層;選擇CIGE的鄰接點DH爲第四層。
思路:
廣度優先就是平向來做,一層一層的處理,通過同一頂點的鄰接點都入棧來完成,而不是從一個鄰接點一直深入。入棧的時候纔可標記爲已經見過。
鄰接矩陣遍歷代碼:
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for (i=0; i<G.numVertexes; i++)
visited[i] = FALSE:
InitQueue(&Q); // 初始化一個輔助隊列
for (i=0; i<G.numVertexes; i++)
{
if (!visited[i]) // 處理未被訪問的
{
visited[i] = TRUE;
printf("%c", G.vexs[i]);
EnQueue(&Q, i); // 將此頂點入隊列
while (!QueueEmpty(Q)) // 若當前隊列不爲空
{
DeQueue(&Q, &i); // 將隊中元素出隊列,賦值給i
for (j=0; j<G.numVertexes; j++)
{
// 判斷兩點間是否存在邊
if (G.arc[i][j]==1 && !visited[j])
{
visited[j] = TRUE;
printf("%c", G.vexs[j]); // 打印頂點
EnQueue(&Q, j); // 將找到的點加入隊列
}
}
}
}
}
}
流程:
這種方法利用了隊列的先進先出,遍歷的時候讀取到的鄰接點都放入隊列中,這樣就能一批一批地處理,而不是像深度那樣抓住一個查到底。
鄰接表遍歷代碼:
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for (i=0; i<GL->numVertexes; i++)
visited[i] = False;
InitQueue(&Q);
for (i=0; i<GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i] = TRUE;
printf("%c", GL->adjList[i].data);
EnQueue(&Q, i);
while (!QueueEmpty(Q))
{
DeQueue(&Q, &i);
p = GL->adjList[i].firstedge; // 找到該頂點的表頭指針
// 把一串鏈表都送進隊列
while (p)
{
if (!visited[p->adjvex])
{
visited[p->adjvex] = TRUE;
printf("%c", GL->adjList[p->adjvex].data);
EnQueue(&Q, p->adjvex);
}
p = p->next;
}
}
}
}
}
JAVA實現深度/廣度遍歷
package Graph;
import java.util.ArrayList;
import java.util.List;
class Queue{
private int top;
private int max;
private int[] list;
public Queue(int max) {
this.top = 0;
this.list = new int[max];
}
public void push(int idx){
list[top++] = idx;
}
public int pop(){
top--;
return list[top];
}
public int Empty(){
if (top==0){
return 1;
}
else {
return 0;
}
}
}
public class Travers {
public static void main(String[] args) {
// 手動鄰接矩陣
// 手動鄰接表
VertexList[] vertexList = new VertexList[10];
int[][] edgeList = new int[16][2];
// 初始化
int[][] edgeMat = new int[10][10];
InitVerAjd(vertexList, edgeList);
// 創建鄰接表
CreateAdjList(vertexList, edgeList);
// 測試創建是否正確
// System.out.println(vertexList[3].data);
// System.out.println(vertexList[3].firstNode.data);
// System.out.println(vertexList[3].firstNode.next.data);
// System.out.println(vertexList[3].firstNode.next.next.data);
// 創建鄰接矩陣
CreateAdjMat(edgeList, edgeMat);
// 鄰接矩陣的深度遍歷
System.out.println("鄰接矩陣的深度遍歷------------");
TraverMatDFS(vertexList, edgeMat);
// 鄰接表的深度遍歷
System.out.println("鄰接表的深度遍歷------------");
TraverListDFS(vertexList, edgeList);
// 鄰接矩陣的廣度遍歷
System.out.println("鄰接矩陣的廣度遍歷------------");
TraverMatBFS(vertexList, edgeMat);
// 鄰接表的廣度遍歷
System.out.println("鄰接表的廣度遍歷------------");
TraverListBFS(vertexList, edgeList);
}
private static void TraverListBFS(VertexList[] vertexList, int[][] edgeList) {
int len = vertexList.length;
int[] visit = new int[len];
for (int i = 1; i < len; i++) {
visit[i] = 0;
}
Queue que = new Queue(len-1);
for (int i = 1; i < len; i++) {
if (visit[i] == 0){
que.push(i);
visit[i] = 1;
System.out.println(vertexList[i].data);
while (que.Empty() == 1){
int idx = que.pop();
adjNode q = vertexList[idx].firstNode;
// 順着鄰接表做廣度
while (q != null){
if (visit[q.data]==0){
// 鄰接入棧
que.push(q.data);
visit[q.data] = 1;
System.out.println(vertexList[q.data].data);
}
q = q.next;
}
}
}
}
}
private static void TraverMatBFS(VertexList[] vertexList, int[][] edgeMat) {
int len = vertexList.length;
int[] visit = new int[len];
for (int i = 1; i < len; i++) {
visit[i] = 0;
}
Queue que = new Queue(len-1);
for (int i = 1; i < len; i++) {
if (visit[i] == 0){
que.push(i);
visit[i] = 1;
System.out.println(vertexList[i].data);
// 棧裏有就一直找
while (que.Empty() == 0){
int idx = que.pop();
// 並不是從這裏標記是否看見,入棧的時候才能標記
// 從i的鄰接點下手,沒見過的都入棧
for (int i1 = 1; i1 < len; i1++) {
// 沒見過且有鄰接
if (visit[i1]==0 && edgeMat[idx][i1]==1){
// 打印且入棧
System.out.println(vertexList[i1].data);
que.push(i1);
visit[i1] = 1;
}
}
}
}
}
}
private static void TraverListDFS(VertexList[] vertexList, int[][] edgeList) {
int len = vertexList.length;
int[] visit = new int[len];
for (int i = 1; i < len; i++) {
visit[i] = 0;
}
for (int i = 1; i < len; i++) {
if (visit[i]==0){
DFSList(vertexList, visit, i);
}
}
}
private static void DFSList(VertexList[] vertexList, int[] visit, int idx) {
visit[idx] = 1;
System.out.println(vertexList[idx].data);
// 往後找
adjNode p = vertexList[idx].firstNode;
while (p != null){
if (visit[p.data]==0){
DFSList(vertexList, visit, p.data);
}
p = p.next;
}
}
// 深度優先鄰接矩陣用
private static void DFSMat(VertexList[] vertexList, int[][] edgeMat, int[] visit, int idx){
int len = edgeMat.length;
// 這兩個在循環的外面,每次調用DFS會打印一個
// 打印和控制都在循環外面進行
visit[idx] = 1;
System.out.println(vertexList[idx].data);
for (int i = 1; i < len; i++) {
if(edgeMat[idx][i]==1 && visit[i]==0){
DFSMat(vertexList, edgeMat, visit, i);
}
}
}
// 深度優先鄰接矩陣遍歷
private static void TraverMatDFS(VertexList[] vertexList, int[][] edgeMat) {
int len = vertexList.length;
int[] visit = new int[len];
for (int i = 0; i < len; i++) {
visit[i] = 0;
}
for (int i = 1; i < len; i++) {
if(visit[i]==0){
DFSMat(vertexList, edgeMat, visit, i);
}
}
}
private static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
int len0 = edgeMat.length;
for (int i = 0; i < len0; i++) {
for (int i1 = 0; i1 < len0; i1++) {
edgeMat[i][i1] = 0;
}
}
int len = edgeList.length;
for (int i = 0; i < len; i++) {
int s = edgeList[i][0];
int e = edgeList[i][1];
edgeMat[s][e] = 1;
edgeMat[e][s] = 1;
}
}
private static void CreateAdjList(VertexList[] vertexList, int[][] edgeList) {
int len = edgeList.length;
for (int i = 1; i < len; i++) {
int s = edgeList[i][0];
int e = edgeList[i][1];
adjNode node1 = new adjNode();
node1.data = s;
node1.next = vertexList[e].firstNode;
vertexList[e].firstNode = node1;
adjNode node2 = new adjNode();
node2.data = e;
node2.next = vertexList[s].firstNode;
vertexList[s].firstNode = node2;
}
}
private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
vertexList[1] = new VertexList('A',0);
vertexList[2] = new VertexList('B',0);
vertexList[3] = new VertexList('C',0);
vertexList[4] = new VertexList('D',0);
vertexList[5] = new VertexList('E',0);
vertexList[6] = new VertexList('F',0);
vertexList[7] = new VertexList('G',0);
vertexList[8] = new VertexList('H',0);
vertexList[9] = new VertexList('I',0);
// 1
edgeList[1][0] = 1;
edgeList[1][1] = 2;
// 2
edgeList[2][0] = 2;
edgeList[2][1] = 3;
// 3
edgeList[3][0] = 3;
edgeList[3][1] = 4;
// 4
edgeList[4][0] = 4;
edgeList[4][1] = 5;
// 5
edgeList[5][0] = 5;
edgeList[5][1] = 6;
// 6
edgeList[6][0] = 6;
edgeList[6][1] = 1;
// 7
edgeList[7][0] = 2;
edgeList[7][1] = 7;
// 8
edgeList[8][0] = 7;
edgeList[8][1] = 6;
// 9
edgeList[9][0] = 2;
edgeList[9][1] = 9;
// 10
edgeList[10][0] = 3;
edgeList[10][1] = 9;
// 11
edgeList[11][0] = 9;
edgeList[11][1] = 4;
// 12
edgeList[12][0] = 4;
edgeList[12][1] = 8;
// 13
edgeList[13][0] = 7;
edgeList[13][1] = 4;
// 14
edgeList[14][0] = 8;
edgeList[14][1] = 5;
// 15
edgeList[15][0] = 7;
edgeList[15][1] = 8;
}
}
最小生成樹
定義:把構造連通網的最小代價生成樹稱爲最小生成樹(Minimum Cost Spanning Tree)。
Prim算法
lowcost數組的作用:
長度爲頂點個數的一維數組,初始化下標爲0的位置的值爲0,其他全爲無窮大,下標指向的內容爲0時,說明下標代表的頂點已經在最小生成樹中了。
這個數組用來存放所有未在最小生成樹中的頂點中,距離已經在最小生成樹中的頂點的最小距離,簡單來說就是未在最小生成樹中的頂點和最小生成樹的最小距離。最小生成樹中尚未有鄰接點的頂點的值,設置爲無窮大。
adjvex數組的作用:
lowcost中存放的是頂點和最小生成樹的最小距離,adjvex數組來記錄離最小生成樹最近的點和最小生成樹中哪個點距離最近,數組離存放對應的最小生成樹的點,以此來生成邊。
算法流程:
- 創建一個lowcost和一個adjvex數組,長度都爲頂點數,lowcost全部初始化爲無窮,然後第一個值設爲0,表示結點已經在最小生成樹中;adjvex數組全部初始化爲0;
- 對每個頂點進行遍歷,每次找lowcost中的權值最小的位置,然後用adjvex來知道是距離最小生成樹中哪個點最近,這時就能生成一個邊(adjvex[k], k);
- 設lowcost[k]=0,就是將該點納入最小生成樹中;
- 此時再循環所有的頂點,如果有離最小生成樹距離更近的,就更新adjvex的值,記錄對應位置。
代碼實現:
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; // 保存相關下標
int lowcost[MAXVEX]; // 保存權值
// 初始化
lowcost[0] = 0;
adjvex[0] = 0;
for (i=1; i<G.numVertexes; i++)
{
lowcost[i] = G.arc[0][i];
adjvex[i] = 0;
}
for (i=1; i<G.numVertexes; i++)
{
min = INFINITY;
j = 1;
k = 0;
while (j < G.numVertexes) // 循環全部的點
{
// 找權值最小的點
if (lowcost[j]!=0 && lowcost[j]<min)
{
min = lowcost[j];
k = j;
}
j++;
}
// k離最小生成樹中最近的點是adjvex[k]
printf("(%d, %d)", adjvex[k], k); // 打印當前頂點邊中權值最小邊
lowcost[k] = 0; // 當前頂點的權值設置爲0, 表示此頂點已經完成任務
// 新點進去,檢測是否會讓剩下的點離最小生成樹更近
for (j=1; j<G.numVertexes; j++)
{
// 尋找剛找出的k的鄰接點中,離最小生成樹更近的點
if (lowcost[j]!=0 && G.arc[k][j] <lowcost[j])
{
// 若下標爲k頂點某鄰接邊權值小於此前選中的點,就更新
lowcost[j] = G.arc[k][j]; // 將較小權值存入lowcost
// 因爲選出的點離k最近,所以要更新一下
adjvex[j] = k; // 將下標爲k的頂點存入adjvex
}
}
}
}
時間複雜度:
Kruskal算法
Prim算法是從頂點入手的,而Kruskal是從邊開始入手,這裏用到了邊集數組結構。
邊的結構代碼:
typedef struct Edge
{
int begin;
int end;
int weight;
};
將邊按權重排序:
算法流程
- 建一個parent數組,用來判斷是否構成環路
- 每條邊遍歷
- 檢測是否構成環,沒有的話就用邊的頭作下標,尾作值,放到parent中
案例:
按照代碼來,到了i=6的時候,已經獲得的邊如圖粗體所示(24和26權重的邊應該是印錯了):
在這之前都是n!=m,這是相當於兩個連通的邊集合納入到了最小生成樹中;
i = 7時,到了邊,此時兩個Fine都會返回6,可知會在A中構成一個環路,所以不要這條邊;
i = 8時,到了邊也會讓A構成環路,所以也不要;
i = 9時,連接,此後的均會構成環路,最小生成樹此時得到:
思路:
直接從邊入手,找最小的,把所有的構不成環路的最小的連接起來,前面的連接是沒有問題的,到了n=m的時候,此時這個邊想進去集合已經來不及了,有跟他作用相同但是更短的邊在裏面。
時間複雜度:
兩種方法對比:
- Kruskal主要針對邊展開,邊數少的時候效率高,所以對稀疏圖有很大優勢
- Prim對稠密圖,邊很多的情況會更好些
JAVA實現Prim算法
public class MinTree {
public static void main(String[] args) {
VertexList[] vertexLists = new VertexList[10];
int[][] edgelist = new int[16][3];
int[][] edgeMat = new int[10][10];
InitVerAjd(vertexLists, edgelist);
CreateAdjMat(edgelist, edgeMat);
// Prim需要用到鄰接權值矩陣
// 權值矩陣是沒有問題的
// for (int i = 1; i < 10; i++) {
// System.out.println("第 " + i + "行");
// for (int j = 1; j < 10; j++){
// System.out.println(edgeMat[i][j]);
// }
// }
// Prim(vertexLists, edgeMat);
Kruskal(vertexLists, edgelist, edgeMat);
}
private static void Prim(VertexList[] vertexLists, int[][] edgeMat) {
int len = vertexLists.length;
int[] adjvex = new int[len];
int[] lowcost = new int[len];
for (int i = 1; i < len; i++) {
// 賦值第一個點的鄰接邊的權重
// 這裏搞錯了
lowcost[i] = edgeMat[1][i];
// 初始化爲1, 就是生活樹中只有第一個頂點
adjvex[i] = 1;
}
// 設爲0表示已經進樹了
lowcost[1] = 0;
// 每次選出一個頂點
for (int i = 1; i < len-1; i++) {
int min = Integer.MAX_VALUE;
int k = 0;
// 不會算第一個點本身
for (int j = 2; j < len; j++) {
if (lowcost[j]!=0 && lowcost[j]<min){
min = lowcost[j];
k = j;
}
}
// 該點被選出
lowcost[k] = 0;
System.out.println("第" + i + "條邊:" + adjvex[k] + "->" + k);
// 更新lowcost和adjvex
for (int m = 2; m < len; m++) {
// 看有無更近
if (lowcost[m]!=0 && lowcost[m]>edgeMat[k][m]){
// 更近就更新
lowcost[m] = edgeMat[k][m];
// 得記錄和那個更近
adjvex[m] = k;
}
}
}
}
// 這次的矩陣帶全值了
public static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
// 長度搞錯了
int lenVer = edgeMat.length;
for (int i = 1; i < lenVer; i++) {
for (int i1 = 1; i1 < lenVer; i1++) {
// 不相連的距離設爲無窮大
edgeMat[i][i1] = Integer.MAX_VALUE;
}
}
int len = edgeList.length;
for (int i = 1; i < len; i++) {
int s = edgeList[i][0];
int e = edgeList[i][1];
edgeMat[s][e] = edgeList[i][2];
edgeMat[e][s] = edgeList[i][2];
}
}
private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
vertexList[1] = new VertexList('A',0);
vertexList[2] = new VertexList('B',0);
vertexList[3] = new VertexList('C',0);
vertexList[4] = new VertexList('D',0);
vertexList[5] = new VertexList('E',0);
vertexList[6] = new VertexList('F',0);
vertexList[7] = new VertexList('G',0);
vertexList[8] = new VertexList('H',0);
vertexList[9] = new VertexList('I',0);
// 1
edgeList[1][0] = 1;
edgeList[1][1] = 2;
edgeList[1][2] = 10;
// 2
edgeList[2][0] = 2;
edgeList[2][1] = 3;
edgeList[2][2] = 18;
// 3
edgeList[3][0] = 3;
edgeList[3][1] = 4;
edgeList[3][2] = 22;
// 4
edgeList[4][0] = 4;
edgeList[4][1] = 5;
edgeList[4][2] = 20;
// 5
edgeList[5][0] = 5;
edgeList[5][1] = 6;
edgeList[5][2] = 26;
// 6
edgeList[6][0] = 6;
edgeList[6][1] = 1;
edgeList[6][2] = 11;
// 7
edgeList[7][0] = 2;
edgeList[7][1] = 7;
edgeList[7][2] = 16;
// 8
edgeList[8][0] = 7;
edgeList[8][1] = 6;
edgeList[8][2] = 17;
// 9
edgeList[9][0] = 2;
edgeList[9][1] = 9;
edgeList[9][2] = 12;
// 10
edgeList[10][0] = 3;
edgeList[10][1] = 9;
edgeList[10][2] = 8;
// 11
edgeList[11][0] = 9;
edgeList[11][1] = 4;
edgeList[11][2] = 21;
// 12
edgeList[12][0] = 4;
edgeList[12][1] = 8;
edgeList[12][2] = 16;
// 13
edgeList[13][0] = 7;
edgeList[13][1] = 4;
edgeList[13][2] = 24;
// 14
edgeList[14][0] = 8;
edgeList[14][1] = 5;
edgeList[14][2] = 7;
// 15
edgeList[15][0] = 7;
edgeList[15][1] = 8;
edgeList[15][2] = 19;
}
}
最短路徑
定義:對於網圖來說,最短路徑,是指兩頂點之間經過的邊上的權值之和最少的路徑,並且我們稱路徑上的第一個點是源點,最後一個頂點是終點。
Dijkstra算法
思路:
- 一步步求出所有頂點的最短路徑, 後面的頂點路徑尋找是建立在前面的尋找的基礎上的,通過不斷遍歷最近點的鄰接點來更新到v0點的距離。
- 會進行很多次最近點的鄰接點和當付錢最近距離的比較,以此來找到最優解。
算法流程:
- 新建三個數組,P,D,final,這三個的長度都是頂點數,P用來存放最短路徑,裏面的值指向最短路徑的前一個頂點;D中存放v0到下標位置點的最短路徑距離;final來記錄當前頂點是否已經算過最短路徑
- 初始化三個數組,D使用鄰接矩陣的v0行初始化,final全爲0表示沒被計算過,P全爲0表示未有路徑。
- 一個大循環,裏面套了兩個小循環,大循環對每個頂點進行遍歷
- 第一個小循環用來尋找D中未被計算過的最小距離和對應的頂點,用final來標記是否計算過,k值用來保存頂點位置
- 第二個小循環遍歷所有的頂點,讓v0到k點的最小距離+k到各個頂點的距離和之前記錄D中的v0到各個頂點的距離比較,如果能更近就更新,同時保存路徑到P
實現代碼:
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; // 存儲最短路徑下標的數組
typedef int ShortPathTable[MAXVEX]; // 存儲到各點最短路徑的權值和
// P[v]的值爲前驅頂點下標,D[v]表示v0到v的最短路徑長度和
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *P, ShortPathTable *D)
{
int v, w, k, min;
int final[MAXVEX]; // final[w]=1 表示求得頂點v0至vw的最短路徑
for (v=0; v<G.numVertexes; v++) // 初始化數據
{
final[v] = 0; // 全部頂點初始化爲未知最短路徑狀態
(*D)[v] = G.matrix[v0][v]; // 和v0鄰接的頂點的權值
(*P)[v] = 0; // 初始化路徑數組P爲0
}
(*D)[v0] = 0; // v0到v0的路徑
final[v0] = 1;
// 主循環,求v0到各個頂點的最短路徑
for (v=1; v<G.numVertexes; v++)
{
min = INFINITY;
for (w=0; w<G.numVertexes; w++) // 尋找離v0最近的頂點
{
// 爲什麼不能重複找呢
// 因爲如果不限定final的話,就一直是最小那個,無法向後推進
if (!final[w] && (*D)[w]<min) // 從未被找到的點裏找
{
k = w;
min = (*D)[w];
}
}
// 最近的點k被考慮過
final[k] = 1;
//
for (w=0; w<G.numVertexes; w++)
{
// 如果經過k頂點的路徑比現在這條路徑的長度短的話
// k是剩餘點中離v0最近的
if (!final[w] && (min+G.matrix[k][w] < (*D)[w]))
{
// 說明找到了最短路徑,修改D[w]和P[w]
(*D)[w] = min + G.matrix[k][w];
(*P)[w] = k;
}
}
}
}
結果圖:
時間複雜度:遍歷嵌套遍歷,爲,如果要求所有頂點到所有頂點的最短路徑,就是。
Floyd算法
依次計算所有頂點經過某點後到達另一頂點的最短路徑。
思路:
- D數組存距離,P數組存路徑
- 三個遍歷,K表示中轉頂點,考慮中轉比直線更近的情況;V表示起始頂點;W表示結束頂點
- K在最外層,每個頂點之間都計算了中轉,這裏只要一箇中轉就能表達所有情況,因爲中轉的起點是之前計算出來的最短路徑,已經積累了很多中轉
- 最裏層的循環判斷是否中轉更近,是的話就更新距離和路徑,讓原本值爲w的P[v][w]指向中轉點,即改變了起點的後期,把中轉點插到了原本的起點和終點之間,這樣起點指向的就是第一個中轉點了。
- 最後的一個遍歷可讓路徑一直最短,從前到後一點點積累中轉點,到最後總有不可再插入中轉點的時候,此時path[k][w]=w,路徑的尋找就結束了。
最短路徑計算代碼實現
typedef int Pathmatrix[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
void ShortestPath_Floyd(MGraph G, Pathmatrix *p, ShortPathTable *D)
{
int v, w, k;
for (v=0; v<G.numVertexes; ++v)
{
for (w=0; w<G.numVertexes; ++v) // 初始化D與P
{
(*D)[v][w] = G.matrix[v][w];
(*P)[v][w] = w;
}
}
for (k=0; k<G.numVertexes; ++k)
{
for (v=0; v<G.numVertexes; ++v)
{
for (w=0; w<G.numVertexes; ++w)
{
// 比較中轉近還是直接近
if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
{
(*D)[v][w] = (*D)[v][k] + (*D)[k][w];
(*P)[v][w] = (*P)[v][k]; // 路徑設置爲中轉點k
}
}
}
}
}
求最短路徑:
利用P來求出兩個點之間的最短路徑,比如求v0到v8,設P[0][8]=m,m爲0-8之間的一個點,然後再以m爲起點,得到P[m][8]=n,n是m和8之間的一箇中轉點,再以n爲起點,得到P[n][8]=…,這個過程中得到的點就是路徑。
最短路徑顯示代碼:
for (v=0; v<G.numVertexes; ++v)
{
for (w=v+1; w<G.numVertexews; w++)
{
// 求v和w之間的路徑
printf("v%d-v%d weight: %d",v,w,D[v][w]);
k = P[v][w];
printf("path: %d", v); // 打印源點
while(k!=w)
{
printf(" -> %d", k);
k = P[k][w];
}
printf(" -> %d\n", w);
}
printf("\n");
}
使用環境:
要求所有頂點到所有頂點的最短路徑問題時,使用Floyd。
JAVA實現Dijkstra和Floyd算法
注意:設置最大值的時候不要使用數據類型的最大值(如Integer.MAX_VALUE
),因爲又可能出現min+Integer.MAX_VALUE
數據溢出變成負數的情況。
public class ShortesPath {
public static void main(String[] args) {
VertexList[] vertexLists = new VertexList[10];
int[][] edgeList = new int[17][3];
int[][] edgeMat = new int[10][10];
InitShortPathVerAjd(vertexLists, edgeList);
utils.CreateAdjMat(edgeList, edgeMat);
// Dijkstra算法
Dijkstra(edgeMat);
// Floyd算法
Floyd(edgeMat);
}
private static void Floyd(int[][] edgeMat) {
int len = edgeMat.length;
// 距離數組和路徑數組,因爲這次是兩層遍歷方法,所以用二維數組
int[][] Dis = edgeMat.clone();
int[][] Path = new int[len][len];
// 初始化Path爲終點
for (int v = 1; v < len; v++) {
for (int w = 1; w < len; w++) {
// v表示起點,w表示終點
Path[v][w] = w;
}
}
// k爲中轉點
for (int k = 1; k < len; k++) {
// 所有的頂點之間都做中轉
for (int v = 1; v < len; v++) {
for (int w = 1; w < len; w++) {
// 如果中專的距離更近,就更新距離和路徑爲中賺
if (Dis[v][w] > Dis[v][k]+Dis[k][w]){
Dis[v][w] = Dis[v][k]+Dis[k][w];
// 相當於改變了起點的後驅,把中轉點插到了原本的起點和頂點之間
Path[v][w] = Path[v][k];
}
}
}
}
FloydForeach(Path);
}
private static void FloydForeach(int[][] path) {
int v = 1;
int w = 8;
int k = path[v][w];
System.out.println(v + "到" + w + "的最短路徑");
System.out.printf((k-1)+" ");
while (k!=w){
// 起點隨着中轉帶你一直在變,終點不變
k = path[k][w];
System.out.printf("-> " + (k-1) + " ");
}
System.out.println("-> " + k);
}
private static void Dijkstra(int[][] edgeMat) {
int len = edgeMat.length;
for (int i = 1; i < len; i++) {
for (int j = 1; j < len; j++) {
System.out.printf(edgeMat[i][j] + " ");
}
System.out.println("");
}
System.out.println("----------------");
// 創建數組,假設下標從1開始
int[] Final = new int[len];
int[] Dis = new int[len];
int[] Path = new int[len];
// 初始化
for (int i = 1; i < len; i++) {
// 全部爲未知
Final[i] = 0;
// 以第一個頂點爲起點
Dis[i] = edgeMat[1][i];
// 路徑都設爲第一個頂點
Path[i] = 1;
}
Final[1] = 1;
Dis[1] = 0;
// 大遍歷
for (int i = 1; i < len; i++) {
// 找離v0最近的沒被考慮過的點
int min = 65535;
// 用來記錄最近
int k = 0;
for (int w = 1; w < len; w++) {
if (Final[w]==0 && Dis[w] < min){
k = w;
min = Dis[w];
}
}
// 設k頂點爲已被考慮過
Final[k] = 1;
// 看是否能通過k更近
for (int w = 1; w < len; w++) {
if (Final[w]==0 && (min+edgeMat[k][w] < Dis[w])){
// 更近就更新距離和路徑
System.out.println("更新前到頂點"+w +"的距離: " + Dis[w]);
Dis[w] = min + edgeMat[k][w];
System.out.println("更新後到頂點"+w +"的距離: " + Dis[w]);
// 更新前驅點,通過k連接w會更近
Path[w] = k;
}
}
}
for (int i = 1; i < len; i++) {
System.out.println(i-1+"的前驅是"+(Path[i]-1));
}
}
private static void InitShortPathVerAjd(VertexList[] vertexList, int[][] edgeList) {
vertexList[1] = new VertexList('A',0);
vertexList[2] = new VertexList('B',0);
vertexList[3] = new VertexList('C',0);
vertexList[4] = new VertexList('D',0);
vertexList[5] = new VertexList('E',0);
vertexList[6] = new VertexList('F',0);
vertexList[7] = new VertexList('G',0);
vertexList[8] = new VertexList('H',0);
vertexList[9] = new VertexList('I',0);
// 1
edgeList[1][0] = 1;
edgeList[1][1] = 0;
edgeList[1][2] = 1;
// 2
edgeList[2][0] = 0;
edgeList[2][1] = 2;
edgeList[2][2] = 5;
// 3
edgeList[3][0] = 1;
edgeList[3][1] = 2;
edgeList[3][2] = 3;
// 4
edgeList[4][0] = 1;
edgeList[4][1] = 3;
edgeList[4][2] = 7;
// 5
edgeList[5][0] = 1;
edgeList[5][1] = 4;
edgeList[5][2] = 5;
// 6
edgeList[6][0] = 2;
edgeList[6][1] = 4;
edgeList[6][2] = 1;
// 7
edgeList[7][0] = 2;
edgeList[7][1] = 5;
edgeList[7][2] = 7;
// 8
edgeList[8][0] = 3;
edgeList[8][1] = 4;
edgeList[8][2] = 2;
// 9
edgeList[9][0] = 4;
edgeList[9][1] = 5;
edgeList[9][2] = 3;
// 10
edgeList[10][0] = 3;
edgeList[10][1] = 6;
edgeList[10][2] = 3;
// 11
edgeList[11][0] = 4;
edgeList[11][1] = 6;
edgeList[11][2] = 6;
// 12
edgeList[12][0] = 4;
edgeList[12][1] = 7;
edgeList[12][2] = 9;
// 13
edgeList[13][0] = 7;
edgeList[13][1] = 5;
edgeList[13][2] = 5;
// 14
edgeList[14][0] = 6;
edgeList[14][1] = 7;
edgeList[14][2] = 2;
// 15
edgeList[15][0] = 6;
edgeList[15][1] = 8;
edgeList[15][2] = 7;
// 16
edgeList[16][0] = 7;
edgeList[16][1] = 8;
edgeList[16][2] = 4;
// 搞錯了,應從1開始
for (int i = 0; i < 16; i++) {
edgeList[i+1][0] += 1;
edgeList[i+1][1] += 1;
}
}
}
拓撲排序
AOV網:在一個表示工程的有向圖中,用頂點表示活動,用弧表示活動之間的優先關係,這樣的有向圖爲頂點表示活動的網,我們稱爲AOV網(Activity On Vertex Network)。
AOV網中的弧表示活動之間存在的某種制約關係,且不允許存在迴路,下面是一個簡單的例子:
拓撲序列:
- 設G=(V, E)是一個具有n個頂點的有向圖,V中的頂點是有序列的,如果滿足從到之間有一個條路經,則在頂點序列中在前面的化,就稱這樣的頂點序列爲一個拓撲序列。
- 對於一個AOV網,拓撲序列可能不止一條。
- 所謂的拓撲排序,其實就是一個有向圖構造拓撲序列的過程。
拓撲排序算法
對AOV網進行拓撲排序的思路:從AOV網中選擇一個入度爲0的頂點輸出,然後刪去此頂點和以此頂點爲尾的弧,繼續重複,直到輸出全部頂點或AOV網中不存在入度爲0的頂點爲止。
數據結構:
AOV網:
鄰接表數據結構:
結構代碼:
typedef struct EdgeNode // 邊表結點
{
int adjvex; // 鄰接點域,存儲該頂點對應的下標
int weight; // 用於存儲權值,非網圖不需要
struct EgdeNode *next; // 鏈域,指向下一個鄰接點
}EdgeNode;
typedef struct VertexNode // 頂點表結點
{
int in; // 頂點入度
int data; // 頂點域
EgdeNode *firstedge;
}VertexNode, AdjList[MAXVEX];
typedef struct graphAdjList
{
AdjList adjList;
int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;
思路:
- 在算法中用棧來處理入度爲0的頂點,目的是爲了避免每個查找時都要遍歷找入度爲0的頂點。
- 從入度爲0開始遍歷,入度爲0表示該點不會作爲弧頭,所以把這種點放在前面不會有問題,每打印一個,就相當於抹掉一個點,此時這個點的鄰接點的入度就-1,如果入度減到了0,那麼該點此時放入棧也是安全的,後面不會有讓該點作爲弧頭的弧。
理解:
- 感覺先輸出的都是後面的點啊?
不是的,後面進去的都是通過前一個點(該點已被打印)的消除而入度爲0的,而且都是跟在後面的鄰接點。 - 那麼最初的幾個入度爲0如何保證呢?還是說從弧找順序而不是從順序中找弧?
應該是第一種,先給弧,再看順序,所以入度爲0的幾個順序無所謂。
// 拓撲排序,若GL無迴路,則輸出序列
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top=0; // 用於棧指針下標
int count=0; // 統計輸出的頂點個數
int *stack; // 建棧存儲入度爲0的頂點
stack = (int *)malloc(GL->numVertexes *sizeof(int));
for (i=0; i<GL->numVertexes; i++)
if (GL->adjList[i].in == 0)
stack[++top] = i; // 入度爲0的頂點入棧
while(top != 0)
{
gettop = stack[top--]; //出棧打印
printf("%d -> ", GL->adjList[gettop].data);
count++; // 統計輸出頂點數
for (e=GL->adjList[gettop].firstedge; e ;e=e->next)
{
// 對頂點的弧邊遍歷
k = e->adjvex;
if (!(--GL->adjList[k].in)) // k號頂點的鄰接點的入度減1
stack[++top] = k; // 入度變成0了則入棧,以便下次循環輸出
}
}
if (count < GL-<numVertexes) // 數量不夠,則存在環
return ERROE;
else
return OK;
}
JAVA實現拓撲排序
頂點類:
public class aovNode {
public int in;
public int data;
public adjNode firstedge;
public aovNode(int in, int data) {
this.in = in;
this.data = data;
}
}
排序實現:
public class TopologySort {
public static void main(String[] args) {
// 14個頂點
int len = 6;
aovNode[] verList = new aovNode[len+1];
// 這裏是有向邊, 七條邊
int[][] edgeList = new int[8][2];
InitTopoplogy(verList, edgeList);
CrateAdjList(verList, edgeList);
Sort(verList, edgeList);
}
private static void Sort(aovNode[] verList, int[][] edgeList) {
int len = verList.length;
// 做一個棧存放入度爲0的點
int[] stack = new int[len];
int top = 0;
int count = 0;
int ver = 0;
adjNode e = null;
for (int i = 1; i < len; i++) {
if (verList[i].in == 0){
stack[top++] = i;
}
}
// 開始遍歷,對點進行處理
while (top != 0){
// 在這裏打印
ver = stack[--top];
System.out.println(ver + "->");
count++;
// 對該點的鄰接點進行處理
for (e = verList[ver].firstedge; e!=null; e=e.next){
// 如果鄰接點的入度-1爲0的話,就入棧
if ((--verList[e.data].in) == 0){
stack[top++] = e.data;
}
}
}
// 如果能輸出所有點,說明無環
if (count == len-1){
System.out.println("無環");
} else {
System.out.println("有環");
}
}
private static void CrateAdjList(aovNode[] verList, int[][] edgeList) {
int len = edgeList.length;
for (int i = 1; i < len; i++) {
int s = edgeList[i][0];
int t = edgeList[i][1];
adjNode adj = new adjNode();
adj.data = t;
adj.next = verList[s].firstedge;
verList[s].firstedge = adj;
}
}
private static void InitTopoplogy(aovNode[] verList, int[][] edgeList) {
// 初始化頂點的值和入度
verList[1] = new aovNode(0,1);
verList[2] = new aovNode(1,2);
verList[3] = new aovNode(2,3);
verList[4] = new aovNode(2,4);
verList[5] = new aovNode(2,5);
verList[6] = new aovNode(0,6);
// 初始化邊
edgeList[1][0] = 1;
edgeList[1][1] = 2;
edgeList[2][0] = 1;
edgeList[2][1] = 3;
edgeList[3][0] = 2;
edgeList[3][1] = 5;
edgeList[4][0] = 3;
edgeList[4][1] = 4;
edgeList[5][0] = 5;
edgeList[5][1] = 3;
edgeList[6][0] = 5;
edgeList[6][1] = 4;
edgeList[7][0] = 6;
edgeList[7][1] = 5;
}
}
關鍵路徑
AOE網:在AOV網的弧上加上權值表示活動的持續時間,這樣的網叫做AOE網(Activity On Edge Network)。
路徑長度:路徑上各個活動所持續的時間之和。
關鍵路徑:從源點(入度爲0)到匯點(出度爲0)具有最大長度的路徑。
關鍵活動:在關鍵路徑上的活動。
算法原理:
如果所有活動的最早開始時間和最晚開始時間相同,就意味着此活動爲關鍵活動,活動間的路徑爲關鍵路徑,若不等則不是。
定義如下參數:
- 事件的最早發生時間evt:即頂點的最早發生時間
- 事件的最晚發生時間ltv:即頂點的最晚發生時間,也就是每個頂點對應的時間最晚需要開始的時間,超出此時間將會延誤整個工期
- 活動的最早開工時間ete:即弧的最早發生時間
- 活動的最晚開工時間lte:即弧的最晚發生時間,也就是不推遲同期的最晚開工時間
由1和2可以求得3和4,然後再根據ete[k]是否與lte[k]相等來判斷是否是關鍵活動。
關鍵路徑算法
全局變量:
- 求etv就是從頭到尾找拓撲序列的過程
- stack2用來存儲拓撲序列
int *etv, *ltv; // 事件最早發生時間和最遲發生時間數組
int *stack2; // 用於存儲拓撲排序的棧,喫stack吐出來的
int top2; // 用於stack2的指針
改進後的求拓撲序列算法:
關鍵點在於最後的求各頂點事件最早發生時間值,鄰接點作爲弧的尾點,開始時間取的是各個弧頭和弧長的最大值,因爲只有前置事件都完成了,才能進入下一事件。
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top = 0;
int count = 0;
int *stack;
stack = (int *) malloc(GL->numVertexes * sizeof(int));
for (i=0; i<GL->numVertexes; i++)
if (0 == GL->adjList[i].in)
stack[++top] = i;
top2 = 0;
etv = (int *) malloc(GL->numVertexes * sizeof(int));
for (i=0; i<GL->numVertexes; i++)
etv[i] = 0; // 初始化爲0
stack2 = (int *) malloc(GL->numVertexes * sizeof(int));
while(top != 0)
{
gettop = stack[top--];
count++;
stack2[++top2] = gettop; // 彈出的頂點壓入拓撲序列棧
for (e=GL->adjList[gettop].firstedeg; e; e=e->next)
{
k = e->adjvex;
if (!(--GL->adjList[k].in))
stack[++top] = k;
// 求各頂點事件最早發生時間值
// 這裏的意思就是k要等前面忙完,取前面的最充裕時間
if ((etv[gettop] + e->weight) > etv[k])
etv[k] = etv[gettop] + e->weight;
}
}
if (count < GL->numVertexes)
return ERROR;
else
return OK;
}
關鍵路徑代碼:
注意:
- 鄰接表裏的weight是對應的弧的值,也就是兩點間的時間。
- 一是求最晚發生時間,這裏是倒序,從最後的點開始,取的是最小最晚時間,也就是讓上一個活動開始的儘可能早,因爲上個活動結束後剩下的時間裏,要保證接下來的時間夠完成所有任務,而不是隻完成剩下的任務中耗時最短的那個,所以必須保證剩下的任務中耗時最長的可以完成,所以這裏用的是小於號,意思就是最晚中的最早。
- 求lte的時候,當ete=lte時,說明這個任務線是時間最緊的,最早最晚時間是被這個任務約束的,而不等的時候,一定是 lte>ete 的,這種情況的任務執行的時候,時間是有剩餘的。
void CriticalPath(GraphAdjList GL)
{
EgdeNode *e;
int i, gettop, k, j;
int ete, lte; // 活動的最早和最晚發生時間
TopologicalSort(GL); //計算數組etv和stack2的值
ltv = (int *) malloc(GL->numVertexes * sizeof(int));; // 事件最晚發生時間
for (i=0; i<GL->numVertexes; i++)
// 全部初始化成最大時間,同時也方便後面的比較
ltv[i] = etv[GL->numVertexes - 1];
while (top2 != 0) // 計算ltv
{
gettop = stack2[top2--]; // 拓撲序列後進先出
for (e=GL->adjList[gettop].firstedge; e; e=e->next)
{
// 求各頂點時間的最遲發生時間ltv的值
k = e->adjvex;
// 求各頂點事件最晚發生時間ltv
// 這裏是要比較最晚開始的,因爲可能好多任務交叉的,所以一定要比較,而不是用減法往前推就行
// 這裏要用最小,因爲如果用最大,有些任務就完不成了
if (ltv[k]-e->weight < ltv[gettop])
ltv[gettop] = ltv[k] - e->weight;
}
}
// 求ete,lte和關鍵活動
// 有了最早和最晚,遍歷所有點和其鄰接點比較即可
for (j=0; j<GL->numVertexes; j++)
{
for (e=GL->adjList[j].firstedge; e; e=e->next)
{
// 讓點與鄰接點時間比較
k = e->adjvex;
ete = etv[j]; // 活動最早發生時間
lte = ltv[k] - e->weight; // 活動最遲發生時間
if (ete == lte) // 兩者相等即在關鍵路徑上
printf("<v%d, v%d>length: %d ",
GL->adjList[j].data, GL->adjList[k].data, e->weight);
}
}
}
JAVA實現關鍵路徑
結點類
public class adjWNode {
int data;
int weight;
adjWNode next;
public adjWNode(int data, int weight) {
this.data = data;
this.weight = weight;
}
}
class aovNode{
public int in;
public int data;
public adjWNode firstedge;
public aovNode(int in, int data) {
this.in = in;
this.data = data;
}
}
主代碼:
package Graph.KeyPath;
public class KeyPath {
public static void main(String[] args) {
aovNode[] verList = new aovNode[10];
int[][] edgeList = new int[13][3];
InitKeyPath(verList, edgeList);
CreateAdjList(verList, edgeList);
// 棧存放排序,這裏要用到從後往前的方法,所以用棧來存儲
int[] stack = new int[10];
// 記錄各個活動最早開始時間的數組
int[] evt = new int[10];
// 用新的拓撲排序來初始化上面兩個數組
// top爲stack數組的top
int top = TopologySave(verList, stack, evt);
keyPath(stack, top, evt, verList);
}
private static void keyPath(int[] stack, int top, int[] evt, aovNode[] verList) {
int len = verList.length;
// 先求一波最晚時間
int[] ltv = new int[len];
// 初始化爲最大時間
for (int i = 0; i < len; i++) {
ltv[i] = evt[len-1];
}
// 從後往前
while (top != 0){
int gettop = stack[--top];
// 對鄰接點下手
for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
// 更新時間,越晚越好
// 鄰接點 - 路徑時間 最小,爲了保證所有任務都能完成
if (ltv[e.data] - e.weight < ltv[gettop]){
ltv[gettop] = ltv[e.data] - e.weight;
}
}
}
// 最早和最晚都有了,遍歷比較就行了
// 這樣可得到關鍵點
for (int i = 0; i < len; i++) {
if (evt[i] == ltv[i]){
System.out.println(i);
}
}
System.out.println("---------");
// 這樣可得到路徑
for (int i = 0; i < len; i++) {
for (adjWNode e=verList[i].firstedge; e!=null; e=e.next){
if (evt[i] == ltv[e.data]-e.weight){
System.out.println(i + "->" + e.data);
}
}
}
}
private static int TopologySave(aovNode[] verList, int[] stack2, int[] evt) {
int len = verList.length;
int[] stack = new int[len];
int count = 0;
int top = 0;
int top2 = 0;
for (int i = 0; i < len; i++) {
if (verList[i].in == 0){
stack[top++] = i;
}
}
// 初始化evt
for (int i = 0; i < len; i++) {
evt[i] = 0;
}
while(top!=0){
int gettop = stack[--top];
count++;
// 存起來
stack2[top2++] = gettop;
for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
if((--verList[e.data].in)==0){
stack[top++] = e.data;
}
// 這裏還得記下最早時間
// e.weight就是從gettop到e.data需要的時間
if (evt[gettop] + e.weight > evt[e.data]){
evt[e.data] = evt[gettop] + e.weight;
}
}
}
if (count == len){
System.out.println("無環");
} else {
System.out.println("有環");
}
return top2;
}
private static void CreateAdjList(aovNode[] verList, int[][] edgeList) {
for (int i=0; i< edgeList.length; i++){
int s = edgeList[i][0];
int e = edgeList[i][1];
adjWNode node = new adjWNode(e, edgeList[i][2]);
node.next = verList[s].firstedge;
verList[s].firstedge = node;
}
}
private static void InitKeyPath(aovNode[] verList, int[][] edgeList) {
verList[0] = new aovNode(0,0);
verList[1] = new aovNode(1,1);
verList[2] = new aovNode(1,2);
verList[3] = new aovNode(2,3);
verList[4] = new aovNode(2,4);
verList[5] = new aovNode(1,5);
verList[6] = new aovNode(1,6);
verList[7] = new aovNode(2,7);
verList[8] = new aovNode(1,8);
verList[9] = new aovNode(2,9);
edgeList[0][0] = 0;
edgeList[0][1] = 1;
edgeList[0][2] = 3;
edgeList[1][0] = 0;
edgeList[1][1] = 2;
edgeList[1][2] = 4;
edgeList[2][0] = 1;
edgeList[2][1] = 3;
edgeList[2][2] = 5;
edgeList[3][0] = 1;
edgeList[3][1] = 4;
edgeList[3][2] = 6;
edgeList[4][0] = 2;
edgeList[4][1] = 3;
edgeList[4][2] = 8;
edgeList[5][0] = 2;
edgeList[5][1] = 5;
edgeList[5][2] = 7;
edgeList[6][0] = 3;
edgeList[6][1] = 4;
edgeList[6][2] = 3;
edgeList[7][0] = 4;
edgeList[7][1] = 6;
edgeList[7][2] = 9;
edgeList[8][0] = 4;
edgeList[8][1] = 7;
edgeList[8][2] = 4;
edgeList[9][0] = 5;
edgeList[9][1] = 7;
edgeList[9][2] = 6;
edgeList[10][0] = 6;
edgeList[10][1] = 9;
edgeList[10][2] = 2;
edgeList[11][0] = 7;
edgeList[11][1] = 8;
edgeList[11][2] = 5;
edgeList[12][0] = 8;
edgeList[12][1] = 9;
edgeList[12][2] = 3;
}
}