数据结构—图(下)

news2024/9/20 6:32:28

文章目录

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

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

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

相关文章

(Matlab)基于CNN-Bi_LSTM的多维时序回归预测(卷积神经网络-双向长短期记忆网络)

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、部分代码展示&#xff1a; 四、完整代码数据下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平…

鹦鹉目标检测数据集VOC格式600张

鹦鹉&#xff0c;一种色彩鲜艳、聪明伶俐的鸟类&#xff0c;以其模仿人类语言的能力和独特的喙形而广受喜爱。 鹦鹉属于鸟纲、鹦鹉科&#xff0c;是热带和亚热带地区的常见鸟类。它们的喙弯曲呈钩状&#xff0c;非常适合啄食种子、果实和坚果等食物。鹦鹉的羽毛通常非常鲜艳&a…

精进单元测试技能——Pytest断言的艺术

本篇文章主要是阐述Pytest在断言方面的应用。让大家能够了解和掌握Pytest针对断言设计了多种功能以适应在不同测试场景上使用。 了解断言的基础 在Pytest中&#xff0c;断言是通过 assert 语句来实现的。简单的断言通常用于验证预期值和实际值是否相等&#xff0c;例如&#xf…

腾讯云域名外部入库流程

注册商是腾讯云&#xff0c;且在腾讯云管理的&#xff0c;请使用此教程外部入库。 如您的域名注册商是腾讯云但在聚名管理&#xff0c;请参考教程&#xff1a;https://www.west.cn/faq/list.asp?unid2539 在外部入库操作之前&#xff0c;请先登录腾讯云获取账号ID信息。…

人类认知中的等价机理与机器智能中等价机理

人类认知中的等价机理是指人们在认知过程中&#xff0c;通过将不同的概念、事物或情境进行等价替代&#xff0c;从而实现思维的连贯和理解的补充。例如&#xff0c;当人们看到一个陌生的动物时&#xff0c;可以将它与已知的动物进行等价对应&#xff0c;从而理解其特征和行为。…

社交心不死:支付宝内测兴趣社交

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 支付宝又双叒做社交了&#xff0c;这次的“兴趣社区”能成吗&#xff1f; 支付宝做社交心不死&#xff0c;近期支付宝又开始内测名为“兴趣社区”的功能。主打找同频玩伴&#xff0c;徒步、骑行、钓鱼&#xff0c…

将本地文件传到远程仓库

工具&#xff1a;gitee、visual studio 2022 语言&#xff1a;C语言 1、在创建项目的时候&#xff0c;将项目存储于自己创建的仓库的本地存储位置&#xff0c;并编写代码 2、先选择仓库&#xff0c;然后单击鼠标右键——>TortoiseGit——>Add..&#xff08;将代码添加到…

Spring 见解 6 Spring事务控制

Spring事务控制 事务介绍 什么是事务&#xff1f; 当你需要一次执行多条SQL语句时&#xff0c;可以使用事务。通俗一点说&#xff0c;如果这几条SQL语句全部执行成功&#xff0c;则才对数据库进行一次更新&#xff0c;如果有一条SQL语句执行失败&#xff0c;则这几条SQL语句…

Android Studio安卓读写NFC Ntag标签源码

本示例使用的发卡器&#xff1a; https://item.taobao.com/item.htm?spma1z10.5-c-s.w4002-21818769070.11.3513789erHXVGx&id615391857885 <?xml version"1.0" encoding"utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout x…

5.2 Android BCC环境搭建(adeb版,下)更新中

五&#xff0c;运行adeb shell adeb shell 六&#xff0c;其他指令 6.1 从开发机移除adeb adeb remove 6.2 更新开发机adeb deb git-pull 6.3 使用ssh替代adeb进行与android设备的通信 adeb --ssh <uri> --sshpass <pass> <cmd> 七&#xff0c;BCC配置…

项目知识—SSM及之后02

1、resultMap写的Base内容必须保证select都使用上 2、VALUE单个 &#xff0c;VALUES多个 3、 一对多&#xff0c;两张表&#xff0c;多的表加外键 比如班级和学生就是一对多&#xff0c;查就是按照学生表去查询 多对多&#xff0c;三张表&#xff0c;关系表加外键 4、数据…

《PCI Express体系结构导读》随记 —— 第I篇 第2章 PCI总线的桥与配置(11)

接前一篇文章&#xff1a;《PCI Express体系结构导读》随记 —— 第I篇 第2章 PCI总线的桥与配置&#xff08;10&#xff09; 2.3 PCI桥与PCI设备的配置空间 PCI设备都有独立的配置空间&#xff0c;HOST主桥通过配置读写总线事务访问这段空间。PCI总线规定了三种类型的PCI配置…

在当前bash(sh)中执行脚本和注册函数

在研究《管理Python虚拟环境的脚本》时&#xff0c;我们使用了source指令而没有使用sh或者bash来执行脚本&#xff0c;就是因为source指令可以让脚本在当前bash(sh)中执行&#xff1b;而sh或者bash则会新启动一个bash来执行。 我们可以通过下面这个脚本做测试 # test.sh # 用…

上海亚商投顾:沪指再度失守2900点 全市场超4800只个股下跌

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日继续调整&#xff0c;沪指跌超1%再度失守2900点&#xff0c;深成指、创业板指均创出调整新低&…

图文详解:CRM系统的功能有哪些?

在如今多数行业市场充分竞争的情况下&#xff0c;同质化产品层出不穷&#xff0c;什么样的企业可以在市场中屹立不倒&#xff1f;必然是拥有广泛稳定的客户群体的企业&#xff0c;所以强大的客户关系管理能力就成为了企业的核心竞争力。 在SaaS发展到今日&#xff0c;基于云服…

MySQL之子查询、连接查询(内外)以及分页查询(实操)

文章目录 前言一、SQL脚本二、实操以及实现思路 前言 续上篇博主MySQL之视图&索引&执行计划这篇给大家讲解MySQL之子查询、连接查询(内&外)以及分页查询 一、SQL脚本 /*Navicat Premium Data TransferSource Server : localhostSource Server Type :…

前端JS加密对抗由浅入深-2

前言&#xff1a; 本文主要讲解&#xff0c;针对前端非对称、多段加密数据传输站点&#xff0c;如何进行动态调试&#xff0c;如何进行安全测试。本次讲解不涉及任何漏洞方面&#xff0c;仅为学习探讨&#xff0c;该站点现已经更改加密方式&#xff0c;严禁非法测试&#xff0…

基于SSM的校园线上订餐系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;vue\html 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是…

振弦采集仪在桥梁结构监测中的应用研究

振弦采集仪在桥梁结构监测中的应用研究 振弦采集仪在桥梁结构监测中的应用研究是指利用振弦采集仪对桥梁结构进行监测和数据采集的研究。振弦采集仪是一种能够实时监测桥梁结构振动情况的设备&#xff0c;通过采集桥梁上的振弦传感器获取振动数据&#xff0c;并将数据传输到监…

ubuntu系统(9):ubuntu 20.02安装pydot

目录 警告信息 1、确保安装了Python和pip 2、安装Graphviz软件包 3、pip安装pydot 验证 在gem5中&#xff0c;pydot库用于生成图形化输出&#xff0c;特别是生成.dot文件和相关的图像文件&#xff0c;如PDF、PNG等。它与gem5结合使用的一个常见用途是生成系统结构图、内存…