数据结构—图(下)

文章目录

  • 12.图(下)
    • (4).生成树和最小生成树
      • #1.什么是生成树和最小生成树?
        • i.生成树
        • ii.最小生成树
      • #2.Prim算法
        • i.算法思想
        • ii.看看例子
        • iii.代码实现
      • #3.Kruskal算法
        • i.算法思想
        • ii.看看例子
        • iii.代码实现
      • #4.次小生成树
    • (5).最短路径问题
      • #1.加权有向图的最短路径问题
      • #2.单源最短路径问题—Dijkstra算法
        • i.基本实现方法
        • ii.优先队列优化方法
      • #3.多源最短路径问题—Floyd算法
        • i.实现方法
        • ii.求传递闭包
      • #4.还有更多
    • (6).拓扑排序
      • #1.什么是拓扑序列?
      • #2.拓扑排序实现方式
      • #3.关键路径和AOE网络
        • i.什么是AOE网络
        • ii.关键路径
        • iii.代码实现
    • 小结

12.图(下)

(4).生成树和最小生成树

#1.什么是生成树和最小生成树?

i.生成树

  G是一个连通无向图,若G’是包含G中所有顶点的一个无回路的连通子图,则称G’为G的一棵生成树,其实还比较简单

  生成树实际上就是某一张图的一种遍历结果,它保证了生成的子图一定是一棵树,因此对于一个具有n个顶点的无向图G其生成树G’一定恰好有n-1条边,在生成树G’中任意添加一条边,则会形成一个回路

  通过DFS得到的生成树叫做深度优先生成树,而BFS得到的生成树则叫做广度优先生成树,连通无向图的生成树不是唯一的,例如对于这个图:
p47
  它的以1为起点的深度优先生成树如下
p48
  不过即便是限定了以1为起点,这个无向连通图的深度优先生成树仍然不是唯一的,例如我们很明显可以看到1→4→2→5→8也是一条合法的深度优先搜索路径,这里只是举出了一种例子,而以1为起点的广度优先搜索树如下
p49
  如果我们认为生成树是无序树,那么以某个点为起点的广度优先生成树是唯一的

ii.最小生成树

  如果我们给图的每条边赋上权重,那么这个图就变成了有权无向图,这时候我们希望求出一个生成树,使得其所有路径的权重和最小,这就是最小生成树(Minimum Spanning Tree, MST),也叫作最小代价生成树

  这个问题其实不难解,因为点到点之间只有连通的情况下才能走,所以这个时候我们可以用一个贪心的思路来解决问题,例如每次选取最小的边,再在后续依次合并,我们有两个比较经典的求最小生成树的方法,分别是:从一点开始慢慢成长成一棵大树的Prim算法不断取最小形成森林再逐渐合并形成一棵大树的Kruskal算法

#2.Prim算法

i.算法思想

  带权连通无向图G=(V,E),设U为V中构成最小生成树的顶点集合,初始时 U = { u 0 } U=\{u_0\} U={u0},算法的流程如下:

  • 从G的某一顶点 u 0 u_0 u0出发,选择与它关联的具有最小权值的边 ( u 0 , v ) (u_0,v) (u0,v),将其顶点加入集合 U U U
  • 以后每一步从一个顶点在U中,而另一个顶点不再U中的各条边选择权值最小的边(u, v),把它的顶点加入到集合U中,直到U=V为止
ii.看看例子

  好像听懂了,又好像没听懂,我们来看看这个例子:
p50
  在这里我们以1作为起始结点,这时候 U = { 1 } U=\{1\} U={1},我们选择最短路径1和对应的顶点加入U,则选择3加入U,此时 U = { 1 , 3 } U=\{1,3\} U={1,3}
p51
  这时候再选择1和3能选到的最小权边,这里选择4,将顶点6放入:
p52
  再选择最小的边(4, 2),此时将顶点4放入, U = { 1 , 3 , 6 , 4 } U=\{1, 3, 6, 4\} U={1,3,6,4}
p53
  然后选取最短的能够增加连接性的边(2, 5),此时将顶点2放入, U = { 1 , 2 , 4 , 5 , 6 } U=\{1, 2, 4, 5, 6\} U={1,2,4,5,6}
p54
  最后把最后一条边权为3的边放入U,此时得到 U = { 1 , 2 , 3 , 4 , 5 , 6 } U=\{1,2,3,4,5,6\} U={1,2,3,4,5,6}
p55
  此时,最小生成树已经生成完毕,所以此时我们会发现,实际上树的性质保证了从任何一个结点开始,这个结构仍然是一棵树,所以,Prim算法可以随便从任何一个结点开始取,到最后都能生成同一棵最小生成树

