【学习笔记】数据结构(七)

文章目录

    • 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=1nTD(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过程中,通过非树边能够回溯到的已经被访问的且是最早的节点。对于某个顶点ulow[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)

  1. 强连通
    若图中有两个点u和v, 他们能互相到达, 则称他们强连通
  2. 强连通图
    若是G中任意2个点都可以互相到达, 则称G是一个强连通图
  3. 强连通分量
    有向非强连通图的极大强连通子图(可以有很多个)
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算法

核心思想:

  1. 对原图进行DFS:按照DFS完成的顺序将顶点压入栈中。
  2. 构建逆图:反转所有边的方向。
  3. 对逆图进行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系列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/60415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[编译报错]ImportError: No module named _sqlite3解决办法

1. 问题描述&#xff1a; 在使用python进行代码编译时&#xff0c;提示下面报错&#xff1a; "/home/bspuser/BaseTools/Source/Python/Workspace/WorkspaceDatabase.py", line 18, in <module>import sqlite3File "/usr/local/lib/python2.7/sqlite3/_…

社会信用示范城市信用代码和虚拟变量(1990-2022年)原始数据、计算代码、参考文献和最终计算结果

社会信用示范城市信用代码和虚拟变量可以提供一个以观察和分析城市信用状况的演变。 可以构建一个多维度的城市信用评估模型。这个模型不仅能够反映城市信用状况的历史演变&#xff0c;还能预测未来趋势&#xff0c;为政策制定提供科学依据。 1990-2022年社会信用示范城市信用…

【OH】openHarmony开发环境搭建(基于windows子系统WSL)

前言 本文主要介绍基于windows子系统WSL搭建openHarmony开发环境。 WSL与Vmware虚拟机的区别&#xff0c;可以查看WSL与虚拟机的区别 更详细的安装配置过程可参考微软官网&#xff1a; ​安装 WSL 前提 以下基于windows 111专业版进行配置&#xff0c;windows 10应该也是可以…

机器学习: LightGBM模型(优化版)——高效且强大的树形模型

LightGBM&#xff08;Light Gradient Boosting Machine&#xff09;是一种基于梯度提升决策树&#xff08;GBDT&#xff09;的框架&#xff0c;由微软提出。它具有高效的训练速度、低内存占用、支持并行和GPU加速等特点&#xff0c;非常适合大规模数据的训练任务&#xff0c;尤…

游戏引擎学习第八天

视频参考: https://www.bilibili.com/video/BV1ouUPYAErK/ 理解下面的代码 关于虚函数 代码分解 结构体 foo 的定义&#xff1a; struct foo {int32 X;int64 Y;virtual void Bar(int c); };foo 结构体有两个成员变量&#xff1a;X&#xff08;int32 类型&#xff09;和 Y&…

Xcode 16 使用 pod 命令报错解决方案

原文请点击这个跳转 一、问题现象&#xff1a; 有人会遇到 Xcode 升级到 16 后&#xff0c;新建应用然后使用 pod init 命令会报错如下&#xff1a; Stack Ruby : ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]RubyGems : 3.5.22Host : macOS 15.0 (24A335…

概率论之正态分布密度函数与matlab

文章目录 0.浅谈我的想法1.正态分布引入1.1公式和对应概率1.2模拟生成数据1.3图像绘制1.4图像的调整1.5概率密度函数1.6两个方式的对比分析1.7分布函数1.8分位数效果展示 0.浅谈我的想法 众所周知&#xff0c;在这个数学建模的这个过程之中会遇到很多的这个概率论的相关的问题…

【maven踩坑】一个坑 junit报错 但真正导致这个的不是junit的原因

目录 事件起因环境和工具操作过程解决办法结束语 事件起因 报错一&#xff1a; Internal Error occurred. org.junit.platform.commons.JUnitException: TestEngine with ID junit-vintage failed to discover tests报错二&#xff1a; Internal Error occurred. org.junit.pl…

【算法】——二分查找合集

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 零&#xff1a;二分查找工具 1&#xff1a;最基础模版 2&#xff1a;mid落点问题 一&#xff1a;最…

caozha-CEPCS(新冠肺炎疫情防控系统)

caozha-CEPCS&#xff0c;是一个基于PHP开发的新冠肺炎疫情防控系统&#xff0c;CEPCS&#xff08;全称&#xff1a;COVID-19 Epidemic Prevention and Control System&#xff09;&#xff0c;可以应用于单位、企业、学校、工业园区、村落等等。小小系统&#xff0c;希望能为大…

AI技术赋能电商行业:创新应用与未来展望

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《热点时事》 期待您的关注 引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术正逐步渗透到各行各业&a…

若依项目-结构解读

项目结构 admin模块 common模块 framework模块 service模块 配置 依赖关系 前端接口 src 表结构

音视频入门基础:MPEG2-TS专题(3)——TS Header简介

注&#xff1a;本文有部分内容引用了维基百科&#xff1a;https://zh.wikipedia.org/wiki/MPEG2-TS 一、引言 本文对MPEG2-TS格式的TS Header进行简介。 进行简介之前&#xff0c;请各位先下载MPEG2-TS的官方文档。ITU-T和ISO/IEC都分别提供MPEG2-TS的官方文档。但是ITU提供的…

RN开发遇到的坑

1 、 RN 启动崩溃 https://blog.csdn.net/qq_31915745/article/details/108125671 2、修改报红⻚ https://blog.csdn.net/weixin_43969056/article/details/104757926 3 、编译不过去提示 glog-0.3.5 有问题&#xff0c; 找到 / 项⽬ /node_modules/react-native/scripts/ io…

折叠手机失败了,有手机品牌宣布退出,苹果成为赢家

日前一家手机企业宣布停止研发大折叠手机&#xff0c;这对于国产手机来说显然是一大打击&#xff0c;他们都希望以折叠手机抢占高端手机市场&#xff0c;然而残酷的市场现实却是消费者始终难以接受折叠手机&#xff0c;导致折叠手机的销量规模始终难以扩大。 折叠手机最早由三星…

解决Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题

Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题 项目问题问题描述解决方案具体实现 项目问题 在 Jenkins 中使用 Git 参数插件 进行参数化构建&#xff0c;具有多方面的重要性和好处。这不仅提高了构建的灵活性和透明度&#xff0c;还能大大提升开发和运维效率。以下是使用…

Python数据分析NumPy和pandas(二十七、数据可视化 matplotlib API 入门)

数据可视化或者数据绘图是数据分析中最重要的任务之一&#xff0c;是数据探索过程的一部分&#xff0c;数据可视化可以帮助我们识别异常值、识别出需要的数据转换以及为模型生成提供思考依据。对于Web开发人员&#xff0c;构建基于Web的数据可视化显示也是一种重要的方式。Pyth…

sqli—labs靶场 5-8关 (每日4关练习)持续更新!!!

Less-5 上来先进行查看是否有注入点&#xff0c;判断闭合方式&#xff0c;查询数据列数&#xff0c;用union联合注入查看回显位&#xff0c;发现到这一步的时候&#xff0c;和前四道题不太一样了&#xff0c;竟然没有回显位&#xff1f;&#xff1f;&#xff1f; 我们看一下源…

从建立TRUST到实现FAIR:可持续海洋经济的数据管理

1. 引言 随着我们对信息管理方式的信任&#xff0c;我们的社会对数字化数据的以来呈指数级增长。为了跟上大数据的需求&#xff0c;通过不断的努力和持续实践&#xff0c;对“good”数据管理方式的共识也在不断发展和演变。 加拿大正在建设国家基础设施和服务以及研究数据管理…

基于GPS/GIS数据融合与预处理技术的工厂导航系统(三)

在智能制造与智慧物流的背景下&#xff0c;厂区导航系统的高效性与准确性至关重要。然而&#xff0c;GPS信号易受环境干扰&#xff0c;GIS数据则可能因来源多样而存在误差。本文旨在通过详细剖析GPS数据的获取、解析与误差校正&#xff0c;GIS数据的导入、图层管理与空间数据清…