数据结构—图(下)

文章目录

  • 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;
    else
        cout << "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+树等一系列在之前的博客中没有具体讨论的内容,不过最近就要暂时停更了,感谢大家一个学期以来的支持!

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

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

相关文章

盘点三款服务器运维工具

随着世界变得更加数字化&#xff0c;如何便捷高效地管理服务器变得越来越重要&#xff0c;能有一款简易实用且现代化服务器管理工具就显得尤为关键。今天就选取了三款服务器运维工具进行对比分析&#xff0c;评测每款产品的优缺点。 产品清单 宝塔面板 简介&#xff1a;国内老…

工作流自动化:它是什么,常见示例以及如何实现

由于您的组织旨在留住顶尖人才和高价值客户&#xff0c;因此您需要不断为这两个团队提供一流的体验。 就客户而言&#xff0c;它可以实时解决他们的问题和疑虑&#xff0c;并以深思熟虑、可操作的洞察力主动与他们联系&#xff1b;而且&#xff0c;对于员工来说&#xff0c;它可…

推荐一款强大的AI开源项目!有了它,将你的数据库秒变AI数据库!

前言 在当今数字化的世界中&#xff0c;数据库系统扮演着至关重要的角色。而原生系统的功能我们也大都知晓&#xff0c;无非是一些增删改查、数据优化的使用。但有一些开源工具项目可以帮助我们对数据库降本增效。 在本文中&#xff0c;小编将介绍一个名为SuperDuperDB的开源…

构建多种样式的弹窗(案例)

介绍 本篇Codelab将介绍如何使用弹窗功能&#xff0c;实现四种类型弹窗。分别是&#xff1a;警告弹窗、自定义弹窗、日期滑动选择器弹窗、文本滑动选择器弹窗。需要完成以下功能&#xff1a; 点击左上角返回按钮展示警告弹窗。点击出生日期展示日期滑动选择器弹窗。点击性别展示…

树莓派4B使用ncnn部署yolov5-Lite,推理耗时 247ms 包含前后处理

一. 引言 最近在玩树莓派&#xff0c;想在树莓派上不是一个目标检测算法&#xff0c;大致看了一下&#xff0c;目前开源的大家都在使用yolov5-Lite&#xff0c;使用ncnn去推理加速&#xff0c;于是自己也尝试部署&#xff0c;在此记录一下&#xff0c;个人踩的坑。 二. 版本选…

后端 API 接口文档 Swagger 使用

Swagger 是什么 swagger是一款可以根据 restful 风格生成的接口开发文档&#xff0c;并且支持做测试的一款中间软件。 例如当我们在开发前后端分离项目时&#xff0c;当后端开发完一个功能想要测试时&#xff0c;若此时还没有相应的前端页面发起请求&#xff0c;可以通过 swag…

java回溯算法、最短路径算法、最小生成树算法

回溯算法 回溯算法实际上一个类似枚举的搜索尝试过程&#xff0c;主要是在搜索尝试过程中寻找问题的解&#xff0c;当发现已不满足求解条件时&#xff0c;就“回溯”返回&#xff0c;尝试别的路径。 最短路径算法 从某顶点出发&#xff0c;沿图的边到达另一顶点所经过的路径中…

【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;因为我们要添加的图片大…

Spring AOP概念

什么是 AOP &#xff1f; AOP 为 Aspect Oriented Programming 的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续&#xff0c;是软件开发中的一个热点&#xff0c;也是 Spring …

Mac 环境多JDK安装与切换

一、下载jdk 去Oracle官网上下载想要安装的jdk版本&#xff0c;M芯片选择arm架构的.bmg格式的文件。 https://www.oracle.com/java/technologies/downloads/。 二、安装jdk 2.1 双击下载的文件&#xff0c;安装步骤一步步点继续就好。 2.2 安装完成后会在/Library/Java/JavaV…

常见测试技术都有哪些?