iii.代码实现

  下面给出的是一个基于邻接表的,利用优先队列优化的Prim算法

#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 5050, M = 2e5 + 10;struct E 
{int v, w, x;
} e[M * 2];int n, m, h[N], cnte;void adde(int u, int v, int w) 
{e[++cnte] = E{v, w, h[u]};h[u] = cnte; 
}struct S 
{int u, d;bool operator<(const S& s1) const{return d > s1.d;}
};priority_queue<S> q;
int dis[N];
bool vis[N];int res = 0, cnt = 0;void Prim() 
{memset(dis, 0x3f, sizeof(dis));dis[1] = 0;q.push({1, 0});while (!q.empty()) {if (cnt >= n) break;int u = q.top().u, d = q.top().d;q.pop();if (vis[u]) continue;vis[u] = 1;++cnt;res += d;for (int i = h[u]; i; i = e[i].x) {int v = e[i].v, w = e[i].w;if (w < dis[v]) {dis[v] = w, q.push({v, w});}}}
}int main() 
{cin >> n >> m;for (int i = 1, u, v, w; i <= m; ++i) {cin >> u >> v >> w;adde(u, v, w);adde(v, u, w);}Prim();if (cnt == n)cout << res;elsecout << "No MST.";return 0;
}

  你可以试着理解一下优化后的版本,下面是一个对于邻接矩阵的版本:

