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

文章目录

  • 逻辑结构
  • 物理结构
    • 邻接矩阵
      • 定义
      • 性能分析
      • 性质
      • 存在的问题
    • 邻接表
      • 定义
      • 性能分析
      • 存在的问题
    • 十字链表(有向图)
      • 定义
      • 性能分析
    • 邻接多重表(无向图)
      • 定义
      • 性能分析
  • 数据的操作
    • 图的基本操作
    • 图的遍历
      • 广度优先遍历(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];			// 初始全为falsevoid 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];			// 初始全为falsevoid 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->infoif (!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.mzph.cn/bicheng/15267.shtml

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

相关文章

WORD、PPT技巧

WORD技巧 编辑设置 word标题导航窗口怎么调出word2016&#xff0c;缩小了页面&#xff0c;可是怎么是竖着的一页一页排列啊&#xff1f;以前不是好几页横排着的么&#xff1f;怎么设置&#xff0c;求救&#xff1a;在Word标题栏那一行找到“视图”&#xff0c;点击“显示比例…

20212416 2023-2024-2 《移动平台开发与实践》第5次作业

百度地图应用 1.实验内容2.实验过程2.1 Android Studio配置2.1. 创建一个Android项目2.2 在项目中本地集成BaiduMap SDK 2.2 编写代码2.2.1 配置AndroidManifest.xml文件2.2.2 编写UI界面布局文件2.2.3 编写主函数代码2.2.4 运行结果 3.学习中遇到的问题及解决4.学习感悟与思考…

数据结构篇其三---链表分类和双向链表

​ 前言 数据结构篇其二实现了一个简单的单链表&#xff0c;链表的概念&#xff0c;单链表具体实现已经说明&#xff0c;如下&#xff1a; 单链表 事实上&#xff0c;前面的单链表本质上是无头单向不循环链表。此篇说明的双向链表可以说完全反过来了了。无论是之前的单链表还…

Java进阶学习笔记12——final、常量

final关键字&#xff1a; final是最终的意思。可以修饰类、方法、变量。 修饰类&#xff1a;该类就被称为最终类&#xff0c;特点是不能被继承了。 修饰方法&#xff1a;该方法是最终方法&#xff0c;特点是不能被重写了。 修饰变量&#xff1a;该变量只能被赋值一次。 有些…

智慧校园的建设思路

智慧校园建设的一个主要目的就是要打破学校内的信息孤岛&#xff0c;其核心是在人、流程和信息三个层面的全面整合。智慧校园应该能够为全校师生员工及校外用户提供统一的、一站式的服务渠道&#xff1b;能够将学校各种业务流程连接起来&#xff0c;实现各种应用系统的互联互通…

postgresql insert on conflict 不存在则插入,存在则更新

向一张表执行插入动作&#xff0c;如果插入的字段数据已存在&#xff0c;则执行更新操作&#xff0c;不存在则进行插入操作。 1、创建一张表 CREATE TABLE "user_info" ( "id" int2 NOT NULL, "name" varchar(20) COLLATE "pg_catalog&quo…

基于Tensorflow卷积神经网络人脸识别公寓人员进出管理系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 随着科技的快速发展和智能化水平的提高&#xff0c;公寓管理面临着越来越多的挑战。传统的公寓…

C++ 数据结构算法 学习笔记(32) -五大排序算法

C 数据结构算法 学习笔记(32) -五大排序算法 选择算法 如下若有多个女生的身高需要做排序: 常规思维: 第一步先找出所有候选美女中身高最高的&#xff0c;与最后一个数交换 第二步再找出除最后一位美女外其它美女中的最高者&#xff0c;与倒数第二个美女交换位置 再找出除最…

k8s-pod详解

一、Pod基本概念&#xff1a; 1.pod介绍&#xff1a; Pod是kubernetes中最小的资源管理组件&#xff0c;Pod也是最小化运行容器化应用的资源对象。一个Pod代表着集群中运行的一个进程。kubernetes中其他大多数组件都是围绕着Pod来进行支撑和扩展Pod功能的&#xff0c;例如&am…

电赛经验分享——赛前准备

⏩ 大家好哇&#xff01;我是小光&#xff0c;想要成为系统架构师的嵌入式爱好者。 ⏩在之前的电赛中取得了省一的成绩&#xff0c;本文对电赛比赛前需要准备什么做一个经验分享。 ⏩感谢你的阅读&#xff0c;不对的地方欢迎指正。 加入小光嵌入式交流群&#xff08;qq群号&…