测试技术是用于评估系统或组件的方法&#xff0c;目的是发现它是否满足给定的要求。系统测试有助于识别缺口、错误&#xff0c;或与实际需求不同的任何类型的缺失需求。测试技术是测试团队根据给定的需求评估已开发软件所使用的最佳实践。这些技术可以确保产品或软件的整体质量…

2024年甘肃省职业院校技能大赛 高职学生组电子与信息大类信息安全管理与评估赛项样题卷①

2024年甘肃省职业院校技能大赛 高职学生组电子与信息大类信息安全管理与评估赛项样题 第一阶段&#xff1a;第二阶段&#xff1a;模块二 网络安全事件响应、数字取证调查、应用程序安全第二阶段 网络安全事件响应第一部分 网络安全事件响应第二部分 数字取证调查第三部分 应用程…

网络通信(12)-C#TCP客户端封装帮助类实例

本文使用Socket在C#语言环境下完成TCP客户端封装帮助类的实例。 实例完成的功能: 客户端与服务器连接,实现实时刷新状态。 客户端接收服务器的数据。 客户端发送给服务器的数据。 客户端实时判定状态,断开连接后自动重连。 客户端与服务器端发送心跳包。 在VS中创建C…

电商引流模式:广告电商

广告电商模式是一种将广告收入与电商业务相结合的商业模式。在这种模式下&#xff0c;电商企业通过向消费者提供免费或低价的商品或服务&#xff0c;吸引大量用户关注和参与。同时&#xff0c;电商企业通过与广告主合作&#xff0c;将广告投放到自己的平台上&#xff0c;通过广…

接口工具Apifox

最近发现一款接口测试工具--apifox&#xff0c;我我们很难将它描述为一款接口管理工具 或 接口自测试工具。 官方给了一个简单的公式&#xff0c;更能说明apifox可以做什么。 Apifox Postman Swagger Mock JMeter Apifox的特点&#xff1a; 接口文档定义&#xff1a; Apif…

1.8.。。

1 有道云笔记 2 #include "mywidget.h"myWidget::myWidget(QWidget *parent): QWidget(parent) {//设置窗口大小&#xff0c;背景颜色&#xff0c;纯净窗口this->setFixedSize(700,500);this->setStyleSheet("background-color:white");this->…

Dockerfile基本结构及编写详解

文章目录 1 Dockerfile1.1 Dockerfile的基本结构1.2 Dockerfile文件说明1.3 Dockerfile常见命令1.4 build命令1.5 部署微服务1.6 docker-compose部署 1 Dockerfile ​ Dockerfile其实就是我们用来构建Docker镜像的源码&#xff0c;当然这不是所谓的编程源码&#xff0c;而是一…

深入了解Pytest中的Mocking:简化测试,避免依赖问题!

在软件开发中&#xff0c;测试是确保代码质量的关键步骤之一。而在测试中&#xff0c;经常需要模拟&#xff08;Mock&#xff09;一些对象或函数&#xff0c;以确保测试的独立性和可靠性。在Pytest中&#xff0c;Mocking是一个强大的工具&#xff0c;能够简化测试过程&#xff…

【Docker】部署mysql 和 tomcat

目录 部署MySQL 1.搜索镜像 2. 拉取镜像 部署Tomcat 1. 搜索镜像 2.拉取镜像 3.查看镜像 部署MySQL 1.搜索镜像 docker search mysql 2. 拉取镜像 通过mysql 镜像创建对应的容器&#xff0c;并设置端口映射&#xff0c;目录映射 创建mysql 的目录 docker run -id \ …

【PostgreSQL】在DBeaver中实现序列、函数、视图、触发器设计

【PostgreSQL】在DBeaver中实现序列、函数、触发器、视图设计 基本配置一、序列1.1、序列使用1.1.1、设置字段为主键&#xff0c;数据类型默认整型1.1.2、自定义序列&#xff0c;数据类型自定义 1.2、序列延申1.2.1、理论1.2.2、测试1.2.3、小结 二、函数2.1、SQL直接创建2.1.1…