void prim(int cost[][MAXN], int n, int u)
{int lowcost[MAXN], min;int closest[MAXN];int i, j, k;for (i = 1; i <= n; i++) {lowcost[i] = cost[u][i];closest[i] = u;}for (i = 1; i < n; i++) {min = MAXINT;for (j = 1; j <= n; j++) {if (lowcost[j] != 0 && lowcost[j] < min) {min = lowcost[j];k = j;}printf("{%3d%3d%3d%5d"} ", closest[k], k, min);lowcost[k] = 0;for (j = 1; j <= n; j++) {if (cost[k][j] != 0 && cost[k][j] < lowcost[j]) {lowcost[j] = cost[k][j];closest[j] = k;}}}}
}

#3.Kruskal算法

i.算法思想

  Kruskal算法的思想更加直观一点:

  • 初始时, T = ( V , Φ ) T=(V,\Phi) T=(V,Φ),即T由n个连通分量组成,每个连通分量只有一个顶点,没有边
  • 把边的权值按照从小到大排序,依次进行如下选择:若当前被选择的边的两个顶点在不同的连通分量中,则把这条边加到T中(即每次选权最小且不构成回路的一条边加入T),直到T中有n-1条边为止

  Kruskal的过程强烈依靠并查集来实现,它会判断候选边的两个顶点是否在不同的集合,如果在同一个集合,放弃这条边否则就选择这条边,同时合并两个集合

ii.看看例子

  首先选取最小的一条边1:
p56
  然后再选取最小的一条边2:
p57
  之后再选择3:
p58
  再选择最小的一条边4:
p59
  这一次需要从所有边权为5的边中选一条,只有(2, 3, 5)这条边满足两个顶点在不同的集合中,因此我们选取这一条,至此整个最小生成树就生成完毕了:
p60

iii.代码实现
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
constexpr int eN = 2e5+50; // 边
constexpr int nN = 5e3+20; // 点struct edge
{int beg;int end;int w;bool operator<(const edge& e) const{return w < e.w;}
};edge g[eN];
vector<int> v(nN, -1);
int mst{ 0 };int Find(vector<int>& vec, int x)
{if (vec[x] < 0) return x;return Find(vec, vec[x]);
}void Union(vector<int>& vec, int x, int y)
{int px{ Find(vec, x) }, py{ Find(vec, y) };if (px != py) {vec[px] += vec[py];vec[py] = px;}
}void Kruskal(int N, int M)
{sort(g + 1, g + 1 + M);for (int i = 1; i <= M; i++) {int px{ Find(v, g[i].beg) }, py{ Find(v, g[i].end) };if (px != py) {Union(v, px, py);mst += g[i].w;}}
}int main()
{int N{ 0 }, M{ 0 };cin >> N >> M;for (int i = 1; i <= M; i++) {int st{ 0 }, end{ 0 }, w{ 0 };cin >> st >> end >> w;g[i] = { st, end, w };}Kruskal(N, M);int root{ Find(v, 1) };for (int i = 2; i <= N; i++) {if (Find(v, i) != root) {mst = -1;break;}}cout << mst << endl;return 0;
}

  简单的算法,对吧?

#4.次小生成树

  次小生成树采取了一个试探的方式,对于当前已经生成的最小生成树去尝试换用别的边:

#include <iostream>
#include <algorithm>
using namespace std;constexpr int INF = 0x3f3f3f3f;
constexpr int N = 505;
int G[N][N]{ {0} }, used[N][N]{ {0} }, path[N][N]{ {0} };
int pre[N]{ 0 }, dis[N]{ 0 };
bool vis[N]{ false };
int m, n;void init() 
{for (int i = 1; i <= n; i++) {vis[i] = false;for (int j = 1; j <= n; j++)G[i][j] = INF;}
}int Prim(int st) 
{int sum = 0, Min;for (int i = 1; i <= n; i++) {dis[i] = G[st][i];pre[i] = st;}dis[st] = 0;vis[st] = 1;for (int i = 1; i < n; i++) {Min = INF;for (int j = 1; j <= n; j++) {if (!vis[j] && Min > dis[j]) {Min = dis[j];st = j;}}vis[st] = 1;sum += Min;used[st][pre[st]] = used[pre[st]][st] = 1;for (int j = 1; j <= n; j++) {if (vis[j] && j != st)path[j][st] = path[st][j] = max(path[j][pre[st]], dis[st]);if (!vis[j] && dis[j] > G[st][j]) {dis[j] = G[st][j];pre[j] = st;}}}return sum;
}int sec_Prim(int tmp) 
{int sum{ INF };for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {if (i != j && !used[i][j])sum = min(sum, tmp + G[i][j] - path[i][j]);}}return sum;
}

  通过这个方式可以在连通的情况下求出另外一个最小生成树,这个次小生成树可能比最小生成树更大,也可能和当前的最小生成树相同

(5).最短路径问题

#1.加权有向图的最短路径问题

  从学校出发,到最近的餐厅,要走多少路呢?作为一个懒人,我肯定会选择走更短的路,所以我们可以把这个问题抽象一下:把地图上的每个地标作为顶点,选择两个地标间的最短路径长度为对应的边权重,于是求到更加远的,不能直达的两点的最短路径,就是我们希望解决的问题了,而这也就是加权有向图的最短路径问题,对于一个无向图,其实事情也是一样的,我们可以认为它是一个A到B和B到A的边权相同的图

#2.单源最短路径问题—Dijkstra算法

  Dijkstra是个很厉害的人,从他的名字就可以看出来:他的名字里居然同时有i,j,k三个连续字母! Dijkstra算法用于求单源无负权边图的最短路径,也就是求得从一个点开始,到全局所有其他点的最短路径,但是要注意的是,Dijkstra不能应对负权边的问题,这个问题在Bellman-Ford算法有了解决

i.基本实现方法

  Dijkstra的算法是这样的:

  • S = { v } S=\{v\} S={v},即从顶点v出发
  • 按以下步骤逐个求得从v到其他顶点的最短路径,直到把所有顶点的最短路径求出:
    • a. 选取不在S中且具有最小距离的顶点K
    • b.把K加入集合S
    • c.修改不再S中的顶点的距离

  我们需要使用三个数组来存放有向图的各种信息:

  • dist[]: 记录各个顶点的当前距离(当前到达v的距离)
  • pre[]: 存放从起点v到顶点j的最短路径j前面的顶点
  • S[]: 类似于visited,即标记已经求出最短路的顶点

  例如对于下面这张图:
p61
  我们根据算法的具体操作每一步可以得到dist,pre,S三个数组的变化如下:
p62
  具体的步骤这里就不再赘述了,接下来给出一段基本的实现思路:

int cost[MAXN][MAXN]{{0}};
int dist[MAXN]{0}, pre[MAXN]{0}, n{0}, v{0};void dijkstra(int n, int v)
{bool s[MAXN]{0};int i, j, k, min;for (i = 1; i <= n; i++) {dist[i] = cost[v][i];s[i] = 0;if (dist[i] < MAXINT) pre[i] = v;else pre[i] = 0;}s[v] = true;pre[v] = 0;for (i = 1; i <= n; i++) {min = MAXINT;k = 0;for (j = 1; j <= n; j++) {if (!s[j]) if (dist[j] != 0 && dist[j] < min) {min = dist[j];k = j;} }if (k == 0) break;s[k] = 1;for (j = 1; j <= n; j++) {if (s[j] == 0 && cost[k][j] < MAXINT) {if (dist[k] + cost[k][j] < dist[j]) {dist[j] = dist[k] + cost[k][j];pre[j] = k;}}}}
}

  这样的实现方法的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

ii.优先队列优化方法

  我们可以基于优先队列的方式来优化Dijkstra:

constexpr int maxn = 5e5 + 20;struct edge
{int v, w;
};struct node
{int dis, u;bool operator>(const node& a) const{return dis > a.dis;}
};vector<edge> e[maxn];
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node>> q;void dijkstra(int n, int s)
{memset(dis, 0x3f, sizeof(dis));dis[s] = 0;q.push({ 0, s });while (!q.empty()) {int u = q.top().u;q.pop();if (vis[u]) continue;vis[u] = 1;for (const auto& ed : e[u]) {int v = ed.v, w = ed.w;if (dis[v] > dis[u] + w) {dis[v] = dis[u] + w;q.push({ dis[v], v });}}}
}

  这种情况下,时间复杂度可以被优化到 O ( ∣ E ∣ log ⁡ ∣ V ∣ ) O(|E|\log|V|) O(ElogV)

#3.多源最短路径问题—Floyd算法

  Floyd比较好的一点是,可以直接求出每一个点到其他的点的最短路径长度,比较坏的一点是它的时间复杂度非常高,为 O ( ∣ V ∣ 3 ) O(|V|^3) O(V3),对于一个数据量非常大的图,我们可能得换用Johnson算法求出全局的最短路径

i.实现方法

  Floyd的思路是这样的:假设求从 v i v_i vi v j v_j vj的最短路径,如果 v i v_i vi v j v_j vj有边,那就存在一条长度为cost[i][j]的边,但这不一定是最短路径,因为可能存在一条从 v i v_i vi v j v_j vj,但包含其他中间点的更短的路径
  因此我们需要进行n次试探,测试看从 v i v_i vi v j v_j vj是否能有包含其他中间点的更短路径,所以Floyd的基于代价矩阵A实现,对于第k步的 A ( k ) A^{(k)} A(k),我们可以有以下递推公式:
{ A ( 0 ) ( i , j ) = c o s t ( i , j ) , A ( k ) ( i , j ) = min ⁡ { A ( k − 1 ) ( i , j ) , A ( k − 1 ) ( i , k ) + A ( k − 1 ) ( k , j ) } , 1 ≤ k ≤ n \begin{cases} A^{(0)}(i,j) = cost(i,j),\\ A^{(k)}(i,j) = \min\{A^{(k-1)}(i,j), A^{(k-1)(i,k)+A^{(k-1)}(k,j)}\}, 1\leq k\leq n \end{cases} {A(0)(i,j)=cost(i,j),A(k)(i,j)=min{A(k1)(i,j),A(k1)(i,k)+A(k1)(k,j)},1kn
A ( 0 ) A^{(0)} A(0)为初始给定的代价矩阵,而 A ( k ) ( i , j ) ( 1 ≤ i , j ≤ n ) A^{(k)}(i,j)(1\leq i, j\leq n) A(k)(i,j)(1i,jn)表示从顶点i到顶点j的中间顶点序号不大于k的最短路径长度
  有了这个,我们就可以递推地产生 A ( 0 ) , A ( 1 ) , . . . , A ( n ) A^{(0)},A^{(1)}, ..., A^{(n)} A(0),A(1),...,A(n)了, A ( n ) A^{(n)} A(n)是我们最后需要的解,所以,它的代码真的超级简单:

void floyd(int n)
{for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {a[i][j] = cost[i][j];path[i][j] = 0; // path数组用于存储路径}}for (int k = 1; k <= n; k++) {for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {if (a[i][j] > a[i][k] + a[k][j]) {a[i][j] = a[i][k] + a[k][j];path[i][j] = k;}}}}
}

  一个非常简单的三重循环,一个直接就能看出来的 O ( n 3 ) O(n^3) O(n3)时间复杂度,对吧?

ii.求传递闭包

  对于一个关系R,具备某个性质P的闭包可以定义为:能够使关系R满足属性P的最小集合,关系R可能本身不满足P,我们可以添加较少的一些元组使得其满足性质P,而传递闭包需要的则是 x R y , y R z ⇒ x R z xRy, yRz\Rightarrow xRz xRy,yRzxRz
  R的传递闭包记为t®,它可以用这个算法得到:
t ( R ) = ⋃ i = 1 ∞ R i t(R) = \bigcup_{i=1}^\infty R^i t(R)=i=1Ri

  对于一个有限的关系R,我们可以用有限次的并完成这个操作,因此,我们可以用Floyd算法求传递闭包:

...
for (int k = 1; k <= n; k++) {for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {a[i][j] |= a[i][k] && a[k][j];}}
}

#4.还有更多

  实际上求最短路径的方法还有很多,例如Johnson全源最短路算法、Bellman-Ford算法等等,其中Bellman-Ford算法在后续的 《计算机网络》 课程中还会学到(基于距离向量的路由算法),你可以去了解一下,OI-Wiki就是一个很不错的选择

(6).拓扑排序

#1.什么是拓扑序列?

  例如计算机科学与技术学院开的很多课,具有先修课程的要求:

课程代号课程名称先修课
C1程序设计基础
C2离散数学C1
C3数据结构C1,C2
C4汇编语言C1
C5语言的设计与分析C3,C4
C6计算机原理C10
C7编译原理C3,C5
C8操作系统C3,C6
C9高等数学
C10普通物理C9

  如果以顶点作为活动,我们可以得到以下的有向图:
p63
  这样的图被我们称为顶点表示活动的网络,简称AOV网络,所以我们需要一个顺序,能够在先修的限制下,得到一个合法的课程修读顺序

  接下来是一大堆定义:设S是一个集合,R是S上的一个关系,a,b是S中的元素,若 ( a , b ) ∈ R (a,b)\in R (a,b)R,则称a是b关于R的前驱元素,b是a关于R的后继元素

  设S是一个集合,R是S上的一个关系,a,b,c是S中的元素,若有 ( a , b ) ∈ R (a,b)\in R (a,b)R ( b , c ) ∈ R (b,c)\in R (b,c)R,则必有 ( a , c ) ∈ R (a,c)\in R (a,c)R,则称R是S上的传递关系

  若对于S中的任意元素,不存在 ( a , a ) ∈ R (a,a)\in R (a,a)R,则称R是S上的一个非自反关系

  若S上的一个关系R是传递的以及非自反的,则称R是S上的一个半序关系

  在任何一个具有半序关系R的有限集合中,至少有一个元素没有前驱,也至少有一个元素没有后继

  若R是集合S上的一个半序关系, A = a 1 , a 2 , . . . , a n A=a_1,a_2,...,a_n A=a1,a2,...,an是S中元素的一个序列,且当 ( a i , a j ) ∈ R (a_i,a_j)\in R (ai,aj)R时,有 i < j i<j i<j,则称A是相对于R的一个拓扑序列,若 a i a_i ai a j a_j aj关于R没有关系,则它们在A中的排列次序不受限制

  所以在这么多定义之后,我们就知道什么是拓扑序列了,实际上就是希望得到一个AOV网络的合法执行顺序,所以很容易知道:AOV网络中不能存在有向环路,否则不可能得出一个能够合法执行的顺序,而操作一个有向图得到拓扑序列的过程就是拓扑排序

#2.拓扑排序实现方式

  其实拓扑排序实现起来非常简单:

  • 在网络中选择一个没有前驱的顶点输出
  • 从网络中删除该顶点和以它为尾的所有有向边

  之后重复这两个步骤,直到所有的顶点都被输出网络上的顶点都有前驱顶点(出现回路) 为止

  在计算机中的拓扑排序我们利用邻接表存储有向图,并且维护一个储存每个顶点入度的数组,当某个顶点的入度为0时,就输出这个顶点,同时将该顶点的所有后继顶点入度减一,为了避免重复检测入度为0的顶点,我们可以用一个队列/栈存放这些度为0的顶点,在这里我们采取队列实现:

#include <iostream>
#include <queue>
#include <vector>
using namespace std;
constexpr int MAXN = 5e5 + 20;int n, m;
vector<int> G[MAXN];
int in[MAXN]{ 0 };  // 存储每个结点的入度bool toposort()
{vector<int> L;queue<int> S;for (int i = 1; i <= n; i++)if (in[i] == 0) S.push(i);while (!S.empty()) {int u = S.front();S.pop();L.push_back(u);for (const auto& v : G[u]) {if (--in[v] == 0) {S.push(v);}}}if (L.size() == n) {for (const auto& i : L) cout << i << ' ';return true;}else {return false;}
}int main()
{cin >> n;for (int i = 1; i <= n; i++) {int in1{ 0 };do {cin >> in1;if (in1 != 0) {G[i].push_back(in1);in[in1]++;}} while (in1 != 0);}toposort();return 0;
}

  这个过程非常简单:我们首先从邻接表中找出入度为0的顶点,加入队列后,依次取出,把与它相连的入度为0的顶点都入队,然后把它加入输出序列中,一直这么操作,直到队列为空为止,这时候如果输出序列和顶点数相同,则无环,拓扑排序正常,否则其中存在环,无法进行拓扑排序

#3.关键路径和AOE网络

i.什么是AOE网络

  接下来是最后一个问题:如果我们把顶点作为某个状态,而有向边代表活动,则会得到另一种网络:AOE网络(Activity On Edge Network)

  表示实际工程的AOE-网络应该没有有向回路,存在唯一的入度为0的开始顶点,唯一的出度为0的结束顶点

  例如下面这个AOE网络,其中V1表示工程开始,V2表示活动a1完成,V3表示活动a2完成,V4表示活动a3和a4完成,V5表示活动a5和a6完成,此时整个工程就完成了
p64

ii.关键路径

  在AOE网络中,我们主要关心:完成整个工程至少需要多少时间,这个问题其实比较好解决,因为完成整个工程的最小时间是从开始顶点到结束顶点的最长路径长度

  而我们称从开始顶点到结束顶点的最长路径为关键路径,一个AOE网络可以有多条关键路径,关键路径上的所有活动都是关键活动

p64
  例如我们在上图中用到的这个AOE网络,我们可以定义最早开始时间:例如V1作为起点肯定是0,而V2只依赖从V1开始完成活动a1,所以V2最早也要从5才能开始,对应的我们可以得出V3为7;而V4要好好考虑一下,因为V4同时依赖于V2和V3,从V2到V4需要到8,而从V3到V4需要13,因此这时候我们会发现:因为V4依赖于V3,而V3到V4最早也要在13才能达到,所以V4的最早开始时间是13,对应的也可以得到V5的最早开始时间是17

  有最早开始时间,就可以有最晚开始时间,因为我们刚刚发现这个AOE网络中有那么几个活动因为另外的活动影响,中间存在一段时间什么都不用干,这时候我们要倒着推,某个事件的最迟开始时间为所有事件的完成时间,减去从当前事件到事件完成顶点的最长路径长度,因此我们可以知道:V5的最迟开始时间是17,V4的最迟开始时间是13,V3的最迟开始时间是17-max{10, 3}=7,V2的最迟开始时间是10,V1当然是0啦

  你会发现,V2的最迟开始时间是10,最早开始时间是5,这也就意味着,对应V2的活动并没有那么急迫,换而言之:V2对应的活动并不是影响整个工程结束的关键活动

  对于活动,我们同样可以定义活动的最早开始时间和最迟开始时间,对于活动 a i a_i ai,它由边 < j , k > <j,k> <j,k>表示,则最早开始时间就是顶点j的最早开始时间,而最迟开始时间则是顶点k的最迟开始时间减去活动 a i a_i ai的时间,这样一来,我们就可以求得对应各个活动的最早开始时间和最迟开始时间

我们称,最迟开始时间等于最早开始时间的活动为关键活动,所以在求解关键活动的时候,我们需要求到整个AOE网络的四个数组:顶点最早开始时间,顶点最迟开始时间,活动最早开始时间,活动最迟开始时间,下面我会给出一段代码解决这个问题

iii.代码实现

  这里用用一道题作为例子:
p1p2

  参考代码如下:

#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <cstring>
#include <algorithm>
using namespace std;
constexpr int MAXN = 120;struct edge
{int beg;int end;int w;
};vector<edge> e[MAXN];
int in[MAXN]{ 0 };
int ee[MAXN]{ 0 }, le[MAXN]{ 0 };
int xe[MAXN]{ 0 }, l[MAXN]{ 0 };
bool v[MAXN]{ 0 };
vector<edge> all_es;
map<int, edge> ma;
vector<int> ans;bool toposort(int n)
{vector<int> L;queue<int> S;for (int i = 1; i <= n; i++) {if (in[i] == 0) S.push(i);}while (!S.empty()) {int u{ S.front() };S.pop();if (v[u]) continue;v[u] = true;L.push_back(u);for (const auto& v : e[u]) {if (--in[v.end] == 0) S.push(v.end);ee[v.end] = max(ee[v.end], ee[u] + v.w);}}if (L.size() == n) {ans = L;return true;}else {ans = vector<int>{};return false;}
}int main()
{int n{ 0 }, m{ 0 };cin >> n >> m;memset(le, 0x3f, sizeof(le));all_es.push_back({ 0,0,0 });ee[1] = 0, le[n] = n;for (int i = 1; i <= m; i++) {int u, v, w;cin >> u >> v >> w;e[u].push_back({ u, v, w });all_es.push_back({ u,v,w });in[v]++;}bool available{ toposort(n) };if (available) {int mx = 0, mxi = 0;for (int i = 1; i <= n; i++) {if (ee[i] > mx) {mx = ee[i];mxi = i;}}le[mxi] = ee[mxi];for (auto it = ans.rbegin() + 1; it != ans.rend(); it++) {for (const auto& eg : e[*it]) {le[*it] = min(le[*it], le[eg.end] - eg.w);}}for (int i = 1; i <= m; i++) {xe[i] = ee[all_es[i].beg];l[i] = le[all_es[i].end] - all_es[i].w;}vector<edge> all;mx = 0;for (int i = 1; i <= n; i++) {mx = max(mx, ee[i]);}cout << mx << endl;for (int i = 1; i <= m; i++) {if (xe[i] == l[i]) {all.push_back(all_es[i]);}}stable_sort(all.begin(), all.end(),[](const edge& e1, const edge& e2) {if (e1.beg != e2.beg) {return e1.beg < e2.beg;}else {return e1.end > e2.end;}});for (const auto& fe : all) {cout << fe.beg << "->" << fe.end << endl;}}else cout << 0 << endl;return 0;
}

小结

  图论始终是古老而又富有活力的学科,我们在这一篇中只是简单地介绍了一系列关于图的算法和数据结构,还有很多很多关于图的内容,限于篇幅,我没有在这里介绍,在未来有可能会在这个系列中补充。
  到这里,数据结构这个系列的博客的所有课内部分就基本结束了,在之后,我会用几篇博客来单独补充红黑树、B树和B+树等一系列在之前的博客中没有具体讨论的内容,不过最近就要暂时停更了,感谢大家一个学期以来的支持!

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

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

相关文章

【QML COOK】- 002-添加一个图片

1. 编辑main.qml import QtQuickWindow {width: 800height: 800visible: truetitle: qsTr("Hello World")Image {anchors.fill: parentsource: "qrc:/Resources/Images/arrow.png"} }将Window的width和height都改成800&#xff0c;因为我们要添加的图片大…

x-cmd pkg | you-get - web 媒体内容下载工具

目录 简介首次用户功能特点竞品和相关作品进一步阅读 简介 You-Get 是一个开源的命令行小型下载工具&#xff0c;用于从各种网站下载视频、音频和其他媒体文件。 它可以解析和下载嵌套在网页中的媒体&#xff0c;能从 YouTube、优酷、Niconico 、bilibili 等热门网站下载视频、…

用RASP五步轻松保护云端无服务器架构

近年来无服务器架构发展势头正猛&#xff0c;预计未来十年将增长近25%。据称&#xff0c;2022年无服务器架构市场的规模超过了90亿美元&#xff0c;年复合增长率预计将增加。到2032年&#xff0c;市场规模可能超过900亿美元。 这表明&#xff0c;在组织日益采用DevOps的影响下&…

Nvidia Jetson AGX Orin使用CAN与底盘通信(ROS C++ 驱动)

文章目录 一、Nvidia Jetson AGX Orin使用CAN通信1.1 CAN使能配置修改GPIO口功能1.2 can收发测试 二、通过CAN协议编写CAN的SocketCan ROS1驱动程序2.1 通讯协议2.2 接收数据节点2.3 发送数据节点2.4 功能包配置 三、ROS2驱动程序 一、Nvidia Jetson AGX Orin使用CAN通信 参考…

OCS2 入门教程(四)- 机器人示例

系列文章目录 前言 OCS2 包含多个机器人示例。我们在此简要讨论每个示例的主要特点。 System State Dim. Input Dim. Constrained Caching Double Integrator 2 1 No No Cartpole 4 1 Yes No Ballbot 10 3 No No Quadrotor 12 4 No No Mobile Manipul…

逻辑回归(Logistic Regression)

什么是机器学习 逻辑回归&#xff08;Logistic Regression&#xff09;虽然名字中包含"回归"一词&#xff0c;但实际上是一种用于解决分类问题的统计学习方法&#xff0c;而不是回归问题。它是一种线性模型&#xff0c;常用于二分类问题&#xff0c;也可以扩展到多分…

【亲测】申请腾讯云免费服务器流程,2024新版教程

腾讯云免费服务器申请入口 https://curl.qcloud.com/FJhqoVDP 免费服务器可选轻量应用服务器和云服务器CVM&#xff0c;轻量配置可选2核2G3M、2核8G7M和4核8G12M&#xff0c;CVM云服务器可选2核2G3M和2核4G3M配置&#xff0c;腾讯云百科txybk.com分享2024年最新腾讯云免费服务器…

vue3 修饰符大全(近万字长文)

系列文章目录 TypeScript 从入门到进阶专栏 文章目录 系列文章目录前言一、事件修饰符&#xff08;Event Modifiers&#xff09;1、.stop&#xff08;阻止事件冒泡&#xff09;2、.prevent&#xff08;阻止事件的默认行为&#xff09;3、.capture&#xff08;使用事件捕获模式…

如何优雅的搭建一个轻量化的网站

本地网页 这里我找到了一个带有简单的悬停变色效果的个人博客网站模板。用来演示这次的轻量化网站搭建。你可以复制这段代码到一个txt文件中&#xff0c;修改文件后缀名为html即可得到一个最简单的静态网页文件。在没有搭建网站服务器时&#xff0c;本机可以通过直接双击该文件…

YOLOv5改进之---EIoU,SIoU,AlphaIoU,FocalEIoU,Wise-IoU

本文主要针对yolov5的iou损失函数进行改进,主要涵盖EIoU,SIoU,AlphaIoU,FocalEIoU,Wise-IoU这几种损失。 一、首先会对这几种损失进行介绍: 1、背景 由于EIOU是在CIOU的基础上改进的,为方便理解,此处贴出CIOU的计算公式,其他GIOU、DIOU不作介绍。 CIOU公式如下所示: b ,…

C++ 中的指针和引用有什么区别?

C 中的指针和引用有什么区别&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「C的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#…

alibabacloud学习笔记02(小滴课堂)

什么是注册中心和常见的注册中心有哪些 介绍什么是Nacos和搭建实战 启动Nacos 使用前要先安装jdk。 linux学习专栏笔记中有&#xff0c;大家可以去看。 关闭nacos: 这样我们就登录了nacos。 项目集成Nacos实现服务直接的调用 每个子模块都去添加。 给每个子模块配置nacos配…

C++力扣题目--94,144,145二叉树非递归(迭代)遍历

为什么可以用迭代法&#xff08;非递归的方式&#xff09;来实现二叉树的前后中序遍历呢&#xff1f; 我们在栈与队列&#xff1a;匹配问题都是栈的强项 (opens new window)中提到了&#xff0c;递归的实现就是&#xff1a;每一次递归调用都会把函数的局部变量、参数值和返回地…

【python】使用fitz包读取PDF文件报错“ModuleNotFoundError: No module named ‘frontend‘”

【python】使用fitz包读取PDF文件报错“ModuleNotFoundError: No module named ‘frontend’” 正确解决过程 在读取PDF文件时&#xff0c;我使用了fitz包&#xff0c;当使用代码import fitz导入该包时&#xff0c;出现了报错&#xff1a; 于是我直接使用以下代码安装fronten…

每天刷两道题——第十天

1.1和为k的子数组 给你一个整数数组 n u m s nums nums 和一个整数 k k k &#xff0c;请你统计并返回 该数组中和为 k k k 的子数组的个数 。子数组是数组中元素的连续非空序列。 输入&#xff1a;nums [1,2,3], k 3 输出&#xff1a;2 前缀和 1.2如何使用 前缀和的…

stable diffusion 人物高级提示词(五)场景、特效、拍摄手法、风格

一、场景 场景Promptindoor室内outdoor室外cityscape城市景色countryside乡村beach海滩forest森林mountain山脉snowfield雪原skyscraper摩天大楼ancient monument古代遗迹cathedral大教堂library图书馆museum博物馆office building办公大楼restaurant餐厅street market街头市场…

使用openssl 生成pfx格式证书时报错:unable to load certificates

问题现象包如下&#xff1a; 之前在centos上使用openssl部署证书服务器以及颁发证书的时候遇到的问题&#xff0c;在进行个人证书生成之后需要形成pfx格式证书&#xff0c;结果过程中报错了。网上类似资料比较少&#xff0c;做个记录。 生成pfx格式证书的命令&#xff1a; o…

7 种常见的前端安全攻击

文章目录 七种常见的前端攻击1.跨站脚本&#xff08;XSS&#xff09;2.依赖性风险3.跨站请求伪造&#xff08;CSRF&#xff09;4.点击劫持5.CDN篡改6. HTTPS 降级7.中间人攻击 随着 Web 应用程序对业务运营变得越来越重要&#xff0c;它们也成为更有吸引力的网络攻击目标。但不…

想要复制图片的文案要哪些方法呢

从图片中提取文字的需求越来越普遍。无论是从扫描文档、社交媒体截图还是广告海报中提取文字&#xff0c;都能够帮助我们快速获取信息并提高工作效率。为了满足这一需求&#xff0c;市面上涌现出了众多图片文字提取工具。本文将为你介绍一些实用的工具&#xff0c;帮助你快速完…

ENZO LIFE SCIENCE:MEGACD40L®蛋白

MEGACD40L是一种高活性蛋白&#xff0c;其中的两个三聚体CD40配体分子通过Adiponectin&#xff08;脂联素&#xff09;/ACRP30/AdipoQ的胶原结构域人工连接。该蛋白质非常有效地模拟了在体内自然条件下&#xff0c;CD40L的膜辅助聚集&#xff0c;为[CD40L增强剂]的组合&#xf…