图论(从数据结构的三要素出发)

news2024/11/17 2:39:55

文章目录

  • 逻辑结构
  • 物理结构
    • 邻接矩阵
      • 定义
      • 性能分析
      • 性质
      • 存在的问题
    • 邻接表
      • 定义
      • 性能分析
      • 存在的问题
    • 十字链表(有向图)
      • 定义
      • 性能分析
    • 邻接多重表(无向图)
      • 定义
      • 性能分析
  • 数据的操作
    • 图的基本操作
    • 图的遍历
      • 广度优先遍历(BFS)
        • 算法思想和实现
        • 性能分析
        • 深度优先最小生成树
      • 深度优先遍历(DFS)
        • 算法思想和实现
        • 性能分析
        • 深度优先的生成树和生成森林
  • 数据结构的应用
    • 最小生成树
      • 问题描述
      • Prim算法(结点)
      • Kruskal算法(边)
    • 最短路径
      • BFS算法(不带权)
      • Dijkstra算法(只能是正权图)
      • Bellman Ford算法(可以是负权图)
      • SPFA算法(可以是负权图)
      • Floyd算法(点与点之间的最短路径)
    • 有向无环图(DAG)
    • 拓扑排序
    • 逆拓扑排序
    • 关键路径

逻辑结构

以下图片来源于王道的数据结构

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

物理结构

邻接矩阵

定义

