图论之关键路径讲解
回顾所需知识:
拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。
通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。
AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
拓扑排序的实现步骤※
- 在有向图中选一个没有前驱的顶点并且输出
- 从图中删除该顶点和所有以它为尾的弧 (白话就是:删除所有和它有关的边)
- 重复上述两步,直至所有顶点输出,循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。
因此,也可以通过拓扑排序来判断一个图是否有环。
AOE网介绍
AOE-网只是比AOV-网多了一个边的权重,而且AOV-网一般是设计一个庞大的工程各个子工程实施的先后顺序,而我们的AOE-网就是不仅仅关系整个工程中各个子工程的实施的先后顺序,同时也关系整个工程完成最短时间。
通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键
AOE网特点
AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题:
1、完成整个工程需要的时间
2、哪些活动是影响工程进度的关键
关键路径介绍
关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)
关键活动:
假设起点是vo,则我们称从v0到vi的最长路径的长度为vi的最早发生时间
同时,vi的最早发生时间也是所有以vi为尾的弧所表示的活动的最早开始时间
使用e(i)表示活动ai最早发生时间,除此之外,我们还定义了一个活动最迟发生时间,使用l(i)表示,不推迟工期的最晚开工时间。我们把e(i)=l(i)的活动ai称为关键活动,因此,这个条件就是我们求一个AOE-网的关键路径的关键所在了。
求关键路径的步骤
- 输入顶点数和边数,已经各个弧的信息建立图
- 从源点v1出发,令ve[0]=0;按照拓扑序列往前求各个顶点的ve。如果得到的拓扑序列个数小于网的顶点数n,说明我们建立的图有环,无关键路径,直接结束程序
- 从终点vn出发,令vl[n-1]=ve[n-1],按逆拓扑序列,往后求其他顶点vl值
- 根据各个顶点的ve和vl求每个弧的e(i)和l(i),如果满足e(i)=l(i),说明是关键活动。
代码实现
例题
SDUT2498
#include<bits/stdc++.h>
using namespace std;
const int M = 5e4 + 5;
struct N {
int in, out, weight;
}edge[M]; //用结构体表示边
int node[M]; //记录经过的节点
int distant[M]; //记录最长路径
int degree_in[M]; //记录每个点的入度 目的是寻找源点(没有入度的点)
int sourcepoint; //源点
void bellman(int point_number, int edge_number) {
memset(node, 0, sizeof(node));
memset(distant, 0, sizeof(distant));
for (int k = 2; k <= point_number; k++) { //除去源点,只需要进行(点的数量-1)次求值
int flag = 0; //每次找下一个点的时候都会重置flag
for (int i = 1; i <= edge_number; i++) { //动态规划,找K次,每次都更新源点的权值,最后源点的权值最大,输出的就是最大路径
if ((distant[edge[i].in] < distant[edge[i].out] + edge[i].weight) || ((distant[edge[i].in] == distant[edge[i].out] + edge[i].weight) && (edge[i].out < node[edge[i].in]))) {
distant[edge[i].in] = distant[edge[i].out] + edge[i].weight; //把权值放到每条边的起点上,最后输出源点的权值
node[edge[i].in] = edge[i].out;
flag = 1;
}
}
if (flag == 0) { //如果flag没更新,说明循环每条边的时候找不到比原来的权值更大的路径了,跳出循环即可
break; //这时候源点处已经找到了最大权值
}
}
printf("%d\n", distant[sourcepoint]); //跳出循环后,输出最终结果
while (node[sourcepoint] != 0) {
printf("%d %d\n", sourcepoint, node[sourcepoint]);
sourcepoint = node[sourcepoint];
}
}
int main() {
int point_number, edge_number;
int sv, ev, w;
while (~scanf("%d%d", &point_number, &edge_number)) {
memset(edge, 0, sizeof(edge)); //多组数据输入时 初始化很重要
memset(degree_in, 0, sizeof(degree_in));
for (int i = 1; i <= edge_number; i++) {
scanf("%d%d%d", &sv, &ev, &w);
edge[i].in = sv; //每条边的入度、出度、权重
edge[i].out = ev;
edge[i].weight = w;
degree_in[ev]++; //每个点的入度
}
for (int i = 1; i <= point_number; i++) { //寻找源点
if (degree_in[i] == 0) { //入度为零的点为源点
sourcepoint = i;
}
}
bellman(point_number, edge_number);
}
return 0;
}
看不懂的可以自取视频理解,多看两遍就差不多了