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

文章目录

  • 逻辑结构
  • 物理结构
    • 邻接矩阵
      • 定义
      • 性能分析
      • 性质
      • 存在的问题
    • 邻接表
      • 定义
      • 性能分析
      • 存在的问题
    • 十字链表(有向图)
      • 定义
      • 性能分析
    • 邻接多重表(无向图)
      • 定义
      • 性能分析
  • 数据的操作
    • 图的基本操作
    • 图的遍历
      • 广度优先遍历(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的活动构成关键路径。

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

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

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

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python项目:数据可视化_下载数据【笔记】

源自《Python编程&#xff1a;从入门到实践》 作者&#xff1a; Eric Matthes 02 下载数据 2.1 sitka_weather_07-2021_simple.csv from pathlib import Path import matplotlib.pyplot as plt import csv from datetime import datetimepath Path(D:\CH16\sitka_weather_0…

Python--List列表

list列表⭐⭐ 1高级数据类型 Python中的数据类型可以分为&#xff1a;数字型&#xff08;基本数据类型&#xff09;和非数字型&#xff08;高级数据类型&#xff09; ●数字型包含&#xff1a;整型int、浮点型float、布尔型bool、复数型complex ●非数字型包含&#xff1a;字符…

杰理-耳机进入关机关闭内内置触摸-节省功耗

杰理-耳机进入关机关闭内内置触摸-节省功耗 if (__this->init 0) {return LP_TOUCH_SOFTOFF_MODE_LEGACY; }if ((__this -> softoff_mode LP_TOUCH_SOFTOFF_MODE_ADVANCE) && (__this->softoff_keep 0)) {lp_touch_key_disable(); } __this->softoff_k…

干货 | 2024 EISS 企业信息安全高峰论坛(脱敏)PPT(7份可下载)

2024 EISS 企业信息安全高峰论坛&#xff08;脱敏&#xff09;PPT&#xff0c;共7份。 AI在出海业务的安全实践.pdf Palo Alto Networks为中国企业全球化布局保驾护航.pdf 安全建设与治理思路.pdf 车路云一体化安全体系建设实践.pdf 企业研发安全DevSecOps流程落地实践.pdf 浅谈…

服务器端口号怎么看?如何查看服务器端口号呢?有哪些需要注意的?

简单来说&#xff0c;端口号就是计算机与外界通讯交流的出口&#xff0c;每个端口都有不同的编号&#xff0c;也就是“端口号”。它们是唯一的&#xff0c;用于标识不同的服务和应用程序。通过端口号&#xff0c;我们可以知道哪些服务正在运行&#xff0c;以及如何与它们进行通…

你真的会使用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、执行一次…

【机器学习300问】98、卷积神经网络中的卷积核到底有什么用?以边缘检测为例说明其意义。

卷积核是用于从输入数据中提取特征的关键工具。卷积核的设计直接关系到网络能够识别和学习的特征类型。本文让我以边缘检测为例&#xff0c;带大家深入理解卷积核的作用。 一、卷积核的作用 卷积核&#xff0c;又称为过滤器&#xff0c;本质上是一个小的矩阵&#xff0c;其元素…

ClickHouse 24.4 版本发布说明

本文字数&#xff1a;13148&#xff1b;估计阅读时间&#xff1a;33 分钟 审校&#xff1a;庄晓东&#xff08;魏庄&#xff09; 本文在公众号【ClickHouseInc】首发 新的一个月意味着新版本的发布&#xff01; 发布概要 本次ClickHouse 24.4版本包含了13个新功能&#x1f381;…

Dalle2学习

Dalle2 mini有GitHub库并且有网页可以直接测试

Logstash笔记

目录​​​​​​​ 一、简介 二、单个输入和输出插件 三、多个输入和输出插件 四、pipeline结构 五、队列和数据弹性 六、内存队列 七、持久化队列 八、死信队列 (DLQ) 九、输入插件 1)、beats 2)、dead_letter_queue 3)、elasticsearch 4)、file 5)、redis 十、…