顶点数为 n n n的图 G = ( V , E ) G=(V,E) G=(V,E)的邻接矩阵 A A A n x n nxn nxn的,将 G G G的顶点编号为 v 1 , v 2 , ⋯ , v n v_1,v_2,⋯,v_n v1,v2,,vn,则
A [ i ] [ j ] = { 1 , ( v i , v j )  或  ⟨ v i , v j ⟩  是  E ( G )  中的边  0 , ( v i , v j )  或  ⟨ v i , v j ⟩  不是  E ( G )  中的边  A[i][j]= \begin{cases}1, & \left(v_i, v_j\right) \text { 或 }\left\langle v_i, v_j\right\rangle \text { 是 } E(G) \text { 中的边 } \\ 0, & \left(v_i, v_j\right) \text { 或 }\left\langle v_i, v_j\right\rangle \text { 不是 } E(G) \text { 中的边 }\end{cases} A[i][j]={1,0,(vi,vj)  vi,vj  E(G) 中的边 (vi,vj)  vi,vj 不是 E(G) 中的边 

对带权图而言,若顶点 v i v_i vi v j v_j vj,之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点 V i V_i Vi V j V_j Vj不相连,则通常用0或 ∞ ∞ 来代表这两个顶点之间不存在边:
A [ i ] [ j ] = { w i j , ( v i , v j )  或  ⟨ v i , v j ⟩  是  E ( G )  中的边  0  或  ∞ , ( v i , v j )  或  ⟨ v i , v j ⟩  不是  E ( G )  中的边  A[i][j]= \begin{cases}w_{i j}, & \left(v_i, v_j\right) \text { 或 }\left\langle v_i, v_j\right\rangle \text { 是 } E(G) \text { 中的边 } \\ 0 \text { 或 } \infty, & \left(v_i, v_j\right) \text { 或 }\left\langle v_i, v_j\right\rangle \text { 不是 } E(G) \text { 中的边 }\end{cases} A[i][j]={wij,0  ,(vi,vj)  vi,vj  E(G) 中的边 (vi,vj)  vi,vj 不是 E(G) 中的边 

在这里插入图片描述

typedef char VertexType;							// 顶点数据类型
typedef int EdgeType;								// 边数据类型

typedef struct {
		VertexType vex[MaxVertexNum];				// 顶点集
		EdgeType edge[MaxVertexNum][MaxVertexNum];	// 边集
		int vexnum, arcnum;							// 当前顶点数和边数
}MGraph;

性能分析

  • 空间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),只和顶点数相关,和实际的边数无关。
  • 适合用于存储稠密图。
  • 无向图的邻接矩阵是对称矩阵,可以压缩存储,只需要 n ( n + 1 ) 2 − 1 \frac{n(n+1)}{2}-1 2n(n+1)1个存储空间。

性质

在这里插入图片描述

存在的问题

存储空间极大的浪费了,且删除顶点和边的时间复杂度高。

邻接表

定义

G G G中的每个顶点 v i v_i vi建立一个单链表,第 i i i个单链表中的结点表示依附于顶点 v i v_i vi的边(对于有向图则是以顶点 v i v_i vi为尾的弧),这个单链表就称为顶点v_i$的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储,称为顶点表,所以在邻接表中存在两种结点:顶点表结点边表结点。(类似于树的孩子表示法)
在这里插入图片描述

typedef struct ArcNode {			// 边表结点
	int adjvex;						// 该弧所指向的顶点的位置
	struct ArcNode *nextarc;		// 指向下一条弧的指针
	InfoType info;					// 权值
} ArcNode;

typedef struct VNode {				// 顶点表结点
	VertexType data;				// 顶点信息
	ArcNode *firstarc;				// 指向第一条依附该顶点的弧的指针
} VNode, AdjList[MaxVertexNum];

typedef struct {					
	AdjList vertices;				// 邻接表
	int vernum, arcnum;				// 图的顶点数和弧数
} ALGraph;

性能分析

  • 空间复杂度:有向图 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(V+2∣E),无向图 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 适合用于存储稀疏图。

在这里插入图片描述

存在的问题

对于无向图而言,需要存储两份边,产生冗余数据,在计算入度和入边时间复杂度高。

十字链表(有向图)

定义

为了解决邻接表法中计算入度入边时间复杂度高的问题,我们引入了十字链表法存储有向图

在这里插入图片描述

typedef struct ArcNode {                // 边表结点
    int tailvex;                        // 该弧的起始顶点的位置
    int headvex;                        // 该弧的终止顶点的位置
    struct ArcNode *hlink;              // 指向下一条终止于同一顶点的弧的指针
    struct ArcNode *tlink;              // 指向下一条起始于同一顶点的弧的指针
    InfoType info;                      // 权值信息
} ArcNode;

typedef struct VNode {                  // 顶点表结点
    VertexType data;                    // 顶点信息
    ArcNode *firstin;                   // 指向第一条入弧的指针
    ArcNode *firstout;                  // 指向第一条出弧的指针
} VNode, OLAdjList[MaxVertexNum];

typedef struct {
    OLAdjList vertices;                 // 十字链表的顶点表
    int vernum, arcnum;                 // 图的顶点数和弧数
} OLGraph;

性能分析

  • 空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 只能存储有向图

邻接多重表(无向图)

定义

为了解决

  • 邻接表法中存储无向图需要保存两份边会产生数据冗余的问题
  • 邻接矩阵中删除边和结点复杂度高的问题

我们引入了邻接多重表存储无向图

在这里插入图片描述

typedef struct ENode {                    // 边表结点
    int ivex, jvex;                       // 该边依附的两个顶点的位置
    struct ENode *ilink, *jlink;          // 分别指向依附于顶点ivex和jvex的下一条边
    InfoType info;                        // 边的信息(如权值)
} ENode;

typedef struct VNode {                    // 顶点表结点
    VertexType data;                      // 顶点信息
    ENode *firstedge;                     // 指向第一条依附该顶点的边的指针
} VNode;

typedef struct {
    VNode adjmulist[MaxVertexNum];        // 顶点表数组
    int vernum, edgenum;                  // 图的顶点数和边数
} AMLGraph;

性能分析

  • 空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 只能存储有向图
  • 删除边、删除节点等操作很方便

在这里插入图片描述

数据的操作

图的基本操作

判断图G是否存在边<x, y>或(x, y)

在这里插入图片描述

Neighbors(G,x):列出图G中与结点x邻接的边

在这里插入图片描述
在这里插入图片描述

InsertVertex(G,x):在图G中插入顶点x

在这里插入图片描述

DeleteVertex(G,x):从图G中删除顶点x

在这里插入图片描述
在这里插入图片描述

AddEdge(G,x,y):若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边。

在这里插入图片描述

RemoveEdge(G,x,y):若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边。

在这里插入图片描述

FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。

在这里插入图片描述

NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。

在这里插入图片描述

图的遍历

广度优先遍历(BFS)

算法思想和实现

在这里插入图片描述

bool visited[Max_Vertex_Num];			// 初始全为false

void BFSTraverse(Graph g)					
{
	for (int i = 0; i < g.vexnum; i ++ )
		visited[i] = false;

	for (int i = 0; i < g.vexnum; i ++ )			// 针对非连通图
		if (!visited[i])
			BFS(g, i);
}

void BFS(Graph g, int v)
{
	Queue q, InitQueue(q);
	Enqueue(q, v);
	visit(v), visited[v] = true;

	while (!isEmpty(q))
	{
		DeQueue(q, v);

		for (int w = FirstNeighbor(g, v); w != -1; w = NextNeighbor(g, v, w))
			if (!visit[w])
			{
				visit(w), visited[w] = true;
				EnQueue(q, w);
			}
	}
}

结论:

  • 对于无向图,BFS调用的次数=连通分量数。
  • 对于有向图,BFS调用一次不能访问所有结点,可以得出该子图是非强连通分量。
性能分析

在这里插入图片描述

深度优先最小生成树

在广度遍历的过程中,我们可以得到一棵遍历树,称为广度优先生成树。同一个图的邻接矩阵存储表示是唯一的,所以其广度优先生成树也是唯一的,但因为邻接表存储表示不唯一,所以其广度优先生成树也是不唯一的。

在这里插入图片描述

深度优先遍历(DFS)

算法思想和实现

在这里插入图片描述

bool visited[Max_Vertex_Num];			// 初始全为false

void DFSTraverse(Graph g)					
{
	for (int i = 0; i < g.vexnum; i ++ )
		visited[i] = false;

	for (int i = 0; i < g.vexnum; i ++ )			// 针对非连通图
		if (!visited[i])
			DFS(g, i);
}

void DFS(Graph g, int v)
{
	visit(v);
	visited[v] = true;

	for (int w = FirstNeighbor(g, v); w != -1; w = NextNeighbor(g, v, w))
		if (!visit[w])
			DFS(g, w);
}

结论:

  • 对于无向图,DFS调用的次数=连通分量数。
  • 对于有向图,DFS调用一次不能访问所有结点,可以得出该子图是非强连通分量。
性能分析

在这里插入图片描述

深度优先的生成树和生成森林

与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的,即对连通图调用 DFS才能产生深度优先生成树,否则产生的将是深度优先生成森林,与 BFS类似,基于邻接表存储的深度优先生成树是不唯一的。

在这里插入图片描述

数据结构的应用

最小生成树

问题描述

对于⼀个带权连通无向图 G = ( V , E ) G = (V, E) G=(V,E),⽣成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设 R R R G G G的所有⽣成树的集合,若 T T T R R R中边的权值之和最小的生成树,则 T T T称为 G G G的最小生成树。

  • 如果⼀个连通图本身就是⼀棵树,则其最小生成树就是它本身。
  • 最小生成树可能有多个,但边的权值之和总是唯⼀且最小的。
  • 最小生成树的边数 = 顶点数 - 1。砍掉⼀条则不连通,增加⼀条边则会出现回路。
  • 只有连通图才有生成树,非连通图只有生成森林。

在这里插入图片描述

Prim算法(结点)

从某⼀个顶点开始构建⽣成树;每次将代价最小的新顶点纳⼊生成树,直到所有顶点都纳入为止。

在这里插入图片描述

int dist[MaxVertexNum];						// 结点距离目标生成树的最小距离
bool st[MaxVertexNum];						// 结点是否已经被加入到目标生成树中
int INF = 0x3f3f3f3f;						// 设定最大值

int prim(MGraph g)
{
	memset(dist, 0x3f, sizeof dist);		// 初始化n个互补相连的结点
	
	int res = 0;							// 计算最小生成树的权值
	
	for (int i = 0; i < g.vexnum; i ++ )
	{
		/*寻找距离目标生成树最小距离*/
		int t = -1;
		for (int j = 0; j < g.vexnum; j ++ )
			if (t != -1 || dist[t] > dist[j])
				t = j;

		if (i && dist[t] == INF) return INF;// 证明原图是一个非连通图

		if (i) res += dist[t];				// 第一次当然不用计算权值
		st[t] = true;						// 将该结点加入到目标生成树中去

		/*用已加入目标生成树的结点去更新其他待加入目标生成树结点的距离*/
		for (int j = 0; j < g.vexnum; j ++ )
			dist[j] = min(dist[j], g.edge[t][j]);
	}
	return res;
}

时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),适用于稠密图,用邻接矩阵的方式存储图。(可以用最小堆的方式改进Prim算法中的找距离目标生成树最小距离的结点)

Kruskal算法(边)

每次选择一条权值最小的,使这条的两头连通(原本已经连通的就不选)直到所有结点都连通。

在这里插入图片描述

int p[MaxVertexNum];					// 并查集

struct Edge
{
    int a, b;							// 边(a,b)
    int w;								// 权值
} edges[MaxArcNum];

int find(int x)							// 路径压缩的并查集
{
	if (p[x] != -1) p[x] = find(p[x]);
	return p[x];
}

int kruskal(ALGraph g)
{
	quickSort(edges);					// 快速排序,时间复杂度O(nlogn)

	memset(p, -1, sizeof p);			// 初始化并查集

	int res = 0, cnt = 0;				// 记录最小生成树的权值和记录当前最小生成树的边的个数

	for (int i = 0; i < g.arcnum; i ++ )
	{
		int a = edges[i].a, b = edges[i].b, w = edges[i].w;
		
		a = find(a), b = find(b);		// 查找结点a和b是否在同一棵目标生成子树中
		if (a != b)						// 若不在
		{
			p[a] = b;					// 将两个子树合并成一棵生成子树
            res += w;					// 更新最小生成树的权值
            cnt ++ ;					// 更新当前生成树的边个数
		}
	}
	if (cnt < n - 1) return INF;		// 边数小于结点数-1一定是非连通图
    return res;
}

时间复杂度: O ( ∣ E ∣ l o g 2 ∣ E ∣ ) O(|E|log_2|E|) O(Elog2E),(主要是快速排序的时间复杂度: O ( ∣ E ∣ l o g 2 ∣ E ∣ ) O(|E|log_2|E|) O(Elog2E)+遍历所有边 O ( ∣ E ∣ ) O(|E|) O(E) × \times ×并查集Find操作的时间复杂度 O ( α ( ∣ E ∣ ≤ 4 ) → O ( 1 ) O(\alpha(|E|\leq4)\rightarrow O(1) O(α(E4)O(1)故总的时间复杂度是 O ( ∣ E ∣ l o g 2 ∣ E ∣ + ∣ E ∣ × α ( ∣ E ∣ ) O(|E|log_2|E|+|E|\times\alpha(|E|) O(Elog2E+E×α(E))适用于稀疏图,用邻接表的方式存储图。

最短路径

BFS算法(不带权)

若图 G = ( V , E ) G=(V,E) G=(V,E)非带权图,定义从顶点 u u u到顶点 v v v的最短路径 d ( u , v ) d(u,v) d(u,v)为从 u u u v v v的任何路径中最少的边数;若从 u u u v v v没有通路,则 d ( u , v ) = ∞ d(u,v)=∞ d(u,v)=

因为BFS算法是逐层遍历的,所以最先被访问的结点一定距离最短。

在这里插入图片描述

bool visited[Max_Vertex_Num];			// 初始全为false
int d[Max_Vertex_Num];					
int path[Max_Vertex_Num];
int INF = 0x3f3f3f3f;

void BFS_MIN_Distance(Graph g, int u)
{
	for (int i = 0; i < g.vernum; i ++ )
	{
		d[i] = INF;						// 初始化路径长度
		path[i] = -1;					// 最短路径从哪个顶点过来
	}

	d[u] = 0;
	
	Queue q, InitQueue(q);
	Enqueue(q, u);
	visit(u), visited[u] = true;

	while (!isEmpty(q))
	{
		DeQueue(q, u);

		for (int w = FirstNeighbor(g, u); w != -1; w = NextNeighbor(g, u, w))
			if (!visit[w])
			{
				d[w] = d[u] + 1;
				path[w] = u;
				visit(w), visited[w] = true;
				EnQueue(q, w);
			}
	}
}

Dijkstra算法(只能是正权图)

Dijkstra 算法设置一个集合 S S S记录已求得的最短路径的顶点,初始时把源点 v 0 v_0 v0放入 S S S,集合
S S S每并入一个新顶点 v i v_i vi,都要修改源点 v 0 v_0 v0到集合 V − S V-S VS中顶点当前的最短路径长度值。

带权路径⻓度——当图是带权图时,⼀条路径上所有边的权值之和,称为该路径的带权路径⻓度。

在这里插入图片描述

int dist[MaxVertexNum];
bool st[MaxVertexNum];
int path[MaxVertexNum];

void dijkstra(Graph g, int u)
{
	memset(dist, 0x3f, sizeof dist);
    dist[u] = 0;
    /*遍历n边,每一遍都会更新一个节点到1节点的最小值,由于是正权图,故局部最优,就为全局最优*/
    for (int i = 0; i < g.vernum; i ++ ) 
    {
        /*寻找到距离u最短的点*/
        int t = -1;
        for (int j = 0; j < g.vernum; j ++ ) 
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        st[t] = true;

        /*用t节点更新其余未更新的点(邻接矩阵版本)*/
        for (int j = 0; j < g.vernum; j ++ )
            if (!st[j])
                dist[j] = min(dist[j], dist[t] + g.edge[t][j]), path[j] = t;

		/*用t节点更新其余未更新的点(邻接表版本)*/
		ArcNode *p = g.vertices[t];
        while (p)
        {
        	if (!st[p->adjvex])
            {
            	dist[p->adjvex] = min(dist[p->adjvex], dist[t] + p->info);
                path[p->adjvex] = t;
            }
            p = p->nextarc;
        }
    }
}

时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

在这里插入图片描述

Bellman Ford算法(可以是负权图)

为了解决Dijkstra算法不能处理负权图,我们引入Bellman - ford 算法。
Bellman-Ford算法的设计理念基于图的性质,特别是路径上的边数的限制。以下是对为什么Bellman-Ford算法在进行 (V-1) 次松弛操作后就能确定最短路径的详细解释。

对于一个包含 (V) 个顶点的有向图,最短路径的一个重要性质是:从源点到任何其他顶点的最短路径最多包含 ( ∣ V ∣ − 1 ) (|V|-1) (V1) 条边。这是因为如果一个路径包含 V V V 条边或更多,它必然会包含一个环(根据图论中的路径定义和鸽巢原理),而在最短路径中不应该包含环,因为环只会增加路径的总长度(除非是负权环,但这会使路径总长度趋向负无穷)。

为什么 ( V − 1 ) (V-1) (V1) 次松弛操作足够?

基例:0次松弛操作,在0次松弛操作之后:

  • 源点 s s s的距离 dist ( s ) \text{dist}(s) dist(s)初始化为0。
  • 其他所有顶点的距离初始化为正无穷大。

显然,此时从源点到自身的最短路径已经正确确定,其他顶点的最短路径尚未确定。

归纳假设:假设在第 k k k次松弛操作之后,从源点出发最多经过 k k k条边的最短路径已经正确确定。

归纳步骤
现在,我们需要证明在第 k + 1 k+1 k+1次松弛操作之后,从源点出发最多经过 k + 1 k+1 k+1条边的最短路径也能正确确定。

在第 k + 1 k+1 k+1次松弛操作中,对于每一条边 ( u , v ) (u, v) (u,v),我们尝试进行松弛操作:
dist ( v ) = min ⁡ ( dist ( v ) , dist ( u ) + w ( u , v ) ) \text{dist}(v) = \min(\text{dist}(v), \text{dist}(u) + w(u, v)) dist(v)=min(dist(v),dist(u)+w(u,v))

我们需要证明,从源点到任意顶点 v v v的最短路径最多经过 k + 1 k+1 k+1条边的距离被正确计算。

情况分析

  1. 如果从源点到顶点 v v v的最短路径最多经过 k + 1 k+1 k+1条边,则该路径可以表示为:
    s → u 1 → u 2 → ⋯ → u k → v s \rightarrow u_1 \rightarrow u_2 \rightarrow \cdots \rightarrow u_k \rightarrow v su1u2ukv
    这里, s → u 1 → u 2 → ⋯ → u k s \rightarrow u_1 \rightarrow u_2 \rightarrow \cdots \rightarrow u_k su1u2uk 是一条从源点 s s s到顶点 u k u_k uk的最短路径,且这条路径最多经过 k k k条边。在第 k k k次松弛操作之后,这条路径的最短距离已经正确确定。

  2. 在第 k + 1 k+1 k+1次松弛操作中,通过边 ( u k , v ) (u_k, v) (uk,v)进行松弛操作,可以更新顶点 v v v的最短距离:
    dist ( v ) = min ⁡ ( dist ( v ) , dist ( u k ) + w ( u k , v ) ) \text{dist}(v) = \min(\text{dist}(v), \text{dist}(u_k) + w(u_k, v)) dist(v)=min(dist(v),dist(uk)+w(uk,v))
    由于根据归纳假设, dist ( u k ) \text{dist}(u_k) dist(uk)已经正确表示了从源点到顶点 u k u_k uk的最短距离,所以在第 k + 1 k+1 k+1次松弛操作后,顶点 v v v的距离也会被更新为从源点出发最多经过 k + 1 k+1 k+1条边的最短路径距离。

负权环的检测
在进行完 V − 1 V-1 V1 次松弛操作之后,Bellman-Ford算法再进行一次全图边的松弛操作。如果在这次操作中仍然有边能够被松弛(即存在一条边 ( u , v ) (u, v) (u,v) 使得 dist ( v ) > dist ( u ) + weight ( u , v ) \text{dist}(v) > \text{dist}(u) + \text{weight}(u, v) dist(v)>dist(u)+weight(u,v)),则说明图中存在负权环,因为理论上在 V − 1 V-1 V1次松弛操作之后,所有最短路径应该已经稳定,不应该再有进一步的改进。

总结

  • 最多 V − 1 V-1 V1 条边:从源点到任意顶点的最短路径最多包含 V − 1 V-1 V1条边。
  • 逐步松弛:每次松弛操作最多确保路径增加一条边的最短路径被正确计算。
  • 负权环检测:通过第 V V V次松弛操作检测是否存在负权环。
typedef struct {
    int a, b, c;
} edge, Edges[M];

int dist[MaxVertexNum];
int last[MaxVertexNum];

void bellman_ford(Edges e, int u)
{
    memset(dist, 0x3f, sizeof dist);

    dist[u] = 0;
    for (int i = 0; i < vernum - 1; i ++ )
    {
        memcpy(last, dist, sizeof dist);
        for (int j = 0; j < arcnum; j ++ )
        {
            edge t = e[j];
            dist[t.b] = min(dist[t.b], last[t.a] + t.c);
        }
    }
}

时间复杂度: O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(V∣∣E)

SPFA算法(可以是负权图)

在Bellman-Ford算法中,不必要的松弛操作有以下几种情况:

  1. 已确定最短路径的顶点:某些顶点的最短路径在某轮松弛操作后已经确定,在后续的迭代中,对这些顶点的边进行松弛操作是多余的,因为它们的距离值不再改变。

  2. 无效松弛:对于某些边 ( u , v ) (u, v) (u,v),如果 d i s t a n c e [ u ] + w > = d i s t a n c e [ v ] distance[u] + w >= distance[v] distance[u]+w>=distance[v],即使继续松弛也不会更新 d i s t a n c e [ v ] distance[v] distance[v],这意味着这些松弛操作是无效的。

从而引入SPFA算法对Bellman-Ford算法进行改进,减少不必要的松弛操作。(只有该结点上一轮已经被松弛过,下一轮才会去更新与该结点相邻的所有结点)具体来说,SPFA算法的优化主要体现在:

  1. 队列管理:只对可能导致更新的顶点进行松弛操作。例如,在一次松弛操作中, 如果 d i s t a n c e [ u ] 如果distance[u] 如果distance[u]发生了变化,则将与 u u u相邻的顶点 v v v加入队列,以便后续检查是否需要松弛。

  2. 减少迭代次数:因为队列中只包含需要松弛的顶点,SPFA算法可能会在队列处理完之前结束,不必进行固定的 V − 1 V-1 V1轮松弛操作。

int dist[MaxVertexNum];
bool st[MaxVertexNum];

void spfa(ALGraph g, int u)
{
    memset(dist, 0x3f, sizeof dist);
    dist[u] = 0;

    Queue q, InitQueue(q);				// 在队列内的是待松弛结点
    EnQueue(q, u);
    st[u] = true;

    while (!isEmpty(q))
    {
        DeQueue(q, u)

        st[u] = false;

        for (ArcNode *p = g.vertices[u].firstarc; p; p = p->nextarc)
        {
            if (dist[p->adjvex] > dist[u] + p->info)		// 若可以松弛
            {
                dist[p->adjvex] = dist[u] + p->info
                if (!st[p->adjvex])			// 若不在松弛队列中,加入
                {
                    q.push(p->adjvex);
                    st[p->adjvex] = true;
                }
            }
        }
    }
}

时间复杂度: O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(V∣∣E),但是SPFA算法操作次数通常要比Bellman-Ford算法要少的多。

Floyd算法(点与点之间的最短路径)

f[i, j, k]表示从i走到j的路径上除ij点外只经过1k的点(作为中转点)的所有路径的最短距离。

那么f[i, j, k]一定是从这两个状态转换过来的

  • ij不经过k(作为中转点):f[i, j, k - 1]
  • ij经过k(作为中转点):f[i, k, k - 1] + f[k, j, k - 1]

因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。

int dist[MaxVertexNum][MaxVertexNum];

void floyd(){
    for(int k = 1; k <= MaxVertexNum; k ++)
        for(int i = 1; i <= MaxVertexNum; i ++)
            for(int j = 1; j <= MaxVertexNum; j ++)
                dist[i][j] = min(dist[i][j],dist[i][k] + dist[k][j]);
}

时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

有向无环图(DAG)

有向无环图:若一个有向图中不存在环,则称为有向无环图,简称 DAG图。

在这里插入图片描述
构建表达式的有向无环图

在这里插入图片描述

解题方法

  • Step 1:把各个操作数不重复地排成⼀排
  • Step 2:标出各个运算符的生效顺序(先后顺序有点出入无所谓)
  • Step 3:按顺序加⼊运算符,注意“分层”
  • Step 4:从底向上逐层检查同层的运算符是否可以合体

**加粗样式**

拓扑排序

AOV网(Activity On Vertex NetWork):若用**有向无环图(DAG)**表示一个工程,其顶点表示活动,用有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动 V i V_i Vi必须先于活动 V j V_j Vj进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,简称 AOV网即找到做事的先后顺序)。

在这里插入图片描述
拓扑排序的实现:
① 从AOV网中选择⼀个没有前驱(⼊度为0)的顶点并输出。
② 从网中删除该顶点和所有以它为起点的有向边。
③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点(说明有回路,入度全部>0)为止。
在这里插入图片描述

在这里插入图片描述

int indegree[MaxVertexNum];

bool ToplogicalSort(Graph g)
{
	Queue q, InitQueue(q);
	
	for (int i = 0; i < g.vexnum; i ++ )
		if (indergee[i] == 0)
			EnQueue(q, i);

	int cnt = 0;							// 记录当前已输出的结点数
	while (!isEmpty(q))
	{
		DeQueue(q, i);
		visit(i), cnt ++ ;
		
		for (ArcNode *p = g.vertices[i].firstarc; p; p = p->nextarc)
		{
			int v = p->adjvex;
			if (!(-- indegree[v]))
				EnQueue(v, q);
		}
	}

	if (cnt < g.vexnum) return false;
	return true;
}
  • 用邻接表,时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 用邻接矩阵,时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

逆拓扑排序

对⼀个AOV网,如果采⽤下列步骤进行排序,则称之为逆拓扑排序
① 从AOV网中选择⼀个没有后继 (出度为0) 的顶点并输出。
② 从网中删除该顶点和所有以它为终点的有向边。
③ 重复①和②直到当前的AOV网为空

在这里插入图片描述
用队列实现(非递归)

int outdegree[MaxVertexNum];

bool NevToplogicalSort(Graph g)
{
	Queue q, InitQueue(q);
	
	for (int i = 0; i < g.vexnum; i ++ )
		if (outdegree[i] == 0)
			EnQueue(q, i);

	int cnt = 0;							// 记录当前已输出的结点数
	while (!isEmpty(q))
	{
		DeQueue(q, i);
		visit(i), cnt ++ ;
		
		for (ArcNode *p = g.vertices[i].firstarc; p; p = p->nextarc)
		{
			int v = p->adjvex;
			if (!(-- outdegree[v]))
				EnQueue(v, q);
		}
	}

	if (cnt < g.vexnum) return false;
	return true;
}
  • 用邻接表,时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 用邻接矩阵,时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

用DFS实现(递归)

判断一个有向图中是否存在回路(环)的一个有效方法是使用深度优先搜索(DFS)来检测是否存在后向边(back edge)。在深度优先搜索的过程中,如果从一个顶点访问到已经在当前递归堆栈中的顶点,则说明存在环。

在进行逆拓扑排序时,DFS的实现可以通过以下方式判断回路:

  1. 使用递归堆栈标记:在DFS过程中,除了visited数组外,还使用一个recStack数组来标记当前递归堆栈中的顶点。
  2. 检查后向边:在访问邻接顶点时,如果邻接顶点已经在递归堆栈中,则说明存在回路。
bool visited[MaxVertexNum];
bool recStack[MaxVertexNum];

bool DFSTraverse(Graph g)
{
	for (int i = 0; i < g.vexnum; i ++ )
		visited[i] = false, recStack[i] = false;

	for (int i = 0; i < g.vexnum; i ++ )
		if (!visited[i] && DFS(g, i))
			return true;						// 有回路

	return false;								// 无回路
}

void DFS(Graph g, int u)
{
	visited[u] = true;
	recStack[u] = true;
	for (w = FirstNeigbor(g, v); w != -1; w = NextNeigbor(g, v, w))
	{
		if (!visited[w] && DFS(g, w))
			return true;
		else if (recStack[w])					// 检测到后向边
			return true;
	}
	
	recStack[u] = false;
    visit(u);
    return false;
}
  1. visited数组:用于标记每个顶点是否被访问过。
  2. recStack数组:用于标记当前递归堆栈中的顶点,检测后向边。
  3. DFS函数:在递归调用DFS时,设置recStack[u]为true。在访问邻接顶点时,如果发现邻接顶点已经在递归堆栈中(recStack[w]为true),则说明存在回路。
  4. DFSTraverse函数:对图中的每个顶点进行DFS,如果DFS过程中发现回路,则返回true。

关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)

在这里插入图片描述
AOE网具有以下两个性质:
① 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的。

在这里插入图片描述

从源点到汇点的有向路径可能有多条,所有路径中,具有最大径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

求关键路径的算法步骤如下:

①从源点出发,令 v e ( 源点 ) = 0 v_e(源点)=0 ve(源点)=0,按拓扑有序求其余顶点的最早发生时间 v e ( ) v_e() ve()
②从汇点出发,令 v l ( 汇点 ) = v e ( 汇点 ) v_l(汇点)=v_e(汇点) vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间 v l ( ) v_l() vl()
③根据各顶点的 v e v_e ve值求所有弧(弧头)的最早开始时间 e ( ) e() e()
④根据各顶点的 v l ( ) v_l() vl()值求所有弧(弧头)的最迟开始时间 l ( ) l() l()
⑤求AOE网中所有活动的差额 d ( ) d() d(),找出所有 d ( ) = 0 d()=0 d()=0的活动构成关键路径。

在这里插入图片描述
关键活动、关键路径的特性

  • 若关键活动耗时增加,则整个工程的工期将增长
  • 缩短关键活动的时间,可以缩短整个工程的工期
  • 当缩短到⼀定程度时,关键活动可能会变成非关键活动
  • 可能有多条关键路径,只提高⼀条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。

在这里插入图片描述
在这里插入图片描述
各种图算法在采用邻接矩阵或邻接表存储时的时间复杂度如下所示:

在这里插入图片描述

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

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

相关文章

打开服务器远程桌面连接不上,可能的原因及相应的解决策略

在解决远程桌面连接不上服务器的问题时&#xff0c;我们首先需要从专业的角度对可能的原因进行深入分析&#xff0c;并据此提出针对性的解决方案。以下是一些可能的原因及相应的解决策略&#xff1a; 一、网络连接问题 远程桌面连接需要稳定的网络支持&#xff0c;如果网络连接…

【一步一步了解Java系列】:何为数组,何为引用类型

看到这句话的时候证明&#xff1a;此刻你我都在努力加油陌生人个人主页&#xff1a;Gu Gu Study专栏&#xff1a;一步一步了解Java 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 数组 数组是一推相同数据…

JavaSE--基础语法(第一期)

Java是一种优秀的程序设计语言&#xff0c;它具有令人赏心悦目的语法和易于理解的语义。不仅如此&#xff0c;Java还是一个有一系列计算机软件和规范形成的技术体系&#xff0c;这个技术体系提供了完整的用于软件开发和 跨平台部署的支持环境&#xff0c;并广泛应用于嵌入式系统…

特殊变量笔记

执行demo4.sh文件,输入输出参数itcast itheima的2个输入参数, 观察效果 特殊变量&#xff1a;$# 语法 $#含义 获取所有输入参数的个数 案例需求 在demo4.sh中输出输入参数个数 演示 编辑demo4.sh, 输出输入参数个数 执行demo4.sh传入参数itcast, itheima, 播仔 看效果…

上位机图像处理和嵌入式模块部署(mcu之串口控制gpio)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们陆续学习了gpio输入、输出&#xff0c;串口输入、输出。其实有了这两个接口&#xff0c;就可以做产品了。因为我们可以通过发送串口命令&a…

门禁-jenkins的构建状态同步到gitlab提交流水线

API接口文档 https://docs.gitlab.cn/jh/api/commits.html 配置pipline流水线 生成http请求代码&#xff1a; 使用HttpRequest插件生成 - sharelibs内容 //这是share libs里的 package devopsdef httpReq(reqType, reqUrl, reqBody, accessToken){def gitServer "…

人才测评的应用:人才选拔,岗位晋升,面试招聘测评

人才测评自诞生以来&#xff0c;就被广泛应用在各大方面&#xff0c;不仅是我们熟悉的招聘上&#xff0c;还有其他考核和晋升&#xff0c;都会需要用到人才测评。不知道怎么招聘&#xff1f;或者不懂得如何实现人才晋升&#xff1f;都可以参考人才测评&#xff0c;利用它帮我们…

Amesim基础篇-元件详解-H型膨胀阀四象限解析

一 膨胀阀简介 膨胀阀的主要功能是节流和调节过热度,库内膨胀阀包含节流管、H型膨胀阀、T型膨胀阀三种: 节流管:一根内径较小的管路,当制冷剂通过他时发生等等焓降压降温,具有成本低,内径不可变的特点,因此普遍在家用空调中使用,在汽车空调上使用较少。当我们建模过程…

深入理解CPU缓存一致性

存储体系结构 速度快的存储硬件成本高、容量小&#xff0c;速度慢的成本低、容量大。为了权衡成本和速度&#xff0c;计算机存储分了很多层次&#xff0c;有寄存器、L1 cache、L2 cache、L3 cache、主存&#xff08;内存&#xff09;和硬盘等。 根据程序的空间局部性和时间局…

Java开发大厂面试第22讲:Redis 是如何保证系统高可用的?它的实现方式有哪些?

高可用是通过设计&#xff0c;减少系统不能提供服务的时间&#xff0c;是分布式系统的基础也是保障系统可靠性的重要手段。而 Redis 作为一款普及率最高的内存型中间件&#xff0c;它的高可用技术也非常的成熟。 我们今天分享的面试题是&#xff0c;Redis 是如何保证系统高可用…

Mac上安装OpenLDAP服务器详细教程(Homebrew安装和自带的ldap)

目录 前言 一、安装 Homebrew&#xff08;如果尚未安装&#xff09; 二、使用 Homebrew 安装 OpenLDAP 三、配置 OpenLDAP 步骤一&#xff1a;更新PATH和环境变量 步骤二&#xff1a;配置slapd.conf 步骤三&#xff1a;初始化和启动 OpenLDAP 服务 1.创建数据库目录 2…

你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解

目录 一、onMounted的前世今生 1.1、onMounted是什么 1.2、onMounted在vue2中的前身 1.2.1、vue2中的onMounted 1.2.2、Vue2与Vue3的onMounted对比 1.3、vue3中onMounted的用法 1.3.1、基础用法 1.3.2、顺序执行异步操作 1.3.3、并行执行多个异步操作 1.3.4、执行一次…

非等值连接、等值连接、自然连接

目录 一、笛卡尔积 二、三种连接的关系 三、非等值连接 四、等值连接 五、自然连接 一、笛卡尔积 要理解非等值连接、等值连接、自然连接首先要理解笛卡尔积。 学过《离散数学》的应该很熟悉笛卡尔积。 简单来说&#xff0c;就是有两个集合&#xff0c;其中一个集合中的元…

【华三包过】2024年/华三H3C/云计算GB0-713

H3CNE-cloud-云计算-713 想转行 想继续深入 题库覆盖百分百&#xff0c;题库有新版106道新版113道旧版88道 H3C认证云计算工程师&#xff08;H3C Certified Network Engineer for Cloud&#xff0c;简称H3CNE-Cloud&#xff09; 认证定位于全面掌握虚拟化技术原理及相关产品/…

部门来了个测试开发,听说是00后,上来一顿操作给我看蒙了...

公司新来了个同事&#xff0c;听说大学是学的广告专业&#xff0c;因为喜欢IT行业就找了个培训班&#xff0c;后来在一家小公司实习半年&#xff0c;现在跳槽来我们公司。来了之后把现有项目的性能优化了一遍&#xff0c;服务器缩减一半&#xff0c;性能反而提升4倍&#xff01…

Ollama| 搭建本地大模型,最简单的方法!效果直逼GPT

很多人想在本地电脑上搭建一个大模型聊天机器人。总是觉得离自己有点远&#xff0c;尤其是对ai没有了解的童鞋。那么今天我要和你推荐ollama&#xff0c;无论你是否懂开发&#xff0c;哪怕是零基础&#xff0c;只需十分钟&#xff0c;Ollama工具就可以帮助我们在本地电脑上搭建…

【vue-6】监听

一、监听watch 完整示例代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Documen…

回见,那果园

记不得何时开始骑行&#xff0c;何时开始爬山&#xff0c;何时偶遇洛师傅&#xff0c;何时进了那半山腰的果园。 似乎很远&#xff0c;又很近。 昨天打电话给果园的师傅&#xff0c;本意问问杏是否熟了&#xff0c;周末骑行过去、进山聊天顺道吃个新鲜。 洛师傅呵呵的笑…

【vue-1】vue入门—创建一个vue应用

最近在闲暇时间想学习一下前端框架vue&#xff0c;主要参考以下两个学习资料。 官网 快速上手 | Vue.js b站学习视频 2.创建一个Vue3应用_哔哩哔哩_bilibili 一、创建一个vue3应用 <!DOCTYPE html> <html lang"en"> <head><meta charset&q…

KubeKey 安装 K8s

官网教程 在 Linux 上以 All-in-One 模式安装 KubeSphere 步骤 1&#xff1a;准备 Linux 机器 若要以 All-in-One 模式进行安装&#xff0c;您仅需参考以下对机器硬件和操作系统的要求准备一台主机。 硬件推荐配置 操作系统最低配置Ubuntu 16.04, 18.04, 20.04, 22.042 核 …