图
文章目录
- 图
- 7.1 图的定义和术语
- 7.2 图的存储结构
- 7.2.1 数组表示法 - 邻接矩阵(Adjacency Matrix)
- 7.2.2 邻接表 -(链式)表示法(Adjacency List)
- 7.2.3 十字链表(Orthogonal List)
- 7.2.4 邻接多重表(Adjacent MultiList)
- 7.3 图的遍历
- 7.3.1 深度优先搜索(Depth_First Search-DFS )
- 7.3.2 广度优先搜索(Breadth_Frist Search-BFS)
- 7.4 图的连通性问题
- 7.4.1 无向图的连通分量和生成树
- 7.4.1.1 Tarjan算法 - 解决无向图的割点和桥问题
- 7.4.2 有向图的强连通分量 (Strongly Connected Components - SCCs)
- 7.4.2.1 Tarjan算法 - 解决有向图的强连通分量问题
- 7.4.2.2 Kosaraju算法
- 7.4.3 最小生成树 (Minimum Spanning Tree - MST)
- 7.4.3.1 Prim 算法
- 7.4.3.2 Kruskal 算法
- 7.4.3.3 Prim 算法和Kruskal 算法比较
- 7.5 有向无环图及其应用
- 7.5.1 拓扑排序 - AOV网
- 7.5.2 关键路径 - AOE网
- 7.6 最短路径
- 7.6.1 从某个源点(单源)到其余各顶点的最短路径 -Dijkstra算法
- 7.6.2 每一对顶点之间(所有顶点)的最短路径 - Floyd算法
7.1 图的定义和术语
在图中的数据元素通常称做顶点 (Vertex), V是顶点的有穷非空集合;
VR 是两个顶点之间的关系 的集合
若<v,w> ∈ VR, 则<v,w>表示从v到w的一条弧 (Arc), 且称 v 为弧尾 (Tail) 或初始点 (Initial node) , w 为弧头 (Head) 或终端点 (Terminal node) , 此时的图称为有向图 (Digraph) 。有向图的谓词 P(v,w) 则表示从 的一条单向通路。
若<v,w> ∈ VR, 必有 <w,v> ∈ VR, VR 是对称的,则以无序对 (v,w) 代替这两个有序对,表示v和w之间的一条边 (Edge), 此时的图称为无向图 (Undigraph) 。
完全图(Completed graph):任意两个点都有一条边相连
若n表示图中顶点数目,e表示边或弧的数目:
<vi,vj> ∈ VR,则 vi ≠ vj,对于无向图,e 的取值范围是 0 到 n(n-1)·1/2,有n(n-1)·1/2条边的无向图称为无向完全图
对于有向图, e的取值范围是 n(n-1) 。具有 n(n-1) 条弧的有向图称为有向完全图
有很少条边或弧(如 e<nlogn)的图称为稀疏图 (Sparse graph), 反之称为稠密图 (Dense graph)
有时图的边或弧具有与它相关的数,这种与图的边或弧相关的数叫做权 (Weight)。这种带权的图通常称为网 (Network)
假设有两个图 G= (V, {E}) G’= (V’, { E’}) , 如果 V’⊆ V,E’⊆E, 则称 G’ 为G的子图 (Subgraph) 。
邻接:有边/弧相连的两个顶点之间的关系。
关联(依附):边/弧与顶点之间的关系。
对于无向图 G= (V, {E}),
如果边 (v, v’) ∈ E, 则称顶点 v和v’ 互为邻接点 (Adjacent), v和v’ 相邻接。
(v, v’) 依附 (Incident) 于顶点 v和v’, 或者说 (v,v’) 和顶点 v和v’ 相关联。
顶点v 的度 (Degree) 是和 v相关联的边的数目,记为 TD(V) 。
对于有向图 G=(V,{A}),
如果弧 <v,v’> ∈ A, 则称顶点v邻接到顶点v’ ,顶点v’ 邻接自顶点 v。弧 <v,v’>和顶点v,v’相关联。
以顶点v为头的弧的数目称为v的入度 (InDegree), 记为 ID(v);
以顶点v为尾的弧的数目称为v的出度 (Outdegree) , 记为 OD(v);
顶点v的度为 TD(v) =ID(v)+OD(v)
一般地,如果顶点 vi的度记为 TD(vi); 那么一个有n个顶点, e条边或弧的图,满足如下关系 e = 1 2 ∑ i = 1 n T D ( v i ) e= \frac{1}{2}\sum_{i=1}^n TD(v_i) e=21i=1∑nTD(vi)
无向图G= (V, {E})中从顶点v到顶点 v’ 的路径 (Path) 是一个顶点序列 (v=vi,0,vi,1 , … , vi,m=v’), 其中 (vi,j-1 ,vi,j) ∈ E, 1≤j≤m。
有向图G=(V,{A})的路径也是有向的, 顶点序列应满足 <vi,j-1 ,vi,j>∈E, 1≤j≤m。
路径的长度是路径上的边或弧的数目。
第一个顶点和最后一个顶点相同的路径称为回路或环 (Cycle) 。
序列中顶点不重复出现的路 径称为简单路径。
除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称 为简单回路或简单环。
在无向图 中,如果从顶点 v到顶点 v’ 有路径,则称 v和v’ 是连通的。
如果对于无向图 中任意两个顶点 vi,vj ∈V, vi, vj都是连通的,则称G是连通图 (Connected Graph) 。
极大连通子图:该子图是 G 连通子图,将G 的任何不在该子图中的顶点加入,子图不再连通。
连通分量 (Connected Component) , 指的是无向图中的极大连通子图。
在有向图 中,如果对于每一对 vi,vj ∈V,vi≠ vj,从 vi到 vj和从vj到 vi都存在路 径,则称G是强连通图。
有向图中的极大强连通子图称做有向图的强连通分量。
极小连通子图:该子图是G 的连通子图,在该子图中删除任何一边,子图不再连通。
一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一 棵树的 n-1 条边。
一棵有 个顶点的生成树有且仅有 n-1 条边。如果一个图有 个顶点和小于 n-1 边,则是非连通图。如果它多于 n-1 条边,则一定有环。但是,有 n-1 条边的图不一定 是生成树。
如果一个有向图恰有一个顶点的入度为0, 其余顶点的入度均为 1, 则是一棵有向树。
一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵 不相交的有向树的弧
7.2 图的存储结构
7.2.1 数组表示法 - 邻接矩阵(Adjacency Matrix)
用两个数组分别存储数据元素(顶点)的信息【顶点表】和数据元素之间的关系(边或弧)的信息【邻接矩阵】。
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;/*无向图 1、初始化邻接矩阵时,w=0 2、构造邻接矩阵时,w=1 / \无向网 + 即 有向图\ /有向网 邻接矩阵非对称矩阵 仅为G->arcs[i][j]赋值,无需为G->arcs[j][i]赋值
*/
Status CreateGraph(MGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网case UDG: return CreateUDG(G); //构造无向图*/case UDN: return CreateUDN(G); //构造无向网default:return ERROR;}
}Status CreateUDN(MGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = INFINITY;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2,weight : \n");scanf(" %c %c %d", &v1, &v2, &w); // 输入一条边依附的顶点及权值// 确定 v1 和 v2 在 G 中位置i = LocateVex(G, v1);j = LocateVex(G, v2);G->arcs[i][j] = w; // 弧<v1, v2> 的权值 G->arcs[j][i] = G->arcs[i][j]; // <v1, v2> 的对称弧 <v2, v1>}// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {if (G->arcs[i][j] == INFINITY){printf("∞ ");continue;}printf("%d ", G->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}int main()
{MGraph G;CreateGraph(&G);return 0;
}
时间复杂度:
- 对每个顶点(n)进行遍历初始化,导致 n2 的复杂度。
- 对每条边(e)进行遍历,并在每次遍历时对顶点执行一些操作(LocateVex函数),导致 e⋅n 的复杂度。
所以总的时间复杂度是O( n2 + e⋅n)。
优点:
- 方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
- 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”)
缺点:
-
不便于增加和删除顶点
-
浪费空间–存稀疏图(点很多而边很少)有大量无效元素
对稠密图(特别是完全图)还是很合算的
-
浪费时间–统计稀疏图中一共有多少条边 O( n2)
7.2.2 邻接表 -(链式)表示法(Adjacency List)
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include<stdlib.h>#define MVNUM 20
#define OK 1
#define ERROR 0
typedef int Status; typedef char VerTexType;
typedef int InfoType;
typedef struct ArcNode {int adjvex; //该边所指向的顶点的位置struct ArcNode* nextarc; //指向下一条边的指针InfoType info; //和边相关的信息
}ArcNode;
typedef struct VNode {VerTexType data; //顶点信息ArcNode* firstarc; // 指向第一条依附该顶点的边的指针
}VNode, AdjLst[MVNUM]; //AdjList表示邻接表类型
//AdjLst v == VNode v[MVNUM]
typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct {AdjLst vertices; //vertices=vertex的复数int vexnum, arcnum; //图当前顶点数和弧数GraphKind kind; //图的种类标志
}ALGraph;Status CreateGraph(ALGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网case UDN: return CreateUDN(G); //构造无向网*/case UDG: return CreateUDG(G); //构造无向图default:return ERROR;}
}int LocateVex(ALGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vertices[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateUDG(ALGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表// 1、依次输入点的信息存入顶点表中// 2、使每个表头结点的指针域初始化为NULLint i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");scanf(" %c", &G->vertices[i].data);G->vertices[i].firstarc = NULL;}// 创建邻接表// 1、依次输入每条边依附的两个顶点// 2、确定两个顶点的序号i和j,建立边结点// 3、将此边结点分别插入到vi和vj对应的两个边链表的头部for (k = 0; k < G->arcnum; k++){ printf("输入一条边依附的两个顶点: \n");scanf(" %c %c", &v1, &v2);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*p1ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));if (!p1) {// 处理内存分配失败return NULL;}p1->adjvex = j;//头插法p1->nextarc = G->vertices[i].firstarc;G->vertices[i].firstarc = p1;//生成一个新的边结点*p2ArcNode* p2 = (ArcNode*)malloc(sizeof(ArcNode));if (!p2) {// 处理内存分配失败return NULL;}p2->adjvex = i;p2->nextarc = G->vertices[j].firstarc;G->vertices[j].firstarc = p2;}for (i = 0; i < G->vexnum; i++){printf("%c -> ", G->vertices[i].data);ArcNode* p;p = G->vertices[i].firstarc;while (p){printf("%c ", G->vertices[p->adjvex].data);p = p->nextarc;}printf("\n");}return OK;
}int main()
{ALGraph G;CreateGraph(&G);return 0;
}
邻接矩阵和邻接表
- 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
- 区别:
- 对于任一确定的无向图邻接矩阵是唯一的(行列号与顶点编号致),但邻接表不唯一(链接次序与顶点编号无关)
- 邻接矩阵的空间复杂度为O(n2), 而邻接表的空间复杂度为有向图O(n+e) 或 无向图O(n+2e) 。
- 用途:邻接短阵多用于稠密图;而邻接表多用在稀疏图
7.2.3 十字链表(Orthogonal List)
是有向图的另一种链式存储结构。可以看成是将有向图 的邻接表和逆邻接表结合起来得到的一种链表。
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include<stdlib.h>#define MVNUM 20#define OK 1
#define ERROR 0
typedef int Status; typedef struct ArcBox {int tailvex, headvex; //该弧的尾和头顶点的位置struct ArcBox* hlink, * tlink;//分别为弧头相同和弧尾相同的弧的链域
}ArcBox;
typedef char VerTexType;
typedef struct VexNode {VerTexType data;ArcBox* firstin, * firstout;//分别指向该顶点第一条入弧和出弧
}VexNode;
typedef enum { DG, DN} GraphKind; // {有向图,有向网}
typedef struct {VexNode xlist[MVNUM]; //表头向量int vexnum, arcnum; //有向图的当前顶点数和弧数GraphKind kind; //图的种类标志
}OLGraph;Status CreateGraph(OLGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {case DG: return CreateDG(G); //构造有向图/*case DN: return CreateDN(G); //构造有向网*/default:return ERROR;}
}int LocateVex(OLGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->xlist[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateDG(OLGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表int i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");scanf(" %c", &G->xlist[i].data);G->xlist[i].firstin = NULL;G->xlist[i].firstout = NULL;}// 创建十字链表for (k = 0; k < G->arcnum; k++){ printf("输入一条边依附的两个顶点: \n");scanf(" %c %c", &v1, &v2);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*pArcBox* p = (ArcBox*)malloc(sizeof(ArcBox));p->tailvex = i;p->headvex = j;p->hlink = G->xlist[j].firstin;p->tlink = G->xlist[i].firstout;G->xlist[j].firstin = G->xlist[i].firstout = p;}for ( i = 0; i < G->vexnum; i++){printf("%c的hlink:", G->xlist[i].data);ArcBox* p1;p1 = G->xlist[i].firstin;while (p1){/*printf("%c %c ", G->xlist[p1->tailvex].data, G->xlist[p1->headvex].data);*/printf("-> %d %d ", p1->tailvex, p1->headvex);p1 = p1->hlink;}printf("\n");printf("%c的tlink:", G->xlist[i].data);ArcBox* p2;p2 = G->xlist[i].firstout;while (p2){/*printf("%c %c ", G->xlist[p2->tailvex].data, G->xlist[p2->headvex].data);*/printf("-> %d %d ", p2->tailvex, p2->headvex);p2 = p2->tlink;}printf("\n");}return OK;
}int main()
{OLGraph G;CreateGraph(&G);return 0;
}
7.2.4 邻接多重表(Adjacent MultiList)
是无向图的另一种链式存储结构。解决在邻接表中每一条边(vi , vj)有两个结点,分别在第i个和第j个链表中某些图的操作的不便
#define MVNUM 20typedef enum{unvisited,visited} VisitIf;
typedef int InfoType;
typedef struct EBox
{VisitIf mark; // 访问标记int ivex, jvex; // 该边依附的两个顶点的位置struct EBox* ilink, * j1ink; // 分别指向依附这两个顶点的下一条边InfoType* info;//该边信息指针
}EBox;typedef char VertexType;
typedef struct VexBox {VertexType data;EBox* firstedge; //指向第一条依附该顶点的边
}VexBox;typedef struct {VexBox adjmulis[MVNUM];int vexnum, edgenum; //无向图的当前顶点数和边数
}AMLGraph;
7.3 图的遍历
-
定义:
- 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历。
-
特点:
- 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。
-
避免重复访问:
-
设置辅助数组visited[n],用来标记每个被访问过的顶点。
初始状态visited [i]为0
顶点i被访问,改 visited [i]为1,防止被多次访问
-
7.3.1 深度优先搜索(Depth_First Search-DFS )
算法步骤 :
- ① 访问初始结点 : 在访问图中某一起始顶点 v , 并将该初始结点 v 标记为 " 已访问 " ;
- ② 查找邻接节点 : 由v出发,访问它的任一邻接顶点 w1 ; 再从w1出发,访问与w1邻接但还未被访问过的顶点w2,然后再从w2出发,进行类似的访问,…… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。
- ③ 接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点
- 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的
- 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
/* 邻接矩阵表示的无向图深度遍历实现 */#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <stdbool.h>#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;MGraph G;
bool visited[MAX_VERTEX_NUM];
Status(*VisitFunc)(int v);/*无向图 1、初始化邻接矩阵时,w=0 2、构造邻接矩阵时,w=1/ \无向网 + 即 有向图\ /有向网 邻接矩阵非对称矩阵 仅为G->arcs[i][j]赋值,无需为G->arcs[j][i]赋值
*/
Status CreateGraph(MGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网 case UDN: return CreateUDN(G); //构造无向网*/case UDG: return CreateUDG(G); //构造无向图default:return ERROR;}
}Status CreateUDG(MGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = 0;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2 : \n");scanf(" %c %c", &v1, &v2); // 输入一条边依附的顶点及权值// 确定 v1 和 v2 在 G 中位置i = LocateVex(G, v1);j = LocateVex(G, v2);G->arcs[i][j] = 1; // 弧<v1, v2> 的权值 G->arcs[j][i] = G->arcs[i][j]; // <v1, v2> 的对称弧 <v2, v1>}// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {printf("%d ", G->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}int FirstAdjVex(MGraph G, int v) {int j;for (j = 0; j < G.vexnum; j++){if (G.arcs[v][j]){return j;}}return -1;
}int NextAdjVex(MGraph G, int v, int w) {int j;for (j = w + 1; j < G.vexnum; j++){if (G.arcs[v][j]){return j;}}return -1;
}void DFS(MGraph G, int v)
{//访问第v个顶点visited[v] = true;VisitFunc(v);int w;//依次检查邻接矩阵v所在的行for (w = FirstAdjVex(G, v); w >=0; w = NextAdjVex(G, v, w)){//w是v的邻接点,如果w未访问,则递归调用DFSif (!visited[w]){DFS(G, w);}}
}void DFSTraverse(MGraph G, Status(*Visit)(int v))
{VisitFunc = Visit;int v;for (v = 0; v < G.vexnum; v++){visited[v] = false;}for (v = 0; v < G.vexnum; v++){if (!visited[v]) //对尚未访间的顶点调用 DFS{ DFS(G, v); } }
}Status MyVisit(int v)
{printf("%d-%c ", v, G.vexs[v]);return OK;
}int main()
{CreateGraph(&G);printf("----------DFS---------\n");DFSTraverse(G, MyVisit);return 0;
}
算法效率:
用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)
用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访间 n个头结点的时间,时间复杂度为O(n+e)。
结论:
稠密图适于在邻接矩阵上进行深度遍历;
稀疏图适于在邻接表上进行深度遍历。
7.3.2 广度优先搜索(Breadth_Frist Search-BFS)
算法步骤:
-
从图的某一结点出发,首先依次访问该结点的所有邻接点 Vi1,Vi2…… Vin,
-
再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点
-
重复此过程,直至所有顶点均被访问为止。
/* bfs.h */
#ifndef __BFS_H__
#define __BFS_H__#define MVNUM 20
#define OK 1
#define ERROR 0
typedef int Status;typedef char VerTexType;
typedef int InfoType;
typedef struct ArcNode {int adjvex; //该边所指向的顶点的位置struct ArcNode* nextarc; //指向下一条边的指针InfoType info; //和边相关的信息
}ArcNode;
typedef struct VNode {VerTexType data; //顶点信息ArcNode* firstarc; // 指向第一条依附该顶点的边的指针
}VNode, AdjLst[MVNUM]; //AdjList表示邻接表类型
//AdjLst v == VNode v[MVNUM]
typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct {AdjLst vertices; //vertices=vertex的复数int vexnum, arcnum; //图当前顶点数和弧数GraphKind kind; //图的种类标志
}ALGraph;Status CreateGraph(ALGraph* G);
int LocateVex(ALGraph* G, VerTexType v);
Status CreateUDG(ALGraph* G);#endif
/* bfs.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "bfs.h"
#include <stdio.h>
#include <stdlib.h>Status CreateGraph(ALGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网case UDN: return CreateUDN(G); //构造无向网*/case UDG: return CreateUDG(G); //构造无向图default:return ERROR;}
}int LocateVex(ALGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vertices[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateUDG(ALGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表// 1、依次输入点的信息存入顶点表中// 2、使每个表头结点的指针域初始化为NULLint i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");scanf(" %c", &G->vertices[i].data);G->vertices[i].firstarc = NULL;}// 创建邻接表// 1、依次输入每条边依附的两个顶点// 2、确定两个顶点的序号i和j,建立边结点// 3、将此边结点分别插入到vi和vj对应的两个边链表的头部for (k = 0; k < G->arcnum; k++){printf("输入一条边依附的两个顶点: \n");scanf(" %c %c", &v1, &v2);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*p1ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));if (!p1) {// 处理内存分配失败return NULL;}p1->adjvex = j;//头插法p1->nextarc = G->vertices[i].firstarc;G->vertices[i].firstarc = p1;//生成一个新的边结点*p2ArcNode* p2 = (ArcNode*)malloc(sizeof(ArcNode));if (!p2) {// 处理内存分配失败return NULL;}p2->adjvex = i;p2->nextarc = G->vertices[j].firstarc;G->vertices[j].firstarc = p2;}for (i = 0; i < G->vexnum; i++){printf("%c -> ", G->vertices[i].data);ArcNode* p;p = G->vertices[i].firstarc;while (p){printf("%c ", G->vertices[p->adjvex].data);p = p->nextarc;}printf("\n");}return OK;
}
/* queue.h */
#ifndef __QUEUE_H__
#define __QUEUE_H__#define MAXQSIZE 10
#define OK 1
#define OVERFLOW -1
#define ERROR -2typedef int Status;
typedef struct {int* base;int front;int rear;
} SqQueue;Status InitQueue(SqQueue* Q);
int QueueEmpty(SqQueue Q);
Status EnQueue(SqQueue* Q, int e);
Status DeQueue(SqQueue* Q, int* e);
void DestroyQueue(SqQueue* Q);#endif
/* queue.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "queue.h"
#include <stdio.h>
#include <stdlib.h>Status InitQueue(SqQueue* Q) {Q->base = (int*)malloc(MAXQSIZE * sizeof(int));if (!Q->base) exit(1); // 使用exit(1)表示错误退出Q->front = Q->rear = 0;return OK;
}int QueueEmpty(SqQueue Q) {if (Q.front == Q.rear) {return 0; // 队列为空,返回0}return 1;
}Status EnQueue(SqQueue* Q, int e) {if ((Q->rear + 1) % MAXQSIZE == Q->front) {return OVERFLOW; // 队列已满,返回OVERFLOW}Q->base[Q->rear] = e;Q->rear = (Q->rear + 1) % MAXQSIZE;return OK;
}Status DeQueue(SqQueue* Q, int* e) {if (Q->front == Q->rear) {return ERROR; // 队列为空,返回ERROR}*e = Q->base[Q->front]; // 使用引用来修改e的值Q->front = (Q->front + 1) % MAXQSIZE;return OK;
}void DestroyQueue(SqQueue* Q) {free(Q->base);Q->base = NULL;Q->front = Q->rear = 0;
}
/* test.c*/
/* 邻接表表示的无向图广度遍历实现 */#define _CRT_SECURE_NO_WARNINGS *1#include "bfs.h"
#include "queue.h"
#include <stdbool.h>ALGraph G;
bool visited[MVNUM];Status MyVisit(int v)
{printf("%d-%c ", v, G.vertices[v].data);return OK;
}int FirstAdjVex(ALGraph G, int v) {ArcNode* p = G.vertices[v].firstarc;if (p){return p->adjvex;}return -1;
}int NextAdjVex(ALGraph G, int v, int w) {ArcNode* p = G.vertices[v].firstarc;while (p){if (p->adjvex == w){return p->nextarc->adjvex;}p = p->nextarc;}return -1;
}void BFS(ALGraph G, Status(*Visit)(int v)) {int v;for (v = 0; v < G.vexnum; v++){visited[v] = false;}SqQueue Q;InitQueue(&Q);for (v = 0; v < G.vexnum; v++){if (!visited[v]) // v尚未访问{visited[v] = true;Visit(v);EnQueue(&Q, v);int u, w;while (!QueueEmpty(Q)){DeQueue(&Q, &u);for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)){if (!visited[w]){visited[w] = true;Visit(w);EnQueue(&Q, w);}}}}}
}int main()
{CreateGraph(&G);printf("---------BFS---------\n");BFS(G, MyVisit);return 0;
}
算法效率:
-
用邻接矩阵来表示图,则BFS对子每一个被访问到的顶点,都要循环检测矩阵中的整整一行(n个元素),总的时间代价为O(n2)。
-
用邻接表来表示图,虽然有 2e个表结点,但只需扫描e个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为O(n+e)。
DFS和BFS算法效率比较:
空间复杂度相同,都是O(n)(借用了堆栈或队列)
时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。
7.4 图的连通性问题
7.4.1 无向图的连通分量和生成树
-
生成树: 所有顶点均由边连接在一起,但不存在回路的图。
-
一个图可以有许多棵不同的生成树
-
所有生成树具有以下共同特点:
- 生成树的顶点个数与图的顶点个数相同;
- 生成树是图的极小连通子图,去掉一条边则非连通;
- 一个有 n个顶点的连通图的生成树有n-1条边;【含 n个顶点 n-1 条边的图不一定是生成树。】
- 在生成树中再加一条边必然形成回路;
- 生成树中任意两个顶点间的路径是唯一的。
/* algraph.h */
#ifndef __ALGRAPH_H__
#define __ALGRAPH_H__#define MVNUM 20
#define OK 1
#define ERROR 0
typedef int Status;typedef char VerTexType;
typedef int InfoType;
typedef struct ArcNode {int adjvex; //该边所指向的顶点的位置struct ArcNode* nextarc; //指向下一条边的指针InfoType info; //和边相关的信息
}ArcNode;
typedef struct VNode {VerTexType data; //顶点信息ArcNode* firstarc; // 指向第一条依附该顶点的边的指针
}VNode, AdjLst[MVNUM]; //AdjList表示邻接表类型
//AdjLst v == VNode v[MVNUM]
typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct {AdjLst vertices; //vertices=vertex的复数int vexnum, arcnum; //图当前顶点数和弧数GraphKind kind; //图的种类标志
}ALGraph;Status CreateGraph(ALGraph* G);
int LocateVex(ALGraph* G, VerTexType v);
Status CreateUDG(ALGraph* G);
VerTexType GetVex(ALGraph G, int loc);#endif
/* algraph.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "algraph.h"
#include <stdio.h>
#include <stdlib.h>Status CreateGraph(ALGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网case UDN: return CreateUDN(G); //构造无向网*/case UDG: return CreateUDG(G); //构造无向图default:return ERROR;}
}int LocateVex(ALGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vertices[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateUDG(ALGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表// 1、依次输入点的信息存入顶点表中// 2、使每个表头结点的指针域初始化为NULLint i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");scanf(" %c", &G->vertices[i].data);G->vertices[i].firstarc = NULL;}// 创建邻接表// 1、依次输入每条边依附的两个顶点// 2、确定两个顶点的序号i和j,建立边结点// 3、将此边结点分别插入到vi和vj对应的两个边链表的头部for (k = 0; k < G->arcnum; k++){printf("输入一条边依附的两个顶点: \n");scanf(" %c %c", &v1, &v2);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*p1ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));if (!p1) {// 处理内存分配失败return NULL;}p1->adjvex = j;//头插法p1->nextarc = G->vertices[i].firstarc;G->vertices[i].firstarc = p1;//生成一个新的边结点*p2ArcNode* p2 = (ArcNode*)malloc(sizeof(ArcNode));if (!p2) {// 处理内存分配失败return NULL;}p2->adjvex = i;p2->nextarc = G->vertices[j].firstarc;G->vertices[j].firstarc = p2;}for (i = 0; i < G->vexnum; i++){printf("%c -> ", G->vertices[i].data);ArcNode* p;p = G->vertices[i].firstarc;while (p){printf("%c ", G->vertices[p->adjvex].data);p = p->nextarc;}printf("\n");}return OK;
}//返回图G的顶点位置为loc的顶点信息
VerTexType GetVex(ALGraph G, int loc)
{return G.vertices[loc].data;
}
/* cstree.h */
#ifndef __CSTREE_H__
#define __CSTREE_H__#include "algraph.h"typedef VerTexType TElemType;
//采用孩子兄弟链表存储结构
typedef struct {TElemType data;struct CSNode* lchild;struct CSNode* nextsibling;
}CSNode, * CSTree;//先根遍历树T
void PreOrderTraverse(CSTree T);
//中根遍历树T
void InOrderTraverse(CSTree T);
//返回图G中与顶点v相连的第一个顶点在图中的位置
int FirstAdjVex(ALGraph G, int v);
//返回图G中与顶点v相邻的w的下一个相邻的定点在图中的位置
int NextAdjVex(ALGraph G, int v, int w);
//从第v个顶点出发深度优先遍历图G,建立以T为根的生成树
void DFSTree(ALGraph G, int V, CSTree* T);
//建立无向图G的深度优先生成森林的孩子兄弟链表T
void DFSForest(ALGraph G, CSTree* T);#endif
/* cstree.c */
#define _CRT_SECURE_NO_WARNINGS 1#include "algraph.h"
#include "cstree.h"
#include <stdio.h>
#include <stdlib.h>int visited[MVNUM];
CSTree DFSTree_q = NULL;
int ISFirst = 1;//先根遍历树T
void PreOrderTraverse(CSTree T) {if (T) {printf("%c\t", T->data);PreOrderTraverse((CSTree)T->lchild);PreOrderTraverse((CSTree)T->nextsibling);return;}else {return;}
}//中根遍历树T
void InOrderTraverse(CSTree T) {if (T) {InOrderTraverse((CSTree)T->lchild);printf("%c\t", T->data);InOrderTraverse((CSTree)T->nextsibling);return;}else {return;}
}// 返回图G中与顶点v相连的第一个顶点在图中的位置
int FirstAdjVex(ALGraph G, int v)
{ArcNode* p = G.vertices[v].firstarc;if (p){return p->adjvex;}return -1;
}//返回图G中与顶点v相邻的w的下一个相邻的定点在图中的位置
int NextAdjVex(ALGraph G, int v, int w)
{ArcNode* p = G.vertices[v].firstarc;while (p){if (p->adjvex == w){return (p->nextarc) ? p->nextarc->adjvex : -1;}p = p->nextarc;}return -1;
}//从第v个顶点出发深度优先遍历图G,建立以T为根的生成树
void DFSTree(ALGraph G, int v, CSTree* T)
{int w = 0;CSTree p = NULL;visited[v] = 1;for (w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w)) {if (!visited[w]) {//分配孩子结点p = (CSTree)malloc(sizeof(CSNode));if (p){p->data = GetVex(G, w);p->lchild = NULL;p->nextsibling = NULL;if (ISFirst) {//w是v的第一个未被访问的邻接顶点ISFirst = 0;(*T)->lchild = p;}else {//w是v的其它未被访问的邻接顶点//是上一个邻接顶点的右兄弟结点 DFSTree_q->nextsibling = p;}DFSTree_q = p;//从第w个顶点出发深度优先遍历图G,建立子生成树DFSTree_qDFSTree(G, w, &DFSTree_q);}}}
}//建立无向图G的深度优先生成森林的孩子兄弟链表T
void DFSForest(ALGraph G, CSTree* T)
{CSTree p = NULL;CSTree q = NULL;*T = NULL;int v = 0;for (v = 0; v < G.vexnum; v++) {visited[v] = 0;}for (v = 0; v < G.vexnum; v++) {if (!visited[v]) {//第v个顶点为新的生成树的根结点p = (CSTree)malloc(sizeof(CSNode));if (p){p->data = GetVex(G, v);p->lchild = NULL;p->nextsibling = NULL;if (!(*T)) {//是第一颗生成树的根*T = p;}else {//是其他生成树的根(前一颗的根的“兄弟”)q->nextsibling = p;}//q指示当前生成树的根q = p;//建立以p为根的生成树ISFirst = 1;DFSTree(G, v, &p);} }}
}
/* test.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "algraph.h"
#include "cstree.h"int main()
{ALGraph G;//创建一个无向图CreateUDG(&G);CSTree T;//依照无向图G,建立一颗生成森林,并将其转换成成二叉树存储,二叉树以孩子兄弟链表存储结构存储DFSForest(G, &T);//先根遍历该生成树PreOrderTraverse(T); printf("\n");//中根遍历该生成树InOrderTraverse(T); printf("\n");return 0;return 0;
}
7.4.1.1 Tarjan算法 - 解决无向图的割点和桥问题
👉定义:
给定一个无向连通图 G=(V,E)
若对于 x∈V , 如果从图中删去节点 x 以及与 x 相连的边后, G 分裂成两个或者多个不相连的连通块, 那么这个点是一个割点/割顶;
若对于 e∈E , 如果从图中删去这条边后,G 分裂成两个不相连的连通块,那么就说这个e是一个桥或割边;
👉工作原理:
【时间戳和追溯值的目的是用来识别割点和割边】
-
时间戳:
时间戳是用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序,可以理解成一个序号(这个序号由小到大),用 dfn[x] 来表示。—— 时间戳就是记录了每个顶点在DFS过程中被首次访问的顺序,节点访问的越早,数值就越小(DFN值是一个递增的序列号)
如下图:dfn[1] = 1; dfn[2] = 2; dfn[3] = 3; dfn[4] = 4; dfn[5] = 5; dfn[6] = 6; dfn[7] = 7; dfn[8] = 8; dfn[9] = 9;
-
搜索树
在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,称之为搜索树。
-
追溯值
追溯值用来表示从当前节点 x 通过非树边能够回到的最早节点的编号,也就是
DFN
的最小值。—— low[x]。【记录了每个顶点在DFS过程中,通过非树边能够回溯到的已经被访问的且是最早的节点。对于某个顶点u
,low[u]
表示从u
出发,只通过非树边能够到达的祖先节点中最小的dfn
值。】在无向图中,割点和割边的追溯值(low值)的计算方式是不同的。例如:
5节点是 通过非树边e 回到2节点 ,low[5] = min(low[5], dfn[2]) = min(5, 2) = 2;
5节点是 通过非树边b 回到1节点 ,low[5] = min(low[5], dfn[1]) = min(2, 1) = 1;
-
割点/割顶识别机制:
-
非根节点
非根节点u,存在至少一个子结点v 使得
low[v] ≥ dfn[u]
【子结点可以通过非树边追溯到最早被访问的节点的序号 ≥ 父结点第一次被访问时的序号】意味着子节点v 及其后代无法通过非树边回溯u更早访问的祖先节点【u的祖先访问顺序 小于 u的访问顺序 – dfn[u的祖先] < dfn[u], 如果u的子节点v 及其后代可以通过非树边访问u的祖先,那么low[v] = min{low[v] , dfn[u的祖先]} = dfn[u的祖先] < dfn[u], 那么 low[v] ≥ dfn[u]就是v及其后代无法通过非树边访问u的祖先】,u的移除会v及其子树与图的其它部分不连通。
-
根节点
由于根节点没有祖先节点,所以它的
LOW
值不会通过非树边更新。判断根节点是否为割点的方法是看它是否有多于一个的子树。如果有,那么根节点是割点;如果没有,那么根节点不是割点。
总结:
-
-
点双连通分量(v-DCC):
-
若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称整个无向图叫作点双连通图。(整个无向图中任意两个顶点之间至少存在两条顶点不相交的路径。)
-
在无向图中,点双连通分量是指删除任意一个顶点(及其相关的边)之后,仍然保持连通的子图。换句话说,点双连通分量是一个最大的子图,在这个子图中,任何顶点都不是割点。每个割点至少属于两个连通分量。(该子图中任意两个顶点之间至少存在两条顶点不相交的路径。)
-
点双连通 不 具有传递性——如下图 1和3点双,1和6点双,但是3和6不是点双
-
-
割点:
将所有点双连通分量都缩成点并重新编号,把缩点和割点(在缩点编号基础上继续编号)连边,构成一棵树(或森林)。
-
桥/割边识别机制:
对于任意两个顶点u和v,如果在DFS过程中,v是u的子节点,且
LOW[v] > DFN[u]
,则边(u, v)
是一条割边(桥)。这意味着从v出发,无法通过非树边回溯到u的祖先节点
总结:
-
边双连通分量(e-DCC):
- 若一个无向图中的去掉任意一条边都不会改变此图的连通性,即不存在桥,则称整个无向图叫作边双连通图。
- 在连通的无向图中, 边双连通分量是指删除任意一条边之后, 仍然保持连通的子图。换句话说, 边双连通分量是一个最大的子图,在这个子图中,任何边都不是桥。
- 边双连通具有传递性,即若 x,y 边双连通,y,z边双连通,则 x,z 边双连通。
-
缩点:
将边双连通分量缩为一个点,缩完点后得到的图一定是一棵树(或森林)树边就是原来的割边。
/*割点、点双、缩点图*/
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>#define SIZE 1000int head[SIZE], ver[SIZE * 2], Next[SIZE * 2]; //无向图的邻接表的头节点,存储边的目标节点,存储下一条边的索引
int dfn[SIZE], low[SIZE], stack[SIZE]; //记录每个节点的时间戳,记录每个节点的追溯值,存储当前DFS路径上的节点
int new_id[SIZE], c[SIZE]; // 存储割点的新编号, 存储非割点所属的点双连通分量编号
int n, m;//图的节点数和边数
int tot, num, cnt, tc; // 边的计数器, 时间戳计数器,点双连通分量计数器,缩点图的边计数器
int root;//当前DFS的根节点
int top;//栈顶指针
int hc[SIZE], vc[SIZE * 2], nc[SIZE * 2];//缩点图的邻接表的头节点,存储边的目标节点,存储下一条边的索引
int cut[SIZE];//标记割点
int dcc[SIZE][SIZE];// 存储每个点双连通分量中的节点// 添加边到邻接表
void add(int x, int y) {ver[++tot] = y; // 将目标节点y存入ver数组Next[tot] = head[x]; // 将当前边的下一条边指向x的头节点head[x] = tot;// 更新x的头节点为当前边
}// 添加边到缩点图
void add_c(int x, int y) {vc[++tc] = y;// 将目标节点y存入vc数组nc[tc] = hc[x];// 将当前边的下一条边指向x的头节点hc[x] = tc;// 更新x的头节点为当前边
}/*
Tarjan 割点:
1、初始化时间戳和追溯值
2、将当前遍历的节点压入栈中
3、遍历该节点x的所有邻接边4、获取目标节点5、通过判断目标节点是否被访问从而判断是否是非树边树边:递归Tarjan回溯low[x] = (low[x] < low[y]) ? low[x] : low[y];判断是否是割点low[y] >= dfn[x]是:记录子结点数根节点的子节点数大于1才能存储,非根节点直接存储割点记录点双数存储点双连通分量非树边:low[x] = (low[x] < dfn[y]) ? low[x] : dfn[y];
*/// Tarjan算法主函数
void tarjan(int x) {dfn[x] = low[x] = ++num; // 初始化时间戳和追溯值stack[++top] = x; // 当前遍历的节点压入stack栈中if (x == root && head[x] == 0) { // 孤立点dcc[++cnt][x] = 1;return;}int flag = 0;// 记录当前节点的子节点数量, 主要目的判断根节点的子节点数是否大于1,for (int i = head[x]; i; i = Next[i]) { // 遍历当前节点的所有邻接边int y = ver[i]; // 获取目标节点if (!dfn[y]) {// 如果目标节点未被访问tarjan(y);// 递归访问目标节点low[x] = low[x] < low[y] ? low[x] : low[y]; //回溯的时候 父节点的追溯值=min{父节点的追溯值,子节点的追溯值} 因为 x是 y的父节点,y能访问到的点,x一定也能访问到。if (low[y] >= dfn[x]) {//判断割点flag++;//记录子节点数if (x != root || flag > 1)//根节点的子节点数是否大于1才能存储,非根节点直接存储{cut[x] = 1;//存放割点}cnt++;//记录点双数量int z;do {z = stack[top--]; //记录出栈的节点dcc[cnt][z] = 1; //记录点双连通分量} while (z != y);dcc[cnt][x] = 1; //将割点也记录到点双连通分量中}}else // 非树边情况{low[x] = low[x] < dfn[y] ? low[x] : dfn[y]; //父节点的追溯值 = min{ 父节点的追溯值,子节点的时间戳 }}}
}int main() {// 输入节点数和边数printf("请输入节点数和边数:\n");scanf("%d %d", &n, &m);tot = 1; // 初始化边计数器/* 构建无向图 */for (int i = 1; i <= m; i++) { // 输入每条边int x, y;printf("输入一条边依附的两个顶点:\n");scanf("%d %d", &x, &y);if (x == y) continue; // 忽略自环add(x, y); // 添加边add(y, x); // 添加反向边}/* 调用无向图tarjan算法 */for (int i = 1; i <= n; i++)if (!dfn[i]) {root = i;tarjan(i);}/* 输出割点 */for (int i = 1; i <= n; i++)if (cut[i]) printf("%d ", i);puts("are cut-vertexes");/* 输出每个点双连通分量 */for (int i = 1; i <= cnt; i++) {printf("v-DCC #%d:", i);for (int j = 1; j <= n; j++)if (dcc[i][j]) printf(" %d", j);puts("");}// 给每个割点一个新的编号(编号从cnt+1开始),割点是在点双编号之后num = cnt;for (int i = 1; i <= n; i++)if (cut[i]) new_id[i] = ++num; // 为割点分配新编号// 建新图,从每个v-DCC到它包含的所有割点连边tc = 1;// 初始化新图边计数器for (int i = 1; i <= cnt; i++)// 遍历每个点双连通分量for (int j = 1; j <= n; j++) {int x = j;if (dcc[i][x]) {// 如果当前节点在点双连通分量中if (cut[x]) {// 如果当前节点是割点add_c(i, new_id[x]);// 添加边到新图add_c(new_id[x], i);// 添加反向边}else c[x] = i;// 除割点外,其它点仅属于1个v-DCC}}printf("缩点之后的森林,点数 %d,边数 %d\n", num, tc / 2);printf("编号 1~%d 的为原图的v-DCC,编号 >%d 的为原图割点\n", cnt, cnt);for (int i = 2; i <= tc; i += 2)printf("%d %d\n", vc[i ^ 1], vc[i]);return 0;
}
/*割边、边双、缩点图*/
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <string.h>
#include <stdlib.h>#define SIZE 1000int head[SIZE], ver[SIZE * 2], Next[SIZE * 2]; // 邻接表的头节点, 存储边的目标节点, 存储下一条边的索引
int dfn[SIZE], low[SIZE]; // 记录每个节点的发现时间 - 时间戳,记录每个节点能够回溯到的最小发现时间 - 追溯值
int c[SIZE];// 存储节点所属的边双连通分量编号
int n, m, tot, num, tc;// 图的节点数,边数,边的计数器, 发现时间计数器, 新图的边计数器
int dcc;//边双连通分量计数器
int bridge[SIZE * 2];// 存储桥
int hc[SIZE], vc[SIZE * 2], nc[SIZE * 2];//缩点图的邻接表的头节点,存储边的目标节点,存储下一条边的索引void add(int x, int y) {ver[++tot] = y;Next[tot] = head[x];head[x] = tot;
}void add_c(int x, int y) {vc[++tc] = y;nc[tc] = hc[x];hc[x] = tc;
}/*
Tarjan 割边:
1、初始化时间戳和追溯值
2、遍历该节点x的所有邻接边3、获得目标节点4、通过判断目标节点是否被访问从而判断是否是非树边树边:递归Tarjan回溯low[x] = (low[x] < low[y]) ? low[x] : low[y];判断是否是割边low[y] > dfn[x]是:存储割边非树边且不是反向边:low[x] = (low[x] < dfn[y]) ? low[x] : dfn[y];
*/void tarjan(int x, int in_edge) {dfn[x] = low[x] = ++num; //初始化时间戳和追溯值for (int i = head[x]; i; i = Next[i]) { // 遍历当前节点的所有邻接边int y = ver[i];// 获取目标节点if (!dfn[y]) {// 如果目标节点未被访问 - 树边tarjan(y, i);// 递归访问目标节点low[x] = (low[x] < low[y]) ? low[x] : low[y]; //回溯的时候 父节点的追溯值=min{父节点的追溯值,子节点的追溯值} 因为 x是 y的父节点,y能访问到的点,x一定也能访问到。if (low[y] > dfn[x]) //判断割边/桥//bridge[i] = 1 表示从某个节点出发指向其邻接节点的边是桥。//bridge[i ^ 1] = 1 表示该边的反向(即从邻接节点返回原节点的边)是桥。bridge[i] = bridge[i ^ 1] = 1;}//非树边else if (i != (in_edge ^ 1)) //判断当前边不是in_edge这条边的反向边 low[x] = (low[x] < dfn[y]) ? low[x] : dfn[y]; //父节点的追溯值 = min{ 父节点的追溯值,子节点的时间戳 }}
}
/*1、存储该节点编号到c[x]2、遍历该节点x的所有邻接边3、获得目标节点4、判断目标节点是否在c中有编号 || 该边是割边至少有一个是:continue,获取下一个邻接边都不是:递归dfs
*/
void dfs(int x) {c[x] = dcc;for (int i = head[x]; i; i = Next[i]) {int y = ver[i];if (c[y] || bridge[i]) continue;dfs(y);}
}int main() {// 输入节点数和边数printf("请输入节点数和边数:\n");scanf("%d %d", &n, &m);tot = 1;for (int i = 1; i <= m; i++) {int x, y;printf("输入一条边依附的两个顶点:\n");scanf("%d %d", &x, &y);add(x, y);add(y, x);}for (int i = 1; i <= n; i++)if (!dfn[i]) tarjan(i, 0);printf("Bridges are: \n");for (int i = 2; i < tot; i += 2)if (bridge[i])printf("%d %d\n", ver[i ^ 1], ver[i]);printf("---------------------------");// 存储边双连通分量编号for (int i = 1; i <= n; i++)if (!c[i]) {++dcc;dfs(i);}printf("There are %d e-DCCs.\n", dcc);for (int i = 1; i <= n; i++)printf("%d belongs to DCC %d.\n", i, c[i]);tc = 1;//存储边双连通分量for (int i = 2; i <= tot; i++) {int x = ver[i ^ 1], y = ver[i];if (c[x] == c[y]) continue;add_c(c[x], c[y]);}printf("缩点之后的森林,点数 %d,边数 %d\n", dcc, tc / 2);for (int i = 2; i < tc; i += 2)printf("%d %d\n", vc[i ^ 1], vc[i]);return 0;
}
7.4.2 有向图的强连通分量 (Strongly Connected Components - SCCs)
- 强连通
若图中有两个点u和v, 他们能互相到达, 则称他们强连通 - 强连通图
若是G中任意2个点都可以互相到达, 则称G是一个强连通图 - 强连通分量
有向非强连通图的极大强连通子图(可以有很多个)
7.4.2.1 Tarjan算法 - 解决有向图的强连通分量问题
👉工作原理:
【时间戳和追溯值的目的是用来识别强连通分量】
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>#define N 1000
#define M 1000int ver[M], Next[M], head[N];// 有向图信息:存储边的目标节点, 存储下一条边的索引, 邻接表的头节点
int dfn[N], low[N];// 时间戳,追溯值
int stack[N], ins[N], c[N]; // stack用于存储当前DFS路径上的节点,ins用于标记是否在栈中,c强连通分量编号
int vc[M], nc[M], hc[N], tc; // 缩点图信息:存储边的目标节点, 存储下一条边的索引, 邻接表的头节点,边计数器
int scc[N][N]; // 动态数组,用于存储强连通分量
int n, m, tot, num, top, cnt;// n和m分别是节点数和边数,tot是边的计数器,num是节点的发现时间计数器,top是栈顶指针,cnt是强连通分量的计数器//有向图
void add(int x, int y) {ver[++tot] = y;Next[tot] = head[x];head[x] = tot;
}//缩点图
void add_c(int x, int y) {vc[++tc] = y;nc[tc] = hc[x];hc[x] = tc;
}/*
Tarjan 强连通分量:
1、初始化时间戳和追溯值
2、当前遍历的节点压入stack栈中
3、记录节点在栈中存在
4、遍历当前节点的所有邻接边5、获取目标节点6、判断目标节点是否被访问过没有 - 树边:递归tarjan回溯:low[x] = low[x] < low[y] ? low[x] : low[y];有 - 非树边 + 目标存在栈中:low[x] = dfn[y] < low[x] ? dfn[y] : low[x];7、判断dfn[x] == low[x]记录SCC
*/void tarjan(int x) {/* 进入x节点 */dfn[x] = low[x] = ++num; //初始化时间戳和追溯值stack[++top] = x; // 当前遍历的节点压入stack栈中ins[x] = 1; //节点存在stack中,即为1;否则为0/* 遍历当前节点的所有邻接边 */for (int i = head[x]; i; i = Next[i]) {int y = ver[i];// 获取目标节点if (!dfn[y]) {// 如果目标节点未被访问 - 树边tarjan(y); // 递归访问目标节点 low[x] = low[x] < low[y] ? low[x] : low[y];//回溯的时候 父节点的追溯值=min{父节点的追溯值,子节点的追溯值} 因为 x是 y的父节点,y能访问到的点,x一定也能访问到。}else if (ins[y]) {// 非树边(目标节点被访问过) + 目标节点存在stack中//父节点的追溯值 = min{ 父节点的追溯值,子节点的时间戳 }low[x] = dfn[y] < low[x] ? dfn[y] : low[x];}}/* 离开x节点,记录SCC */if (dfn[x] == low[x]) {cnt++; // 记录强连通分量数量int y;do {y = stack[top--]; // 记录出栈节点ins[y] = 0; // 改节点存在stack中状态为0c[y] = cnt; // 记录目标节点对应的强连通分量编号scc[cnt][y] = 1; // 存储强连通分量中的节点} while (x != y);}
}int main() {// 输入节点数和边数printf("请输入节点数和边数:\n");scanf("%d %d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;printf("输入一条边依附的两个顶点(有向):\n");scanf("%d %d", &x, &y);add(x, y);}for (int i = 1; i <= n; i++){if (!dfn[i]) tarjan(i);}printf("There are %d SCCs.\n", cnt);for (int i = 1; i <= n; i++){printf("%d belongs to SCC %d.\n", i, c[i]);}for (int x = 1; x <= n; x++) {for (int i = head[x]; i; i = Next[i]) {int y = ver[i];if (c[x] == c[y]) continue;add_c(c[x], c[y]);}}return 0;
}
7.4.2.2 Kosaraju算法
核心思想:
- 对原图进行DFS:按照DFS完成的顺序将顶点压入栈中。
- 构建逆图:反转所有边的方向。
- 对逆图进行DFS:从栈顶开始,依次对每个顶点进行DFS,每次DFS会找到一个强连通分量。【对于点u来说,在遍历反向图时所有能够到达的v都和u在一个强连通分量当中】
/* mgraph.h */
#ifndef __MGRAPH_H__
#define __MGRAPH_H__#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;Status CreateMGraph(MGraph* G, MGraph* RG);Status CreateDG_M(MGraph* G, MGraph* RG);int LocateVex_M(MGraph* G, VertexType v);#endif
/* mgragh.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mgragh.h"Status CreateMGraph(MGraph* G, MGraph* RG)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;RG->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;RG->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;RG->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;RG->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {case DG: return CreateDG_M(G, RG); //构造有向图/*case DN: return CreateDN(G); //构造有向网case UDG: return CreateUDG(G); //构造无向图case UDN: return CreateUDN(G); //构造无向网*/default:return ERROR;}
}Status CreateDG_M(MGraph* G, MGraph* RG)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);RG->vexnum = G->vexnum;RG->arcnum = G->arcnum;int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符RG->vexs[i] = G->vexs[i];}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = 0;RG->arcs[i][j] = 0;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2 : \n");scanf(" %c %c", &v1, &v2); // 输入一条边依附的顶点// 确定 v1 和 v2 在 G 中位置i = LocateVex_M(G, v1);j = LocateVex_M(G, v2);G->arcs[i][j] = 1; // 弧<v1, v2> 的权值 RG->arcs[j][i] = 1; }// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {printf("%d ", G->arcs[i][j]);}printf("\n");}printf("---------------reverse-----------------\n");for (i = 0; i < RG->vexnum; ++i) {for (j = 0; j < RG->vexnum; ++j) {printf("%d ", RG->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex_M(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}
/* test.c*/
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include<stdlib.h>
#include "mgragh.h"#define MAXN 100 // 假设图中最多有100个节点int visited[MAXN]; // 访问标记数组
int color[MAXN]; // 用于标记强连通分量的颜色
int s[MAXN]; // 栈
int sccCnt = 0; // 强连通分量的数量
int top = -1; // 栈顶指针int FirstAdjVex(MGraph G, int v) {int j;for (j = 0; j < G.vexnum; j++){if (G.arcs[v][j]){return j;}}return -1;
}int NextAdjVex(MGraph G, int v, int w) {int j;for (j = w + 1; j < G.vexnum; j++){if (G.arcs[v][j]){return j;}}return -1;
}// 深度优先搜索函数,用于第一次DFS
void dfs1(MGraph G,int v) {visited[v] = 1;int w;//依次检查邻接矩阵v所在的行for (w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w)){//w是v的邻接点,如果w未访问,则递归调用DFSif (!visited[w]){dfs1(G, w);}}s[++top] = v; // 将节点u压入栈
}// 深度优先搜索函数,用于第二次DFS
void dfs2(MGraph RG, int u) {color[u] = sccCnt;int w;//依次检查邻接矩阵v所在的行for (w = FirstAdjVex(RG, u); w >= 0; w = NextAdjVex(RG, u, w)){if (!color[w]){dfs2(RG, w);}}
}// Kosaraju算法
void kosaraju(MGraph G, MGraph RG) {sccCnt = 0;top = -1; // 重置栈顶指针int i;for (i = 0; i < G.vexnum; ++i) {if (!visited[i]) {dfs1(G, i);}}for (int i = RG.vexnum - 1; i >= 0; --i) {if (!color[s[i]]) {++sccCnt;dfs2(RG, s[i]);}}
}int main()
{MGraph G, RG;//有向图G和其逆图RGCreateMGraph(&G, &RG);kosaraju(G, RG);// 打印结果for (int i = 0; i < G.vexnum; ++i) {printf("节点%d属于第%d个强连通分量\n", i, color[i]);}return 0;
}
7.4.3 最小生成树 (Minimum Spanning Tree - MST)
最小生成树定义:
给定一个无向网络在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树
MST 性质:- 本质是贪心算法
设 N =(V, E)是一个连通网,U 是顶点集 V的一个非空子集。
若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。
7.4.3.1 Prim 算法
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;/*无向图 1、初始化邻接矩阵时,w=0 2、构造邻接矩阵时,w=1/ \无向网 + 即 有向图\ /有向网 邻接矩阵非对称矩阵 仅为G->arcs[i][j]赋值,无需为G->arcs[j][i]赋值
*/
Status CreateGraph(MGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图case DN: return CreateDN(G); //构造有向网case UDG: return CreateUDG(G); //构造无向图*/case UDN: return CreateUDN(G); //构造无向网default:return ERROR;}
}Status CreateUDN(MGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = INFINITY;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2,weight : \n");scanf(" %c %c %d", &v1, &v2, &w); // 输入一条边依附的顶点及权值// 确定 v1 和 v2 在 G 中位置i = LocateVex(G, v1);j = LocateVex(G, v2);G->arcs[i][j] = w; // 弧<v1, v2> 的权值 G->arcs[j][i] = G->arcs[i][j]; // <v1, v2> 的对称弧 <v2, v1>}// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {if (G->arcs[i][j] == INFINITY){printf("∞ ");continue;}printf("%d ", G->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}int sum = 0;/** prim最小生成树** 参数说明:* G -- 邻接矩阵图* start -- 从图中的第start个元素开始,生成最小树*/
void prim(MGraph G, int start)
{int min, i, j, k, m, n;int index = 0; // prim最小树的索引,即prims数组的索引char prims[MAX_VERTEX_NUM]; // prim最小树的结果数组int weights[MAX_VERTEX_NUM]; // 顶点间边的权值// prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。prims[index++] = G.vexs[start];// 初始化"顶点的权值数组",// 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。for (i = 0; i < G.vexnum; i++)weights[i] = G.arcs[start][i];// 将第start个顶点的权值初始化为0。// 可以理解为"第start个顶点到它自身的距离为0"。weights[start] = 0;for (i = 0; i < G.vexnum; i++){// 由于从start开始的,因此不需要再对第start个顶点进行处理。if (start == i)continue;j = 0;k = 0;min = INFINITY;// 在未被加入到最小生成树的顶点中,找出权值最小的顶点。while (j < G.vexnum){// 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。if (weights[j] != 0 && weights[j] < min){min = weights[j];k = j;}j++;}sum += min;// 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。// 将第k个顶点加入到最小生成树的结果数组中prims[index++] = G.vexs[k];// 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。weights[k] = 0;// 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。for (j = 0; j < G.vexnum; j++){// 当第j个节点没有被处理,并且需要更新时才被更新。if (weights[j] != 0 && G.arcs[k][j] < weights[j])weights[j] = G.arcs[k][j];}}// 打印最小生成树printf("\n");printf("PRIM(%c)=%d: ", G.vexs[start], sum);for (i = 0; i < index; i++)printf("%c ", prims[i]);printf("\n");
}int main()
{MGraph G;CreateGraph(&G);prim(G, 0);return 0;
}
7.4.3.2 Kruskal 算法
问题1 如何寻找权值最小的边
- 应对策略:对图的所有边按照权值大小进行排序,然后按从小到大的顺序取出边。
问题2 如何判断边及其顶点加入最小生成树是否会形成回路。
- 应对策略:每条边机及其相连的顶点都视作一颗子树,然后判断这课子树的根是否和最小生成树的根相同;
- 若相同,则会形成回路;
- 若不同,则不会形成回路,将子树并入最小生成树。
步骤:
1、将每一条边存储到road数组
2、将road数组按每一条边的权值从大到小排序
3、遍历road数组,并获取该边的开始点和结束点对应的子树根root,相同为同一棵树会形成闭环,不同则合并两棵树使这两个节点的root值统一
/*mgragh.h*/
#include<stdio.h>// 顶点的最大个数
#define MaxVertix 30
#define INF 32767 // INF infinite 无穷大,表权重无穷大// 状态值
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0// 状态码 -- 状态值
typedef int Status;// 1.邻接矩阵 - 存储结构
// 定点、边的类型
typedef char VertexType; // 顶点的数据类型
typedef int ArcType; // 边的数据类型// 构造数据类型
typedef struct
{VertexType Verts[MaxVertix];ArcType UdArcs[MaxVertix][MaxVertix]; // 无向网 -- 矩阵表示法int VerNum; // 顶点个数int ArcNum; // 边的个数
}AMGraph; // Adjacency Matrix Graph 邻接矩阵// 弧结点类型 - 用狐表示的图结构
typedef struct
{int v1, v2; // 狐关联的两顶点下标int Weight; // 狐的权重
}Road;// 函数声明
Status CreateUDN(AMGraph* G); // 创建无向网//最小生成树的 Kruskal 克鲁斯卡尔 算法
Status CreateGTree_Kruskal(AMGraph* G); // 根据邻接矩阵图,从第 v 个结点构造 最小生成树
/* mgragh.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "mgragh.h"// 获取 顶点 ver 的下标
int LocateVertex(AMGraph* G, VertexType* v)
{if (!G) return ERROR;int i;for (i = 0; i < G->VerNum; i++)if (*v == G->Verts[i])return i;return -1; // 返回 -1 表示未找到顶点
}// 创建无向网
Status CreateUDN(AMGraph* G) // UndirectNet
{if (!G) return ERROR;printf("请输入顶点及边个数(Vers Arcs): \n");scanf("%d %d", &G->VerNum, &G->ArcNum);getchar();int i;//录入顶点printf("\n请输入顶点的值(英文字母): \n");for (i = 0; i < G->VerNum; i++){do{VertexType v;scanf("%c", &v); //scanf("%[a-zA-Z]", G->VerNum); // 只接收26个英文字母getchar();if ((65 <= v && v <= 90) || (97 <= v && v <= 122)){G->Verts[i] = v;break;}printf("输入错误,请输入英文字母!\n");} while (1); // do-while循环用于处理错误输入}//初始化所有边的权int j;for (i = 0; i < G->VerNum; i++)for (j = i; j < G->VerNum; j++){G->UdArcs[i][j] = INF; // 权重为无穷大,表示两顶点非邻接G->UdArcs[j][i] = INF;}//录入边的权值for (i = 0; i < G->ArcNum; i++){VertexType v1, v2;int w;do{printf("\n请输入边关联的顶点及权值(v1 v2 weight): \n");scanf("%c %c %d", &v1, &v2, &w);getchar();if (v1 < 65 || (90 < v1 && v1 < 97) || 122 < v1){printf("输入错误,请输入英文字母!\n");continue;}if (v2 < 65 || (90 < v2 && v2 < 97) || 122 < v2){printf("输入错误,请输入英文字母!\n");continue;}//查找顶点位置int a, b;a = LocateVertex(G, &v1);b = LocateVertex(G, &v2);if (a < 0) // 判断顶点是否存在{printf("输入的顶点%c不存在,请重新输入!\n", v1);continue;}if (b < 0) // 判断顶点是否存在{printf("输入的顶点%c不存在,请重新输入!\n", v2);continue;}//链接到两顶点的边赋权值G->UdArcs[a][b] = w;G->UdArcs[b][a] = w;break;} while (1); // do-while循环用于处理错误输入}return OK;
}// 将弧按从小到大排序
Status Sort(Road R[], int e)
{if (!R) return ERROR; // 处理空数组int i, j;// 冒泡排序for (i = 0; i < e - 1; i++) // e 个数字,排 e - 1 趟。排完 1 趟 i+1for (j = 1; j < e - 1 - i; j++) // 数字之间的大小关系所需要比较的次数。每排完一趟,比较次数-1if (R[j - 1].Weight > R[j].Weight) // 前一个数大于后面一个数,交换两数的位置{Road tmp = R[j - 1];R[j - 1] = R[j];R[j] = tmp;}return OK;
}//获取结点所在子树的根
int GetRoot(int r[], int len, int v) // len - 数组长度; v - 结点下标
{if (!r) return -1; // 处理空数组int i;for (i = 0; i < len; i++){if (r[v] == v) // 一个顶点存储的是自己的下标,则表示此顶点是一颗树的根结点return v;else v = r[v];}return -1;
}// 根据邻接矩阵图,从第 v 个结点构造 最小生成树
Status CreateGTree_Kruskal(AMGraph* G)
{/* 思路:以弧为单位,通过选取最小的弧来构造一颗颗局部的小树(小树也是最小生成树),再将小树合并为一个大树,就得到完整的最小生成树(采用了局部最优得到整体最优的思想)步骤:1.选取最小的弧2.判断最小弧关联的两顶点是否属于同一颗树a.若最小弧关联的两顶点属于不同的两颗树,就将这条弧和这两颗树合并到一颗树里面b.若最小弧关联的两顶点属于同一颗树,舍弃这条弧(两顶点都在一颗树里面了,还并入这条弧,就出现回路了,就不是树结构了)3.重复上述步骤,直至得到一个完整的最小生成树 *///处理空指针、非法下标if (!G)return ERROR;Road road[MaxVertix]; // 记录弧关联的顶点。通过弧来表示出图中顶点与顶点、顶点与弧、弧与弧之间的关系// 记录每颗树的根结点下标,用于判断弧关联的两顶点是否属于同一棵树,防止回路// 数组下标对应顶点,数组存储的值是顶点所属树的根的下标int root[MaxVertix];int i, j, k = 0;for (i = 0; i < G->VerNum; i++){root[i] = i; //初始化:将每个顶点视作独立的一颗树// 寻找连通顶点的下标、弧的权重,并记录下来if (k < G->ArcNum)for (j = i + 1; j < G->VerNum; j++) // 无向图的邻接矩阵是对称的,所以只有统计上三角或者下三角即可if (G->UdArcs[i][j] < INF) // 两顶点连通{road[k].v1 = i; // 记住顶点1的小标road[k].v2 = j; // 记住顶点2的小标road[k++].Weight = G->UdArcs[i][j]; // 记住两连通顶点关联的弧的权重}}Sort(road, G->ArcNum); // 将弧按从小到大的顺序排序//寻找最小生成树for (i = 0; i < G->ArcNum; i++) // 已是升序,每次都能去到最小的弧{int a = GetRoot(root, G->VerNum, road[i].v1); // 获取结点所在树的根int b = GetRoot(root, G->VerNum, road[i].v2);if (a != b) // 根结点不相同,则两结点不属于同一颗树。{root[a] = b;// 合并两棵树。将一颗树的根结点作为另一颗树的根(2棵树拥有同一个根时,就合二为一了)printf("%c--(%d)-->%c\n", G->Verts[road[i].v1], road[i].Weight, G->Verts[road[i].v2]);}}return OK;
}int main()
{AMGraph G;CreateUDN(&G);CreateGTree_Kruskal(&G);return 0;
}
7.4.3.3 Prim 算法和Kruskal 算法比较
算法名 | Prim 算法 | Kruskal 算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n2)(n为顶点数) | O(eloge) (e为边数) |
适应范围 | 稠密图 | 稀疏图 |
7.5 有向无环图及其应用
有向无环图: 无环的有向图,简称 DAG图(Directed Acycline Graph)
AOV网(Activity On Vertex network): 用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网。
AOE网(Activity On Edge network): 用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,弧的权表示活动持续时间,称这种有向图为边表示活动的网。
7.5.1 拓扑排序 - AOV网
定义:
在 AOV 网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若 AOV 网中有弧 <i,j>存在,则在这个序列中,i一定排在 j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
方法:
在有向图中选一个没有前驱的顶点且输出之;
从图中删除该顶点和所有以它为尾的弧。
重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止
一个AOV网的拓扑序列不是唯一的
拓扑排序的一个重要应用:
检测 AOV 网中是否存在环:
对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该 AOV 网必定不存在环。
/* algraph.h */
#ifndef __ALGRAPH_H__
#define __ALGRAPH_H__#define MVNUM 20
#define OK 1
#define ERROR 0
typedef int Status;typedef char VerTexType;
typedef int InfoType;
typedef struct ArcNode {int adjvex; //该边所指向的顶点的位置struct ArcNode* nextarc; //指向下一条边的指针InfoType info; //和边相关的信息
}ArcNode;
typedef struct VNode {VerTexType data; //顶点信息ArcNode* firstarc; // 指向第一条依附该顶点的边的指针int indegree;//入度
}VNode, AdjLst[MVNUM]; //AdjList表示邻接表类型
//AdjLst v == VNode v[MVNUM]
typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct {AdjLst vertices; //vertices=vertex的复数int vexnum, arcnum; //图当前顶点数和弧数GraphKind kind; //图的种类标志
}ALGraph;Status CreateGraph(ALGraph* G);
int LocateVex(ALGraph* G, VerTexType v);
Status CreateDG(ALGraph* G);#endif
/* algraph.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "algraph.h"
#include <stdio.h>
#include <stdlib.h>Status CreateGraph(ALGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {case DG: return CreateDG(G); //构造有向图/*case DN: return CreateDN(G); //构造有向网case UDN: return CreateUDN(G); //构造无向网case UDG: return CreateUDG(G); //构造无向图*/default:return ERROR;}
}int LocateVex(ALGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vertices[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateDG(ALGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表// 1、依次输入点的信息存入顶点表中// 2、使每个表头结点的指针域初始化为NULLint i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");int data;scanf(" %c", &data);G->vertices[i].data = data;G->vertices[i].firstarc = NULL;G->vertices[i].indegree = 0;}// 创建邻接表// 1、依次输入每条边依附的两个顶点// 2、确定两个顶点的序号i和j,建立边结点// 3、将此边结点插入到vi对应的边链表的头部for (k = 0; k < G->arcnum; k++){printf("输入一条边依附的两个顶点: \n");scanf(" %c %c", &v1, &v2);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*p1ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));if (!p1) {// 处理内存分配失败return NULL;}p1->adjvex = j;//头插法p1->nextarc = G->vertices[i].firstarc;G->vertices[i].firstarc = p1;G->vertices[j].indegree++;}for (i = 0; i < G->vexnum; i++){printf("%c -> ", G->vertices[i].data);ArcNode* p;p = G->vertices[i].firstarc;while (p){printf("%c ", G->vertices[p->adjvex].data);p = p->nextarc;}printf("\n");}for (i = 0; i < G->vexnum; i++){printf("%c 的入度为 %d ", G->vertices[i].data, G->vertices[i].indegree);printf("\n");}return OK;
}
/* stack.h */
#ifndef __STACK_H__
#define __STACK_H__#define STACK_INIT_SIZE 100 //存储空间初始分配量
#define STACKINCREMENT 10 //存储空间分配增量#define OK 1 //完成
#define OVERFLOW -1 //失败
#define ERROR -2 //错误typedef int Status;
typedef struct {int* base; // 在栈构造之前和销毁之后,base的值为NULLint* top; // 栈顶指针int stacksize; //指示栈的当前可使用的最大容量
}SqStack;Status InitStack(SqStack* S);
Status Push(SqStack* S, int e);
int Pop(SqStack* S);
Status GetTop(SqStack S, int* e);
int StackEmpty(SqStack S);
Status DestroyStack(SqStack* S);#endif
/* stack.c */
#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>
#include <stdlib.h>
#include "stack.h"Status InitStack(SqStack* S) {// 构造一个空栈SS->base = (int*)malloc(STACK_INIT_SIZE * sizeof(int));if (!S->base) exit(OVERFLOW);S->top = S->base;S->stacksize = STACK_INIT_SIZE;return OK;
}Status Push(SqStack* S, int e) {// 插入元素 e为新的栈顶元素if (S->top - S->base >= S->stacksize) { //栈满,追加存储空间S->base = (int*)realloc(S->base, (S->stacksize + STACKINCREMENT) * sizeof(int));if (!S->base) exit(OVERFLOW);S->top = S->base + S->stacksize;S->stacksize += STACKINCREMENT;}*S->top++ = e;return OK;
}int Pop(SqStack* S) {int e = 0;// 若栈不空,则删除s的栈顶元素,用e返回其值,并返回OK;否则返回 ERRORif (S->top == S->base) return ERROR;e = *--S->top;return e;
}Status GetTop(SqStack S, int* e) {// 若栈不空, 则用e返回s的栈顶元素, 并返回0K; 否则返回ERRORif (S.top == S.base) return ERROR;*e = *(S.top - 1);return OK;
}int StackEmpty(SqStack S)
{if (S.top == S.base) return 1;return 0;
}Status DestroyStack(SqStack* S) {// 销毁栈Sfree(S->base);S->base = NULL;S->top = NULL;S->stacksize = 0;return OK;
}
/* text.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include "algraph.h"
#include "stack.h"# 时间复杂度为 O(n+e)
Status TopologicalSort(ALGraph G)
{SqStack S;InitStack(&S);int i;for (i = 0; i < G.vexnum; ++i){if (G.vertices[i].indegree == 0){Push(&S, i); //入度为0的入栈【无前驱】}}int count = 0;while (!StackEmpty(S)){int i = Pop(&S);printf("%c ", G.vertices[i].data);count++;ArcNode* p;for (p = G.vertices[i].firstarc; p; p = p->nextarc){int k = p->adjvex;G.vertices[k].indegree--;if (G.vertices[k].indegree == 0){Push(&S, k);}}}if (count < G.vexnum){printf("存在环");return ERROR;}else {printf("不存在环");return OK;}
}int main()
{ALGraph G;CreateGraph(&G);TopologicalSort(G);return 0;
}
7.5.2 关键路径 - AOE网
定义:
路径长度最长的路径(路径长度 – 路径上各活动持续时间之和。)
4个描述量:
ve(vj) – 表示事件 vj的最早发生时间。
vl(vj) – 表示事件 vj 的最迟发生时间。
e(i) – 表示活动 ai 的最早开始时间。
l(i) – 表示活动 ai 的最迟开始时间。
l(i)-e(i) – 表示完成活动ai的时间余量。而【l(i) - e(i) == 0】的活动为关键活动(关键路径上的活动)
方法:
/* algraph.h */
#ifndef __ALGRAPH_H__
#define __ALGRAPH_H__#define MVNUM 20#define OK 1
#define ERROR 0
typedef int Status;typedef char VerTexType;
typedef int InfoType;
typedef struct ArcNode {int adjvex; //该边所指向的顶点的位置struct ArcNode* nextarc; //指向下一条边的指针InfoType info; //和边相关的信息
}ArcNode;
typedef struct VNode {VerTexType data; //顶点信息ArcNode* firstarc; // 指向第一条依附该顶点的边的指针int indegree;//入度
}VNode, AdjLst[MVNUM]; //AdjList表示邻接表类型
//AdjLst v == VNode v[MVNUM]
typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
typedef struct {AdjLst vertices; //vertices=vertex的复数int vexnum, arcnum; //图当前顶点数和弧数GraphKind kind; //图的种类标志
}ALGraph;Status CreateGraph(ALGraph* G);
int LocateVex(ALGraph* G, VerTexType v);
Status CreateDG(ALGraph* G);#endif
/* algraph.c */
#define _CRT_SECURE_NO_WARNINGS *1#include "algraph.h"
#include <stdio.h>
#include <stdlib.h>Status CreateGraph(ALGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {case DG: return CreateDG(G); //构造有向图/*case DN: return CreateDN(G); //构造有向网case UDN: return CreateUDN(G); //构造无向网case UDG: return CreateUDG(G); //构造无向图*/default:return ERROR;}
}int LocateVex(ALGraph* G, VerTexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vertices[i].data == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}Status CreateDG(ALGraph* G)
{int weight;// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);// 输入各点,构造表头结点表// 1、依次输入点的信息存入顶点表中// 2、使每个表头结点的指针域初始化为NULLint i, k, j;VerTexType v1, v2;for (i = 0; i < G->vexnum; i++){printf("Enter the value of vertices : \n");int data;scanf(" %c", &data);G->vertices[i].data = data;G->vertices[i].firstarc = NULL;G->vertices[i].indegree = 0;}// 创建邻接表// 1、依次输入每条边依附的两个顶点// 2、确定两个顶点的序号i和j,建立边结点// 3、将此边结点插入到vi对应的边链表的头部for (k = 0; k < G->arcnum; k++){printf("输入一条边依附的两个顶点及其权值: \n");scanf(" %c %c %d", &v1, &v2, &weight);i = LocateVex(G, v1);j = LocateVex(G, v2);//生成一个新的边结点*p1ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));if (!p1) {// 处理内存分配失败return NULL;}p1->adjvex = j;p1->info = weight;//头插法p1->nextarc = G->vertices[i].firstarc;G->vertices[i].firstarc = p1;G->vertices[j].indegree++;}for (i = 0; i < G->vexnum; i++){printf("%c -> ", G->vertices[i].data);ArcNode* p;p = G->vertices[i].firstarc;while (p){printf("%c ", G->vertices[p->adjvex].data);p = p->nextarc;}printf("\n");}for (i = 0; i < G->vexnum; i++){printf("%c 的入度为 %d ", G->vertices[i].data, G->vertices[i].indegree);printf("\n");}return OK;
}
/* stack.h */
#ifndef __STACK_H__
#define __STACK_H__#define STACK_INIT_SIZE 100 //存储空间初始分配量
#define STACKINCREMENT 10 //存储空间分配增量#define OK 1 //完成
#define OVERFLOW -1 //失败
#define ERROR -2 //错误typedef int Status;
typedef struct {int* base; // 在栈构造之前和销毁之后,base的值为NULLint* top; // 栈顶指针int stacksize; //指示栈的当前可使用的最大容量
}SqStack;Status InitStack(SqStack* S);
Status Push(SqStack* S, int e);
int Pop(SqStack* S);
Status GetTop(SqStack S, int* e);
int StackEmpty(SqStack S);
Status DestroyStack(SqStack* S);#endif
/* stack.c */
#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>
#include <stdlib.h>
#include "stack.h"Status InitStack(SqStack* S) {// 构造一个空栈SS->base = (int*)malloc(STACK_INIT_SIZE * sizeof(int));if (!S->base) exit(OVERFLOW);S->top = S->base;S->stacksize = STACK_INIT_SIZE;return OK;
}Status Push(SqStack* S, int e) {// 插入元素 e为新的栈顶元素if (S->top - S->base >= S->stacksize) { //栈满,追加存储空间S->base = (int*)realloc(S->base, (S->stacksize + STACKINCREMENT) * sizeof(int));if (!S->base) exit(OVERFLOW);S->top = S->base + S->stacksize;S->stacksize += STACKINCREMENT;}*S->top++ = e;return OK;
}int Pop(SqStack* S) {int e = 0;// 若栈不空,则删除s的栈顶元素,用e返回其值,并返回OK;否则返回 ERRORif (S->top == S->base) return ERROR;e = *--S->top;return e;
}Status GetTop(SqStack S, int* e) {// 若栈不空, 则用e返回s的栈顶元素, 并返回0K; 否则返回ERRORif (S.top == S.base) return ERROR;*e = *(S.top - 1);return OK;
}int StackEmpty(SqStack S)
{if (S.top == S.base) return 1;return 0;
}Status DestroyStack(SqStack* S) {// 销毁栈Sfree(S->base);S->base = NULL;S->top = NULL;S->stacksize = 0;return OK;
}
/* text.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include "algraph.h"
#include "stack.h"int ve[MVNUM];
int vl[MVNUM];
SqStack S;
SqStack T;Status TopologicalSort(ALGraph G)
{InitStack(&S); //零入度顶点栈InitStack(&T); //拓扑序列顶点栈int i;for (i = 0; i < G.vexnum; ++i){ve[i] = 0; //各顶点事件的最早发生时间 ve初始化if (G.vertices[i].indegree == 0){Push(&S, i); //入度为0的入栈【无前驱】}}int count = 0;while (!StackEmpty(S)){int j = Pop(&S);Push(&T, j);++count;ArcNode* p;for (p = G.vertices[j].firstarc; p; p = p->nextarc){int k = p->adjvex;G.vertices[k].indegree--;if (G.vertices[k].indegree == 0){Push(&S, k);}// j --info-- > k ve(k) = Max{ ve(j) + info, ve(k)} 计算ve值if (ve[j] + p->info > ve[k]){ve[k] = ve[j] + p->info;}}}printf("\n-----------ve-----------\n");for (i = 0; i < G.vexnum; i++){printf("%d ", ve[i]);}printf("\n-----------ve-----------\n");if (count < G.vexnum){printf("存在环\n");return ERROR;}else {printf("不存在环\n");return OK;}
}Status CriticalPath(ALGraph G) {if (!TopologicalSort(G)) return ERROR;//初始化顶点事件的最迟发生时间vlint i;for (i = 0; i < G.vexnum; ++i){vl[i] = ve[G.vexnum - 1]; }// 按拓扑逆序求各顶点的vl值while (!StackEmpty(T)){int j = Pop(&T);ArcNode* p;for (p = G.vertices[j].firstarc; p; p = p->nextarc){int k = p->adjvex;int info = p->info;if (vl[k] - info < vl[j]){vl[j] = vl[k] - info;}}}printf("\n-----------vl-----------\n");for (i = 0; i < G.vexnum; i++){printf("%d ", vl[i]);}printf("\n-----------vl-----------\n");// 求 ee, el 和关键活动int j;for (j = 0; j < G.vexnum; j++){ArcNode* p;for (p = G.vertices[j].firstarc; p; p = p->nextarc){int k = p->adjvex;int info = p->info;int e = ve[j];int l = vl[k] - info;char tag = (e == l) ? 'Y' : 'N';printf("活动 %c --(%d)--> %c 活动最早发生时间:%d,活动最迟发生时间:%d,是否关键活动:%c \n", G.vertices[j].data, info, G.vertices[k].data, e, l, tag);}}}int main()
{ALGraph G;CreateGraph(&G);CriticalPath(G);DestroyStack(&T);DestroyStack(&S);return 0;
}
7.6 最短路径
在有向网中 A 点(源点)到达飞点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。
7.6.1 从某个源点(单源)到其余各顶点的最短路径 -Dijkstra算法
算法:- 时间复杂度O(n2)
1、把 V分成两组:
(1) S:已求出最短路径的顶点的集合。
(2) T=V-S:尚未确定最短路径的顶点集合
2、将T中顶点按最短路径递增的次序加入到S中
保证:
(1)从源点 v0到S中各顶点的最短路径长度都不大于从v0到 T中任何顶点的最短路径长度。
(2)每个顶点对应一个距离值:
S 中顶点: 从v0到此顶点的最短路径长度。
T 中顶点: 从v0到此顶点的只包括S中顶点作中间顶点的最短路径长度。
/* mgraph.h */
#ifndef __MGRAPH_H__
#define __MGRAPH_H__#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;Status CreateGraph(MGraph* G);
Status CreateDN(MGraph* G);
int LocateVex(MGraph* G, VertexType v);#endif
/* mgragh.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include "mgragh.h"Status CreateGraph(MGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图*/case DN: return CreateDN(G); //构造有向网/*case UDG: return CreateUDG(G); //构造无向图case UDN: return CreateUDN(G); //构造无向网*/default:return ERROR;}
}Status CreateDN(MGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = INFINITY;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2,weight : \n");scanf(" %c %c %d", &v1, &v2, &w); // 输入一条边依附的顶点及权值// 确定 v1 和 v2 在 G 中位置i = LocateVex(G, v1);j = LocateVex(G, v2);G->arcs[i][j] = w; // 弧<v1, v2> 的权值 }// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {if (G->arcs[i][j] == INFINITY){printf("∞ ");continue;}printf("%d ", G->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}
/* test.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include "mgragh.h"//用Dijkstra算法求有向网G的vO顶点到其余顶点v的最短路径P[v]及其带权长度D[v]
void ShortestPath_DIJ(MGraph* G, int v0)
{int P[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //最短路径 // 若P[v][w]代表 w是从v0到v当前求得最短路径上的顶点。// 例如 v0->v3 需要经过v0,v2,v3点(v=v3 ; w ={v0,v2,v3}),则P[v0][v0],P[v0][v2],P[v0][v3]需要被赋值int D[MAX_VERTEX_NUM]; //带权长度int final[MAX_VERTEX_NUM]; //相当于集合Sint v;for (v = 0; v < G->vexnum; v++){final[v] = 0; // 初始化S集合D[v] = G->arcs[v0][v]; // 初始化带权长度为v0节点的邻接矩阵值int w;for (w = 0; w < G->vexnum; w++){P[v][w] = 0; // 初始化最短路径都为空}if (D[v] < INFINITY){// V0 到 v 经过的点有 v0 和 v // 如果不是无穷则可以直达,如果是无穷则v0不能直达到vP[v][v0] = G->arcs[v0][v];P[v][v] = G->arcs[v0][v];}}D[v0] = 0; // 从v0开始,v0的权值为0final[v0] = 1; //并将v0放入集合S中// 开始主循环,每次求得vO到某个v顶点的最短路径,并加v到S集int i;for (i = 0; i < G->vexnum; i++) //其余G.vexnum-1个顶点{if (v0 == i){continue;}int min = INFINITY; // 当前所知离vO顶点的最近距离int w;for (w = 0; w < G->vexnum; w++) //遍历获取最小权值及其对应的结束节点{if (!final[w]) // w顶点在V-S集合中(不在S中){if (D[w] < min) // w顶点离vO顶点更近{v = w;min = D[w];}}}final[v] = 1; // 离vO顶点最近的v加入S集//更新当前最短路径及距离for (w = 0; w < G->vexnum; w++){//w∈V-S, 如果G->arcs[v][w] == INFINITY,min + G->arcs[v][w]就超出范围结果为负数if (!final[w] && G->arcs[v][w] != INFINITY && (min + G->arcs[v][w] < D[w])) {D[w] = min + G->arcs[v][w];int k;//说明v0经过v可以到w 权值为D[w] 例如:v0经过v2可以到v3 权值为13for (k = 0; k < G->vexnum; k++){//v0 到 w = v3 必须先走到 v = v2 ,所以先把v0到v2的最短路径P[v2] 赋值给v3 P[v3]P[w][k] = P[v][k];}for (k = 0; k < G->vexnum; k++){if (P[w][k] != 0){P[w][k] = D[w];} }// 修改v0到v3必须要经过v3点,P[v3][v3]需要被赋值P[w][w] = D[w];}}}int j;for (i = 0; i < G->vexnum; i++){if (v0 == i) continue;printf("点%c - 点%c 最短路径为:", G->vexs[v0], G->vexs[i]);int weight = 0;for (j = 0; j < G->vexnum; j++){if (P[i][j]){printf("%c ", G->vexs[j]);weight = P[i][j];}if (j == G->vexnum - 1){printf(" 权值为:%d\n", weight);}}}
}int main()
{MGraph G;CreateGraph(&G);ShortestPath_DIJ(&G, 0);return 0;
}
7.6.2 每一对顶点之间(所有顶点)的最短路径 - Floyd算法
解决这个问题的一个办法是:每次以一个顶点为源点,重复执行Dijkstra算法 n 次。- 时间复杂度O(n3)
另一个办法:Floyd算法。- 时间复杂度O(n3)
/* mgraph.h */
#ifndef __MGRAPH_H__
#define __MGRAPH_H__#define INFINITY INT_MAX
#define MAX_VERTEX_NUM 20 //最大顶点个数#define OK 1
#define ERROR 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */typedef enum { DG, DN, UDG, UDN } GraphKind; // {有向图,有向网,无向图,无向网}
//DG代表有向图(Directed Graph),DN代表有向网(Directed Network),UDG代表无向图(Undirected Graph),UDN代表无向网(Undirected Network)。有向图和有向网的区别在于,有向网的边是有权重的typedef int VRType; // 假设边的权重为整数类型
typedef char VertexType; // 假设顶点用字符类型表示typedef struct {VertexType vexs[MAX_VERTEX_NUM]; //顶点向量,即用来存储图中的顶点VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,即图中所有边的信息int vexnum, arcnum; //图的当前顶点数和弧数GraphKind kind; // 图的种类标志
}MGraph;Status CreateGraph(MGraph* G);
Status CreateDN(MGraph* G);
int LocateVex(MGraph* G, VertexType v);#endif
/* mgraph.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include "mgragh.h"Status CreateGraph(MGraph* G)
{char kindStr[10]; // 假设输入的字符串不会超过9个字符printf("Enter the type of graph (DG, DN, UDG, UDN): ");scanf("%s", kindStr);// 将输入的字符串转换为枚举类型if (strcmp(kindStr, "DG") == 0) {G->kind = DG;}else if (strcmp(kindStr, "DN") == 0) {G->kind = DN;}else if (strcmp(kindStr, "UDG") == 0) {G->kind = UDG;}else if (strcmp(kindStr, "UDN") == 0) {G->kind = UDN;}else {printf("Invalid graph type.\n");return ERROR;}switch (G->kind) {/*case DG: return CreateDG(G); //构造有向图*/case DN: return CreateDN(G); //构造有向网/*case UDG: return CreateUDG(G); //构造无向图case UDN: return CreateUDN(G); //构造无向网*/default:return ERROR;}
}Status CreateDN(MGraph* G)
{// 输入总顶点数和边数printf("Enter the vexnum and arcnum of graph : \n");scanf("%d %d", &G->vexnum, &G->arcnum);int i = 0, j = 0, k = 0;VertexType v1, v2;VRType w;// 构造顶点向量for (i = 0; i < G->vexnum; ++i) {printf("Enter the vexs of graph : \n");scanf(" %c", &G->vexs[i]); // 注意:在%c前面加一个空格,用于跳过空白字符}// 初始化邻接矩阵,使每个权值初始化为极大值for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {G->arcs[i][j] = INFINITY;}}// 构造邻接矩阵for (k = 0; k < G->arcnum; ++k) {printf("Enter v1,v2,weight : \n");scanf(" %c %c %d", &v1, &v2, &w); // 输入一条边依附的顶点及权值// 确定 v1 和 v2 在 G 中位置i = LocateVex(G, v1);j = LocateVex(G, v2);G->arcs[i][j] = w; // 弧<v1, v2> 的权值 }// 打印邻接矩阵for (i = 0; i < G->vexnum; ++i) {for (j = 0; j < G->vexnum; ++j) {if (G->arcs[i][j] == INFINITY){printf("∞ ");continue;}printf("%d ", G->arcs[i][j]);}printf("\n");}return OK;
}int LocateVex(MGraph* G, VertexType v) {// 遍历顶点数组,查找顶点vfor (int i = 0; i < G->vexnum; ++i) {if (G->vexs[i] == v) {return i; // 如果找到,返回顶点索引}}return -1; // 如果没有找到,返回-1
}
/* test.c */
#define _CRT_SECURE_NO_WARNINGS *1#include <stdio.h>
#include <stdlib.h>
#include "mgragh.h"//Floyd算法求有向网G中各对顶点v和w之间的最短路径P[v][w]及其带权长度D[v][w]
void ShortestPath_FLOYD(MGraph* G)
{// P[v][w][u] 从v到w最短路径上需要经过节点uint P[MAX_VERTEX_NUM][MAX_VERTEX_NUM][MAX_VERTEX_NUM];int D[MAX_VERTEX_NUM][MAX_VERTEX_NUM];int v, w, u;// 初始化for (v = 0; v < G->vexnum; v++){for (w = 0; w < G->vexnum; w++){D[v][w] = G->arcs[v][w]; // 初始化权值带权长度Dfor (u = 0; u < G->vexnum; u++){P[v][w][u] = 0; // 初始化最短路径P}if (D[v][w] < INFINITY){P[v][w][v] = 1;P[v][w][w] = 1;}}}// 计算最短路径for (u = 0; u < G->vexnum; u++) {for (v = 0; v < G->vexnum; v++){for (w = 0; w < G->vexnum; w++) {//说明从v->u->w比v->w更短if (D[v][u] != INFINITY && D[u][w] != INFINITY && D[v][u] + D[u][w] < D[v][w]){D[v][w] = D[v][u] + D[u][w];int i;for (i = 0; i < G->vexnum; i++){P[v][w][i] = P[v][u][i] || P[u][w][i];} }if (v == w){D[v][w] = 0;}}}}for (v = 0; v < G->vexnum; v++){for (w = 0; w < G->vexnum; w++){if (!D[v][w]) continue;printf("从%c到%c最短路径:", G->vexs[v], G->vexs[w]);int weight = 0;for (u = 0; u < G->vexnum; u++){if (P[v][w][u]){printf("%c ", G->vexs[u]);}}printf(" 权值为:%d\n", D[v][w]);}}
}int main()
{MGraph G;CreateGraph(&G);ShortestPath_FLOYD(&G, 0);return 0;
}
参考:
教材:
严蔚敏《数据结构》(C语言版).pdf
博客:
【数据结构——图和图的存储结构】
Tarjan算法与无向图连通性
60 分钟搞定图论中的 Tarjan 算法(一)
【推荐】轻松掌握tarjan强连通分量
最小生成树 —— Kruskal 克鲁斯卡尔算法
代码:
李煜东的《算法竞赛进阶指南》
Prim算法
视频:
数据结构与算法基础(青岛大学-王卓)
Tarjan系列