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

2024-05-25 17:44

本文主要是介绍图论(从数据结构的三要素出发),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 逻辑结构
  • 物理结构
    • 邻接矩阵
      • 定义
      • 性能分析
      • 性质
      • 存在的问题
    • 邻接表
      • 定义
      • 性能分析
      • 存在的问题
    • 十字链表(有向图)
      • 定义
      • 性能分析
    • 邻接多重表(无向图)
      • 定义
      • 性能分析
  • 数据的操作
    • 图的基本操作
    • 图的遍历
      • 广度优先遍历(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.chinasem.cn/article/1002195

相关文章

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(

浙大数据结构:树的定义与操作

四种遍历 #include<iostream>#include<queue>using namespace std;typedef struct treenode *BinTree;typedef BinTree position;typedef int ElementType;struct treenode{ElementType data;BinTree left;BinTre

Python 内置的一些数据结构

文章目录 1. 列表 (List)2. 元组 (Tuple)3. 字典 (Dictionary)4. 集合 (Set)5. 字符串 (String) Python 提供了几种内置的数据结构来存储和操作数据,每种都有其独特的特点和用途。下面是一些常用的数据结构及其简要说明: 1. 列表 (List) 列表是一种可变的有序集合,可以存放任意类型的数据。列表中的元素可以通过索

浙大数据结构:04-树7 二叉搜索树的操作集

这道题答案都在PPT上,所以先学会再写的话并不难。 1、BinTree Insert( BinTree BST, ElementType X ) 递归实现,小就进左子树,大就进右子树。 为空就新建结点插入。 BinTree Insert( BinTree BST, ElementType X ){if(!BST){BST=(BinTree)malloc(sizeof(struct TNo

【数据结构入门】排序算法之交换排序与归并排序

前言         在前一篇博客,我们学习了排序算法中的插入排序和选择排序,接下来我们将继续探索交换排序与归并排序,这两个排序都是重头戏,让我们接着往下看。  一、交换排序 1.1 冒泡排序 冒泡排序是一种简单的排序算法。 1.1.1 基本思想 它的基本思想是通过相邻元素的比较和交换,让较大的元素逐渐向右移动,从而将最大的元素移动到最右边。 动画演示: 1.1.2 具体步

数据结构:线性表的顺序存储

文章目录 🍊自我介绍🍊线性表的顺序存储介绍概述例子 🍊顺序表的存储类型设计设计思路类型设计 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾” 和“内容共创官” ,现在我来为大家介绍一下有关物联网-嵌入

[数据结构]队列之顺序队列的类模板实现

队列是一种限定存取位置的线性表,允许插入的一端叫做队尾(rear),允许删除的一端叫做队首(front)。 队列具有FIFO的性质 队列的存储表示也有两种方式:基于数组的,基于列表的。基于数组的叫做顺序队列,基于列表的叫做链式队列。 一下是基于动态数组的顺序队列的模板类的实现。 顺序队列的抽象基类如下所示:只提供了接口和显式的默认构造函数和析构函数,在派生类中调用。 #i