本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激。
一、AOV网和AOE网
顶点活动(AOV)网: 顶点表示活动,边集表示活动间的优先关系;AOV网常用来表示活动间的优先关系;
边活动(AOE)网: 顶点表示事件,边集(带权)表示活动,活动一般用时间表示,而事件可以比作中介状态(状态转移->动态规划),当旧活动都结束后才会激活事件以进行新活动;AOE网常用来表示工程的进行过程。
AOV网和AOE网都是有向无环图,且AOV网可以通过将顶点拆成两个顶点,分别表示活动的起点和终点,将两个顶点之间的边表示活动,给定边权即可转化为AOE网。
源点:入度为0的点;
汇点:出度为0的点;
工程的关键要素: 1.工程的持续时间; 2.哪条路径是影响工程持续时间的关键因素(这条路径的变化会引起整个工程时间的变化),即最长路径;
AOE中的最长路径即为关键路径,关键路径上的活动被称为关键活动。
最长路径问题: 寻求图中的最长简单路径(每个顶点只经过一次的路径)。
针对最长路径问题,书中只给出了在有向无环图条件下的求解方式,至于有向有环图和无向图不在讨论范围内。
二、关键路径
求解关键路径实际就是求解有向无环图中(DAG)的最长路径,只是从工程的角度来理解罢了。
求解方式简述:先求点,后夹边;
具体求解思路:
关键活动代表了不能拖延的活动,一旦拖延就会引起整个工程进度推迟;
1.设置数组e[]和l[],其中e[r]和l[r]代表活动ar的最早开始时间和最迟开始时间,当e[r]==l[r]时说明活动ar是关键活动,即活动无法推迟。
2.设置数组ve[]和vl[],其中ve[i]和vl[i]分别代表事件i的最早开始时间和最迟开始时间;
对其中的一截活动分析如下:
3.活动的最早开始时间和最迟开始时间受到事件的影响,影响关系具体表现为:
(1)活动ar的最早开始时间等价于事件Vi的最早开始时间,即e[r] = ve[i];
(2)活动ar的最迟开始时间等价于事件Vj的最迟开始时间减去活动ar的持续时间,即l[r] = vl[j] - w[ar];
4.事件的最早开始时间和最迟开始时间受到旧活动和新活动集合的影响,影响关系具体体现为:
(1)旧活动集合的最早结束时间等价于新事件的最早开始时间,即
(2)新活动集合的最迟开始时间等价于旧事件的最迟开始时间,即
这样事件集合的最早开始时间和最迟开始时间可以由事件本身和活动持续时间推导出来(状态转移),继而可以求出活动的最早开始时间和最迟开始时间。
从上面的步骤可以得到思考:
1.活动的求解依赖于事件,而事件的求解可以通过旧活动和新活动的集合转化而来,这种活动的集合关系又可以转化为事件本身来表示,这种转化形式就是状态转移,即后面将学习的动态规划;
2.如何理解最短和最长?可以用一句话概述:最短是相对的,而最长是绝对的,旧活动集合的最早结束时间就是旧活动中耗时最长的路径。
三、具体求解
在求解旧活动集合的最早结束时间时,为了保证当前访问的事件的前导事件(旧事件)都已经被访问过,采用拓扑排序来实现当前访问事件的最早开始时间求解。而由于寻找前驱事件的方式不直观(其实在领接表的结构体中多加一个变量保存前驱节点就能实现,相当于双向链表那样的实现方式),所以采用在访问到前驱事件时直接更新后继事件的方式实现对ve[]数组的更新。
而若要求解新活动集合的最迟开始时间,可以采用逆拓扑排序来实现当前访问事件的最迟开始时间求解。通过访问当前事件的后继事件来更新当前事件的方式实现对vl[]数组的更新。逆拓扑序列可以将拓扑序列保存到栈中实现。
需要注意的点:ve[]、vl[]数组的初始化,因为这两个数组的求解都是基于他们本身的不同状态而进行的,所以需要注意这两个数组的边界初始化问题,对于ve[],事件的最早开始时间就是0,而对于vl[],事件的最迟开始时间取决于工程的最早结束时间,即最后一个事件(汇点)的最早开始时间。
若事先没有告知汇点信息,则可以通过求ve[]数组的最大值来确定,因为所有事件的最早开始时间的最大值就是工程的最早结束时间,即汇点。而工程的最早结束时间也就是关键活动所花费的时间(边权之和),即关键路径长度。
四、小题练习
1.关键路径长度
完整代码如下:
//其实队列和栈并不是必须的,这里并没有反复使用队列和栈的特性,用数组足够,明确从哪里开始取即可
//使用队列只是因为拓扑排序看起来好像和bfs有点像,但其实并不需要先进先出的思想
//状态转移!!!所以需要注意体会状态的转移过程
#include<cstdio>
#include<queue>
#include<stack>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXN = 100;
struct Node{
int v , w;
};
vector <Node> Adj[MAXN];
int ve[MAXN] = {} , vl[MAXN] = {};
stack <int> topOrder; //记录拓扑排序
int inDegree[MAXN]; //记录顶点入度
int n , m;
int MAX = 0; //记录ve数组最大值,确定汇点
int MAXI = 0;
bool topological_Sort(){
queue <int> q; //记录入度为 0的点
for(int i = 0; i < n; i++){
if(inDegree[i] == 0) q.push(i);
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u); //记录拓扑序列
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i].v;
inDegree[v]--;
if(inDegree[v] == 0) q.push(v);
if(ve[u] + Adj[u][i].w > ve[v]) //更新后继结点,原意是通过前驱结点更新ve[u],但访问前驱节点并不容易实现
ve[v] = ve[u] + Adj[u][i].w; //也可以在领接表的结构体中开一个记录前驱结点的变量
if(ve[v] > MAX) {
MAX = ve[v];
MAXI = v;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
int Critical_Path(){ //关键路径
if(!topological_Sort()) return -1;
fill(vl , vl + n , MAX);
while(!topOrder.empty()){ //逆拓扑排序
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v;
if(vl[v] - Adj[u][i].w < vl[u]) //用u的后继节点来更新vl[u]
vl[u] = vl[v] - Adj[u][i].w; //状态转移
}
}
return ve[MAXI];
}
int main(){
scanf("%d%d", &n , &m);
for(int i = 0; i < m; i++){
int u , v , w;
scanf("%d%d%d", &u , &v , &w);
Node uv = {v , w};
Adj[u].push_back(uv);
inDegree[v]++;
}
int ans = Critical_Path();
if(ans == -1) printf("No");
else {
printf("Yes\n");
printf("%d", ans);
}
}
2.关键活动
关键活动可以通过遍历图的所有边(活动)来获得,通过边的两个端点(事件i和事件j)来求活动的最早开始时间和最迟开始时间。注意字典序输出,需要事先对领接表的出边信息按边的终点大小升序排序;
完整代码如下:文章来源:https://www.toymoban.com/news/detail-766410.html
#include<cstdio>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN = 100;
struct Node{
int v , w;
};
vector <Node> Adj[MAXN];
int vl[MAXN] = {} , ve[MAXN] = {};
stack <int> topOrder;
int inDegree[MAXN];
int n , m;
int MAX = -1, MAXI = -1;
bool cmp(Node a , Node b){
return a.v < b.v;
}
bool topological_Sort(){
queue <int> q;
for(int i = 0; i < n; i++)
if(inDegree[i] == 0) q.push(i);
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u);
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v;
inDegree[v]--;
if(inDegree[v] == 0) q.push(v);
if(ve[u] + Adj[u][i].w > ve[v])
ve[v] = ve[u] + Adj[u][i].w;
if(ve[v] > MAX){
MAX = ve[v];
MAXI = v;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
void Critical_Path(){
if(topological_Sort()) printf("Yes\n");
else {
printf("No");
return;
}
fill(vl , vl + n , MAX);
while(!topOrder.empty()){
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v;
if(vl[v] - Adj[u][i].w < vl[u])
vl[u] = vl[v] - Adj[u][i].w;
}
}
for(int i = 0; i < n; i++) sort(Adj[i].begin() , Adj[i].end() , cmp); //对终点进行排序
for(int u = 0; u < n; u++) { //遍历领接表的所有边,注意不是图的遍历
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v , w = Adj[u][i].w;
int e = ve[u] , l = vl[v] - w; //活动的最早开始时间和最早结束时间
if(e == l) printf("%d %d\n", u , v);
}
}
}
int main(){
scanf("%d%d", &n , &m);
for(int i = 0; i < m; i++){
int u , v , w;
scanf("%d%d%d", &u , &v , &w);
Node uv = {v , w};
Adj[u].push_back(uv);
inDegree[v]++;
}
Critical_Path();
}
3.关键路径
将遍历得到的关键路径以图的形式保存在一个新的领接表中,最后通过DFS遍历所有关键路径。还是要注意字典序输出;
完整代码如下:文章来源地址https://www.toymoban.com/news/detail-766410.html
//源点不唯一,汇点也不唯一,对于程序求解,不需要注意超级源点或者超级汇点
#include<cstdio>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN = 100;
struct Node{
int v , w;
};
vector <Node> Adj[MAXN];
vector <int> ansAdj[MAXN];
int vl[MAXN] = {} , ve[MAXN] = {};
stack <int> topOrder;
int inDegree[MAXN] = {};
int tempinDegree[MAXN] = {};
int n , m;
int MAX = -1; //记录汇点
bool cmp(int a , int b){
return a < b;
}
bool topological_Sort(){
queue <int> q;
for(int i = 0; i < n; i++)
if(inDegree[i] == 0) q.push(i);
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u);
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v;
inDegree[v]--;
if(inDegree[v] == 0) q.push(v);
if(ve[u] + Adj[u][i].w > ve[v])
ve[v] = ve[u] + Adj[u][i].w;
if(ve[v] > MAX){ //更新汇点
MAX = ve[v];
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
bool Critical_Path(){
if(!topological_Sort()) return false;
fill(vl , vl + n , MAX);
while(!topOrder.empty()){
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v;
if(vl[v] - Adj[u][i].w < vl[u])
vl[u] = vl[v] - Adj[u][i].w;
}
}
for(int u = 0; u < n; u++) { //遍历领接表的所有边,注意不是图的遍历
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v , w = Adj[u][i].w;
int e = ve[u] , l = vl[v] - w; //活动的最早开始时间和最早结束时间
if(e == l) ansAdj[u].push_back(v); //领接表存储
}
}
return true;
}
vector <int> Path; //有向无环图不需要散列表记录访问过的结点,因为肯定不存在回路
void DFS(int s){ //由于只有一个源点和汇点,所以一次DFS即可遍历全部关键路径
Path.push_back(s);
if(Adj[s].size() == 0) {
for(int i = 0; i < Path.size(); i++){
printf("%d", Path[i]);
if(i + 1 < Path.size()) printf("->");
}
printf("\n");
}
sort(ansAdj[s].begin() , ansAdj[s].end() , cmp);
for(int i = 0; i < ansAdj[s].size(); i++){
int v = ansAdj[s][i];
DFS(v);
}
Path.pop_back();
}
int main(){
scanf("%d%d", &n , &m);
for(int i = 0; i < m; i++){
int u , v , w;
scanf("%d%d%d", &u , &v , &w);
Node uv = {v , w};
Adj[u].push_back(uv);
inDegree[v]++;
tempinDegree[v]++;
}
if(!Critical_Path()) printf("No");
else {
printf("Yes\n");
for(int origin = 0; origin < n; origin++) {
if(tempinDegree[origin] == 0 && ansAdj[origin].size() != 0) DFS(origin);
}
}
}
到了这里,关于关键路径(算法笔记)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!