AI大模型探索之路-实战篇6: Function Calling技术调研之详细流程剖析

系列篇章&#x1f4a5; AI大模型探索之路-实战篇4&#xff1a;DB-GPT数据应用开发框架调研实践 AI大模型探索之路-实战篇5&#xff1a; Open Interpreter开放代码解释器调研实践 目录 系列篇章&#x1f4a5;一、前言二、Function Calling详细流程剖析1、创建OpenAI客户端2、定…

手机边听边充音频转接器双盲插系列:便捷充电,畅享音乐6500

在快节奏的生活中&#xff0c;手机已经成为我们不可或缺的日常用品。无论是工作、学习还是娱乐&#xff0c;手机都扮演着重要角色。然而&#xff0c;当我们沉浸在音乐的海洋中时&#xff0c;手机电量不足的困扰却时常打断我们的美好体验。为了解决这一难题&#xff0c;手机边听…

ESP32-C3模组上跑通OTA升级(8)

接前一篇文章&#xff1a;ESP32-C3模组上跑通OTA升级&#xff08;7&#xff09; 本文内容参考&#xff1a; 杂项系统 API - ESP32 - — ESP-IDF 编程指南 latest 文档 《ESP32-C3 物联网工程开发实战》 乐鑫科技 特此致谢&#xff01; 七、固件版本 将不同功能的固件标记为…

好书推荐|MATLAB科技绘图与数据分析

提升你的数据洞察力&#xff0c;用于精确绘图和分析的高级MATLAB技术 MATLAB科技绘图与数据分析——jd 本书内容 《MATLAB科技绘图与数据分析》结合作者多年的数据分析与科研绘图经验&#xff0c;详细讲解MATLAB在科技图表制作与数据分析中的使用方法与技巧。全书分为3部分&a…

ChatGPT可以开车吗?分享大型语言模型在自动驾驶方面的应用案例

自动驾驶边缘案例需要复杂的、类似人类的推理&#xff0c;远远超出传统的算法和人工智能模型。而大型语言模型正在致力实现这一目标。 人工智能技术如今正在快速发展和应用&#xff0c;人工智能模型也是如此。拥有100亿个参数的通用模型的性能正在碾压拥有5000万个参数的任务特…

Java进阶学习笔记11——多态

什么是多态&#xff1f; 多态是在继承/实现情况下一种现象&#xff0c;表现为&#xff1a;对象多态和行为多态。 同一个对象&#xff0c;在不同时刻表现出来的不同形态。 多态的前提&#xff1a; 要有继承/实现关系 要有方法的重写 要有父类引用指向子类对象。 多态的具体代码…

电脑文件夹怎么加密?文件夹加密软件推荐

加密文件夹是保护电脑数据的重要方法&#xff0c;我们可以使用专业的文件夹加密软件来加密保护文件夹。下面小编就为大家介绍三种优秀的文件夹加密软件&#xff0c;安全保护电脑文件夹。 文件夹加密超级大师 文件夹加密超级大师是一款功能强大的文件夹加密软件&#xff0c;文件…

Spring 模拟管理Web应用程序

MVC&#xff1a;Model View Controller 1&#xff09;controller&#xff1a;控制层&#xff08;Servlet是运行服务器端&#xff0c;处理请求响应java语言编写技术&#xff09; 2&#xff09;service&#xff1a;业务层&#xff08;事务&#xff0c;异常&#xff09; 3&#xf…

Apache Hive 安装与配置的详细教程

1. Hive简介 Hive是基于Hadoop的一个数据仓库工具&#xff0c;用来进行数据提取、转化、加载&#xff0c;这是一种可以存储、查询和分析存储在Hadoop中的大规模数据的机制。hive数据仓库工具能将结构化的数据文件映射为一张数据库表&#xff0c;并提供SQL查询功能&#xff0c;能…