文章目录
- 5.图的应用
- 5.1 最小生成树
- 5.1.1 Prim算法
- 5.1.2 Kruskal算法
- 5.1.3 最小生成树代码
- A.邻接矩阵
- B.邻接表
- 5.2 最短路径
- 5.2.1 BFS
- 5.2.2 Dijkstra
- 5.2.3 Floyd
- 5.2.4 三种算法的比较
- 5.3 有向无环图描述表达式
- 5.4 拓扑排序
- 5.5 关键路径
5.图的应用
5.1 最小生成树
-
定义
对一个带权连通无向图 G = ( V , E ) G=(V,E) G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。
设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(MST)。
-
性质
1.最小生成树可能有多个,但边的权值之和总是唯一且最小的;
2.最小生成树的边数=顶点数-1。砍掉一条则不连通,增加一条会出现回路;
3.如果一个连通图本身就是一棵树,则其最小生成树就是它本身;
4.只有连通图才有最小生成树,非连通图只有生成森林。
-
注
最小生成树是所有边权值之和最小,但不能保证任意两个顶点之间的路径最短。如:
-
伪代码
GENERIC_MST(G){ T=NULL; while T未形成一棵生成树; do 找到一条最小代价边(u,v)并且加入T后不会产生回路; T=T∪(u,v); }
通用算法每次加入一条边以逐渐形成一棵生成树。
5.1.1 Prim算法
-
定义
从某一个顶点开始构建生成树;
每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
- 即选最小权值的结点
-
时间复杂度
O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),适用于稠密图(|E|大的)。
-
算法的实现思想
-
思路:
1.从 V 0 V_0 V0开始,总共需要n-1轮处理。
2.第一轮处理:循环遍历所有个结点,找到
lowCast
最低的,且还没加入树的顶点。3.再次循环遍历,更新还没加入的各个顶点的
lowCast
值。每一轮时间复杂度 O ( 2 n ) O(2n) O(2n),一共有n轮。
-
代码步骤:
1.创建
isJoin
数组,初始为false,判断结点是否加入树。2.创建
lowCost
数组,用于存储到该结点的最短距离。3.从 v 0 v_0 v0开始,将与其连接的权值加入到
lowCost
数组中。4.遍历
lowCast
数组,找到最小值,将其加入树中,并继续遍历与其相连的边。
-
-
伪代码
void Prim(G,T){ T=∅; //初始化空树 U={w}; //添加任意一个顶点w while((V-U)!=∅){ //若树中不含全部顶点 设(u,v)是使u∈U与v∈(V-U),且权值最小的边; T=T⋃{(u,v)}; //边归入树 U=U⋃{v}; //顶点归入树 } }
5.1.2 Kruskal算法
-
定义
每次选则一条权值最小的边,使这条边的两头连通(原本已经连通的不选),直到所有结点都连通。
- 即每次选最小的边
-
时间复杂度
O ( ∣ E ∣ l o g 2 ∣ E ∣ ) O(|E|log_2|E|) O(∣E∣log2∣E∣),适用于边稀疏图。
-
算法的实现思想
-
思路:
1.初始:将各条边按权值排序。
2.共执行e轮,每轮判断两个顶点是否属于同一集合,需要 O ( l o g 2 e ) O(log_2e) O(log2e)
-
-
伪代码
void Kruskal(V,T){ T=V; //初始化树T,仅含顶点 numS=n; //连通分量数 while(numS>1){ //若连通分量数大于1 从E中取出权值最小的边(v,u); if(v和u属于T中不同的连通分量){ //就是没有形成环 T=T⋃{(v,u)}; //将此边加入生成树中 numS--; //连通分量数减1 } } }
5.1.3 最小生成树代码
A.邻接矩阵
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
#define V 5 // 图的顶点数
// 找到距离集合最近的顶点
int min_key(int key[], bool mst_set[]) {
int min = INT_MAX, min_index;
for (int v = 0; v < V; v++) {
if (mst_set[v] == false && key[v] < min) {
min = key[v];
min_index = v;
}
}
return min_index;
}
// 打印最小生成树
void print_mst(int parent[], int graph[V][V]) {
printf("Edge Weight\n");
for (int i = 1; i < V; i++)
printf("%d - %d %d \n", parent[i], i, graph[i][parent[i]]);
}
// Prim算法
void prim_mst(int graph[V][V]) {
int parent[V]; // 存放最小生成树的父节点
int lowCost[V]; // 用于存放顶点到最小生成树的最小权重
bool isJoin[V]; // 记录顶点是否已经加入最小生成树
for (int i = 0; i < V; i++) {
lowCost[i] = INT_MAX;
isJoin[i] = false;
}
lowCost[0] = 0; // 初始点为0
parent[0] = -1; // 根节点没有父节点
for (int count = 0; count < V - 1; count++) {
int u = min_key(lowCost, isJoin);
isJoin[u] = true;
for (int v = 0; v < V; v++) {
if (graph[u][v] && !isJoin[v] && graph[u][v] < lowCost[v]) {
parent[v] = u;
lowCost[v] = graph[u][v];
}
}
}
print_mst(parent, graph);
}
// Kruskal算法
// 结构体用于表示边
struct Edge {
int src, dest, weight;
};
// 比较函数,用于排序
int compare(const void* a, const void* b) {
return ((struct Edge*)a)->weight - ((struct Edge*)b)->weight;
}
// 查找函数,用于查找集合的根节点
int find(int parent[], int i) {
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
// 合并函数,用于合并两个集合
void Union(int parent[], int x, int y) {
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
// Kruskal算法
void kruskal_mst(int graph[V][V]) {
struct Edge result[V]; // 用于存放最小生成树的边
int e = 0; // 表示result数组中的边数
int i = 0; // 表示当前考虑的边
// 边集合
struct Edge edges[V*V];
for (int u = 0; u < V; u++) {
for (int v = u + 1; v < V; v++) {
if (graph[u][v] != 0) {
edges[e].src = u;
edges[e].dest = v;
edges[e].weight = graph[u][v];
e++;
}
}
}
// 根据权重对边进行排序
qsort(edges, e, sizeof(edges[0]), compare);
int parent[V]; // 用于记录每个顶点的父节点
for (int v = 0; v < V; v++)
parent[v] = -1;
// 最小生成树的边数小于V-1时继续
while (i < V - 1 && e > 0) {
struct Edge next_edge = edges[--e];
// 检查是否会产生环
int x = find(parent, next_edge.src);
int y = find(parent, next_edge.dest);
if (x != y) {
result[i++] = next_edge;
Union(parent, x, y);
}
}
printf("Edge Weight\n");
for (int i = 0; i < V - 1; i++)
printf("%d - %d %d \n", result[i].src, result[i].dest, result[i].weight);
}
// 测试主函数
int main() {
int graph[V][V] = {
{0, 2, 0, 6, 0},
{2, 0, 3, 8, 5},
{0, 3, 0, 0, 7},
{6, 8, 0, 0, 9},
{0, 5, 7, 9, 0}
};
printf("Prim's Minimum Spanning Tree:\n");
prim_mst(graph);
printf("\nKruskal's Minimum Spanning Tree:\n");
kruskal_mst(graph);
return 0;
}
B.邻接表
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
#define MaxVertexNum 100
#define INF 9999
typedef struct ArcNode {
int adjvex;
int weight;
struct ArcNode *next;
} ArcNode;
typedef struct VNode {
int data;
ArcNode *first;
} VNode, AdjList[MaxVertexNum];
typedef struct {
AdjList vertices;
int vexnum, arcnum;
} ALGraph;
void InitALGraph(ALGraph *G, int vexnum, int arcnum) {
G->vexnum = vexnum;
G->arcnum = arcnum;
for (int i = 0; i < vexnum; i++) {
G->vertices[i].data = i;
G->vertices[i].first = NULL;
}
}
void AddEdgeUndirectedALGraph(ALGraph *G, int v1, int v2, int weight) {
ArcNode *arcNode1 = (ArcNode *)malloc(sizeof(ArcNode));
arcNode1->adjvex = v2;
arcNode1->weight = weight;
arcNode1->next = G->vertices[v1].first;
G->vertices[v1].first = arcNode1;
ArcNode *arcNode2 = (ArcNode *)malloc(sizeof(ArcNode));
arcNode2->adjvex = v1;
arcNode2->weight = weight;
arcNode2->next = G->vertices[v2].first;
G->vertices[v2].first = arcNode2;
}
void PrintALGraph(ALGraph G) {
for (int i = 0; i < G.vexnum; i++) {
printf("%d -> ", G.vertices[i].data);
ArcNode *p = G.vertices[i].first;
while (p != NULL) {
printf("(%d, %d) ", p->adjvex, p->weight);
p = p->next;
}
printf("\n");
}
}
// Prim算法
void Prim(ALGraph G) {
int lowCost[G.vexnum], parent[G.vexnum];
bool inMST[G.vexnum];
for (int i = 0; i < G.vexnum; i++) {
lowCost[i] = INF;
parent[i] = -1;
inMST[i] = false;
}
lowCost[0] = 0;
for (int i = 0; i < G.vexnum - 1; i++) {
int minIndex, minCost = INF;
for (int j = 0; j < G.vexnum; j++) {
if (!inMST[j] && lowCost[j] < minCost) {
minCost = lowCost[j];
minIndex = j;
}
}
inMST[minIndex] = true;
ArcNode *p = G.vertices[minIndex].first;
while (p != NULL) {
if (!inMST[p->adjvex] && p->weight < lowCost[p->adjvex]) {
lowCost[p->adjvex] = p->weight;
parent[p->adjvex] = minIndex;
}
p = p->next;
}
}
printf("Edge Weight\n");
for (int i = 1; i < G.vexnum; i++) {
printf("%d - %d %d\n", parent[i], i, lowCost[i]);
}
}
// Kruskal算法
typedef struct {
int src, dest, weight;
} Edge;
int find(int parent[], int i) {
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
void Union(int parent[], int x, int y) {
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
int compare(const void *a, const void *b) {
return ((Edge *)a)->weight - ((Edge *)b)->weight;
}
void Kruskal(ALGraph G) {
Edge result[G.arcnum];
Edge edges[G.arcnum];
int parent[G.vexnum];
int e = 0;
for (int i = 0; i < G.vexnum; i++) {
ArcNode *p = G.vertices[i].first;
while (p != NULL) {
if (i < p->adjvex) {
edges[e].src = i;
edges[e].dest = p->adjvex;
edges[e].weight = p->weight;
e++;
}
p = p->next;
}
}
qsort(edges, G.arcnum, sizeof(Edge), compare);
for (int i = 0; i < G.vexnum; i++)
parent[i] = -1;
int i = 0, j = 0;
while (i < G.vexnum - 1 && j < G.arcnum) {
Edge next_edge = edges[j++];
int x = find(parent, next_edge.src);
int y = find(parent, next_edge.dest);
if (x != y) {
result[i++] = next_edge;
Union(parent, x, y);
}
}
printf("Edge Weight\n");
for (int i = 0; i < G.vexnum - 1; i++) {
printf("%d - %d %d\n", result[i].src, result[i].dest, result[i].weight);
}
}
int main() {
ALGraph G;
InitALGraph(&G, 5, 7);
AddEdgeUndirectedALGraph(&G, 0, 1, 2);
AddEdgeUndirectedALGraph(&G, 0, 3, 6);
AddEdgeUndirectedALGraph(&G, 1, 2, 3);
AddEdgeUndirectedALGraph(&G, 1, 3, 8);
AddEdgeUndirectedALGraph(&G, 1, 4, 5);
AddEdgeUndirectedALGraph(&G, 2, 4, 7);
AddEdgeUndirectedALGraph(&G, 3, 4, 9);
PrintALGraph(G);
printf("Prim's Minimum Spanning Tree:\n");
Prim(G);
printf("\nKruskal's Minimum Spanning Tree:\n");
Kruskal(G);
return 0;
}
5.2 最短路径
-
单源最短路径问题
从一个顶点出发,到各个顶点的最短路径。
-
BFS算法(无权图)
-
Dijkstra算法(带权图、无权图)
-
-
各顶点间的最短路径
每对顶点间的最短路径。
- Floyd算法(带权图、无权图)
5.2.1 BFS
-
定义
求无权图的单源最短路径
在广度优先遍历基础上改造。
-
局限性:只适用于无权图,或所有边的权值都相同的图。
如下图,G港直接到R城的距离不是最短的,先到P城再到R城才是最短的。
-
-
代码
const int MAX_INT=0X7fffffff; //表示无穷大 //求顶点u到其他顶点的最短路径 void BFS_MIN_Distance(Graph G,int u){ //d[i]表示从u到i结点的最短路径 for(i=0;i<G.vexnum;i++){ d[i]=MAX_INT; //初始化路径长度 path[i]=-1; //最短路径从哪个顶点过来的 } d[u]=0; visited[u]=TRUE; EnQueue(Q,u); //BFS算法主过程 while(!isEmpty(Q)){ DeQueue(Q,u); //队头元素u出队 for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){ //若w为u尚未访问的邻接顶点 if(!visited[w]){ d[w]=d[u]+1; //w的路径长度为起始顶点到u的距离加1 path[w]=u; //w的最短路径是从u过来的 visited[w]=TRUE; //设已访问标记 EnQueue(Q,w); //顶点w入队 } } } }
5.2.2 Dijkstra
-
定义
-
带权路径长度:带权图中,一条路径上所有边的权值之和。
-
Dijkstra算法可解决带权图和无权图的单源最短路径问题。
-
局限性:不适用于有负权值的带权图:
-
-
算法思想
1.从v0开始,初始化以下三个数组:
2.遍历所有顶点,找到还没确定最短路径的顶点(
final
值为false
),且dist
最小的顶点Vi
,令final[i]=true
(i的最短路径确定了)。3.检查所有邻接自
Vi
的顶点,若final
为false
,则更新dist
和path
信息。4.重复第2、3步,直到
final
都为true
。 -
时间复杂度
O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2):所有顶点遍历一次(把final从false变成true)为O(n)的时间,每次遍历path数组找到最短路径又是 O ( n ) O(n) O(n)的时间,所以为 O ( n 2 ) O(n^2) O(n2)。
5.2.3 Floyd
-
定义
求出一对顶点间的最短路径。
使用动态规划思想,将问题的求解分为多个阶段:
-
也可以解决带负权值的图,但是不能解决带负权回路的图(有负权值的边组成回路),这种图有可能没有最短路径。如:
-
-
步骤
1.初始化两个二维数组
2.以 V k V_k Vk为中转点(以下公式中,k是中转点,要判断i直接到j的距离和结果k中转的i到j的距离哪个更加短):
3.从 A ( − 1 ) A^{(-1)} A(−1)和 p a t h ( − 1 ) path^{(-1)} path(−1)开始,经过第2步的n轮递推,得到 A ( n − 1 ) A^{(n-1)} A(n−1)和 p a t h ( n − 1 ) path^{(n-1)} path(n−1)。
-
核心代码
//。。。准备工作,根据图的信息初始化矩阵A和path for(int k=0;k<n;k++){ //考虑以Vk作为中转点 for(int i=0;i<n;i++){ //遍历整个矩阵,i为行号,j为列号 for(int j=0;j<n;j++){ if(A[i][j]>A[i][k]+A[k][j]){ //如果以Vk为中转点的路径更短 A[i][j]=A[i][k]+A[k][j]; //更新最短路径长度 path[i][j]=k; //更新中转点 } } } }
-
复杂度
- 时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)
- 空间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
5.2.4 三种算法的比较
5.3 有向无环图描述表达式
-
定义
-
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称DAG图。
-
有向无环图描述表达式:要用有向无环图的形式描述表达式。如题:
-
-
解题方法
1.把各操作数不重复的排成一排。
2.标出各个运算符生效的顺序(先后顺序有点出入没关系)。
3.按顺序在图中加入运算符,注意分层。
4.从底向上逐层检查同层的运算符是否可以合体。
-
有向无环图存表达式是不唯一的。
5.4 拓扑排序
-
定义
-
AOV网(用顶点表示活动的网)
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动 V i V_i Vi必须先于活动 V j V_j Vj进行。
-
拓扑排序
在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
1)每个顶点出现且只出现一次;
2)若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。
或定义为:
拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。
如:
-
-
实现步骤
1.从AOV网中选择一个**没有前驱(入度为0)**的顶点并输出;
2.从网中删除该顶点和所有以它为起点的有向边;
3.重复第1和2步,直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
-
代码
bool TopologicalSort(Graph G){ InitStack(S); //初始化栈,存储入度为0的顶点 for(int i=0;i<G.vexnum;i++){ if(indegree[i]==0){ Push(S,i); //将所有入度为0的顶点进栈 } } int count=0; //计数,记录当前已经输出的顶点数 //栈不为空,说明存在入度为0的顶点 while(!IsEmpty(S)){ Pop(S,i); //顶点元素出栈 print[count++]=i; //输出顶点i for(p=G.vertices[i].firstarc;p;p=p->nextarc){ //将所有i指向的顶点的入度减1,并将入度减1后为0的顶点压入栈S v=p->adjvex; if(!(--indegree[v])){ Push(S,v); //入度为0,则入栈 } } } if(count<G.vexnum) return false; //排序失败,有向图中有回路 else return true; }
-
时间复杂度
- 分析:每个顶点都要入栈出栈,所以每个顶点都要遍历一次,因为要删除每条边,所以每条边也要遍历一次。
- 邻接表时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
- 邻接矩阵时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
-
-
逆拓扑排序
和拓扑排序的区别就是把入度为0改为出度为0的。
-
逆拓扑排序的实现(DFS算法)
//对图G进行深度优先遍历 void DFSTraverse(Graph G){ for(v=0;v<G.vexnum;v++){ visited[v]=false; //初始化已访问标记数据 } for(v=0;v<G.vexnum;v++){ if(!visited[v]){ DFS(G,v); } } } //从顶点v出发,深度优先遍历图G void DFS(Graph G,int v){ visited[v]=true; for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ if(!visited[w]){ DFS(G,w); } } print(v); }
-
-
性质
- 拓扑排序、逆拓扑排序序列可能不唯一。
- 若图中有环,则不存在拓扑排序/逆拓扑排序序列。
5.5 关键路径
-
定义
-
AOE网
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销,称为用边表示活动的网络,简称AOE网。(Activity On Edge Network)
-
AOE网性质
1)只有在某个顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
2)只有在进入某顶点的各有向边所代表的活动已结束时,该顶点所代表的事情才能发送。
另外,有些活动可以并行。
-
开始顶点(源点):AOE中只有一个入度为0的,表示整个工程的开始;
结束顶点(汇点):AOE中只有一个出度为0的顶点,表示整个工程的结束。
-
关键路径
关键路径:从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径。
关键活动:关键路径上的活动。
关键路径长度:完成整个工程最短时间。
-
各时间点
事件 v k v_k vk最早发生时间 v e ( k ) ve(k) ve(k):决定了所有从 v k v_k vk开始的活动能够开工的最早时间。
活动 a i a_i ai最早发生时间 e ( i ) e(i) e(i):指该活动弧的起点所表示的事件的最早发生时间。
事件 v k v_k vk最迟发生时间 v l ( k ) vl(k) vl(k):指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
活动 a i a_i ai最迟发生时间 l ( i ) l(i) l(i):指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
活动 a i a_i ai最早发生时间 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)−e(i):如果值为0,说明该活动不能拖延,是关键活动。
-
-
求关键路径
1.求所有事件的最早发生时间
按拓扑排序序列,依次求各个顶点的 v e ( k ) ve(k) ve(k):
ve(源点)=0; ve(k)=Max{ve(j)+Weight(vj,vk)},vj是vk的任意前驱
2.求所有事件的最迟发生时间
3.求所有活动的最早发生时间
4.求所有活动的最迟发生时间
5.所有活动的时间余量
6.时间余量为0的就是关键活动
-
关键活动、关键路径的特性
1.关键活动耗时增加,工期增加;缩短关键活动事件,可缩短工期,但缩到一定程度会变成非关键活动。
2.可能有多条关键路径,则要加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。