在线人才测评在企业招聘和大学生求职中的应用场景

每年的春招秋招&#xff0c;都是毕业生们忙着找工作的季节&#xff0c;相比社招来说&#xff0c;春招秋招是每个毕业生务必重视的机会&#xff0c;大厂名企毕竟名额有限&#xff0c;如果找到自己心仪的职业岗位&#xff0c;作为毕业生就必须提前准备&#xff0c;深入了解招聘的…

五管OTA输入极性快速判断

做CMFB还有负反馈的时候曾经在判断输入输出极性上吃了大亏&#xff0c;直接做实验波形正确就是输入正端&#xff0c;全差分就不用考虑这么多了 和弯折&#xff0c;形状类似7&#xff0c;相同方向输入正端&#xff0c;相反的就是输入负端&#xff0c;输出也是和输入负端一个方向…

【NLP】人机对话

概念 机器翻译就是用计算机把一种语言翻译成另外一种语言的技术 机器翻译的产生与发展 17 世纪&#xff0c;笛卡尔与莱布尼茨试图用统一的数字代码来编写词典 1930 机器脑 1933 苏联发明家特洛阳斯基用机械方法将一种语言翻译为另一种语言 1946 ENIAC 诞生 1949 机器翻译问题…

香蕉成熟度检测YOLOV8NANO

香蕉成熟度检测YOLOV8NANO&#xff0c;采用YOLOV8NANO训练&#xff0c;得到PT模型&#xff0c;然后转换成ONNX模型&#xff0c;让OEPNCV调用&#xff0c;从而摆脱PYTORCH依赖&#xff0c;支持C。python&#xff0c;安卓开发。能检测六种香蕉类型freshripe freshunripe overripe…

Vita-CLIP: Video and text adaptive CLIP via Multimodal Prompting

标题&#xff1a;Vita-CLIP: 通过多模态提示进行视频和文本自适应CLIP 源文链接&#xff1a;https://openaccess.thecvf.com/content/CVPR2023/papers/Wasim_Vita-CLIP_Video_and_Text_Adaptive_CLIP_via_Multimodal_Prompting_CVPR_2023_paper.pdfhttps://openaccess.thecvf.…

ue5 中ps使用记录贴

一、快捷键记录 放大图形 ctrlalt空格 放大图形 缩小视口 ctrl空格 ctrlD 取消选区 ctrlt缩小文字 w魔棒工具 选择魔棒的时候把容差打开的多一点 二、案例 移动文字 在相应的图层选择 移动文字 修改图片里的颜色 在通道里拷贝红色通道&#xff0c;复制红色通道粘贴给正常图…

大模型应用商业化落地关键:给企业带来真实的业务价值

2024 年被很多人称为大模型应用的元年&#xff0c;毫无疑问&#xff0c;大模型已经成为共识&#xff0c;下一步更急迫的问题也摆在了大家的面前——大模型到底能够用在哪&#xff1f;有哪些场景能落地&#xff1f;怎么做才能创造真正的价值&#xff1f; 在刚刚过去的 AICon 全…

【排序算法】快速排序(四个版本以及两种优化)含动图)

制作不易&#xff0c;三连支持一下吧&#xff01;&#xff01;&#xff01; 文章目录 前言一.快速排序Hoare版本实现二.快速排序挖坑法版本实现三.快速排序前后指针版本实现四.快速排序的非递归版本实现五.两种优化总结 前言 前两篇博客介绍了插入和选择排序&#xff0c;这篇博…

halcon配合yolov8导出onnx模型检测物体

1.工业上多数视觉开发都是用halcon开发的&#xff0c;halcon本身也有自己的深度学习网络&#xff0c;至于halcon如果不使用本身的深度学习&#xff0c;使用其他网络导出的onnx模型怎样配合使用&#xff1f;本文基于yolov8写一个列子。 2。创建输入数据的转换代码 #region 创建输…

【bugfix】/usr/local/bin/docker-compose:行1: html: 没有那个文件或目录

前言 在使用 docker-compose 管理容器化应用时&#xff0c;偶尔会遇到一些意想不到的错误&#xff0c;比如当尝试运行 docker-compose 命令时&#xff0c;终端非但没有展示预期的输出&#xff0c;反而出现类似网页错误的信息。这类问题通常与 docker-compose 的安装或配置有关…