算法理论
AOV网与AOE网
AOV
网(Activity On Vertex):顶点表示活动,边表示活动间的优先关系的有向无环图;AOE
网(Activity On Edge):带权的边表示活动,而用顶点表示事件的有向无环图;边权表示完成活动需要的时间;AOE
网中的最长路径被称为关键路径,关键路径上的活动称为关键活动,关键活动会影响整个工程的进度- 理解:关键路径的定义是
AOE
网中的最长路径,为什么其长度会等于整个工程的最短完成时间呢?
**A:**从时间的角度上看,不能拖延的活动严格按照时间表所达到的就是最短时间;而从路径长度的角度上看,关键路径选择的总是最长的道路。
最长路径
- 对一个没有正环的图(指从源点可达的正环),如果需要求最长路径长度,则可以把所有边的边权乘以
-1
,然后使用Bellman-Ford
算法或SPFA
算法求最短路径,将所得结果取反即可; - 如果图中有正环,那么最长路径是不存在;但如果要求最长简单路径(每个顶点最多只经过一次),那么虽然最长简单路径存在,却无法通过
Bellman-Ford
等算法得到,原因是最长路径问题是NP-Hard
问题(即没有多项式时间复杂度算法可解决); - 如果求有向无环图的最长路径长度,关键路径的求法比上面取反用
SPFA
等算法更快;
关键路径
思路:即求解DAG
(有向无环图)中最长路径的方法 - 先求点,再夹边
- 1、按照拓扑序和逆拓扑序分别计算个顶点(事件)的最早发生时间和最迟发生时间:
- 最早(拓扑序):
ve[j] = max { ve[i] + length[i->j] }
(j
的所有入边) - 最迟(逆拓扑序):
vl[i] = min { vl[j] - length[i->j] }
(i
的所有出边)
- 最早(拓扑序):
- 2、用上面的计算结果计算各边(活动)的最早开始时间和最迟开始时间:
- 最早:
e[i->j] = ve[j]
- 最迟:
l[i->j] = vl[j] - length[i->j]
- 最早:
- 3、
e[i->j] == l[i->j]
的边(活动)即为关键活动
代码实现
适用于 汇点确定且唯一 的情况,以n-1
号顶点为汇点为例;
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn = 10010;
struct edge{
int v; //终点
int w; //边权
};
vector<edge> Adj[maxn]; //邻接表(边带权值)
int in_degree[maxn]; //入度
int N; // 顶点数
stack<int> topOrder; //栈 保存拓扑序列
int ve[maxn]; //结点的最早发生时间
int vl[maxn]; //结点的最迟发生时间
//拓扑排序 顺便求ve数组
bool topLocgicalSort() {
queue<int> Q;
for(int i = 0; i < N; i++) { //所有入度为0的结点入队
if(in_degree[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; //u的后集结点v
in_degree[v]--; //结点v的入度-1
if(in_degree[v] == 0) //入度减为0时入队
Q.push(v);
//用ve[u]来更新u的所有后继结点
if(ve[u] + Adj[u][i].w > ve[v]) { //选最大值
ve[v] = ve[u] + Adj[u][i].w;
}
}
}//while
if(topOrder.size() == N) return true; //拓扑排序成功
else return false; //失败
}
//颠倒拓扑序列得到一组合法的逆拓扑序列,求vl数组
void get_vl() {
while(!topOrder.empty()) {
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i].v;
//用u的所有后继结点v的vl来更新vl[u]
if(vl[v] - Adj[u][i].w < vl[u]) { //选最小值
vl[u] = vl[v] - Adj[u][i].w;
}
}//for-i
}//while
}//get_vl
//关键路径
//使用动态规划可以更简洁地求解关键路径(11.6节)
int CriticalPath() {
memset(ve, 0, sizeof(ve)); //ve数组初始化为0
if(topLocgicalSort() == false) { //计算ve[]值
return -1; //非有向无环图
}
fill(vl, vl + maxn, ve[N - 1]); //初始化vl数组,值为终点(汇点)的ve值
get_vl(); //计算vl[]值
//遍历邻接表的所有边(代表活动),计算活动的最早开始时间e和最迟开始是按l
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;
//计算活动(边)的最早开始时间e和最迟开始时间l
int e = ve[u], l = vl[v] - w;
if(e == l) { //若e==l,则u->v为关键活动
printf("%d->%d\n", u, v);
//如果需要完整输出关键路径,保存即可(建一个邻接表,保存u->v)
}
}//for - i
}//for - u
return ve[N -1]; //返回关键路径长度
}//CriticalPath
1、如果实现不知道汇点编号,如何比较快地获取关键路径长度?
取 ve[]
数组中的最大值为汇点即可;
原因:ve[]
数组的含义是时间的最早开始时间,因此所有事件中最大的一定是最后一个(或多个)时间,即汇点;
代码操作: 在fill()
函数之前添加一段语句,改变vl[]
函数初始值即可;
int maxLength = 0;
for (int i = 0; i < n; i++) { //找到ve[]中的最大值
if (ve[i] > maxLength) maxLength = ve[i];
}
fill (vl, vl + n, maxLength);
- 2、如果想输出完整路径,就需要把关键活动存下来;
方法是新建一个邻接表,当确定u->v
是关键活动时,将其加入邻接表,这样最后生成的就是所有关键路径合成的图,最后可以用DFS
遍历来获取所有关键路径。(即可能有多条路径) - 3、使用动态规划的做法可以更简洁地求解关键路径,补充完成后会加入链接!!
应用练习
王道P218(清华大学)
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <map>
#include <stack>
#include <queue>
using namespace std;
const int maxn = 505;
int n, m;
int INF = 1e8;
int inDegrees[maxn];
vector<int> Adj[maxn];
int weight[maxn];
stack<int> topSortStack;
int fi[maxn];
int gi[maxn];
void topSort() {
queue<int> pq;
for(int i = 1; i <= n; i++){
if(inDegrees[i] == 0){
pq.push(i);
}
}
while(!pq.empty()){
int top = pq.front();
topSortStack.push(top);
pq.pop();
for(int i = 0; i < Adj[top].size(); i++){
int v = Adj[top][i];
inDegrees[v]--;
fi[v] = max(fi[v], fi[top] + weight[top]);
if(inDegrees[v] == 0){
pq.push(v);
}
}
}
}
int main() {
cin >> n >> m;
fill(fi, fi+maxn, 0);
fill(inDegrees, inDegrees + maxn, 0);
for(int i = 1; i <= n; i++){
cin >> weight[i];
}
int a, b;
for(int i = 0; i < m; i++){
cin >> a >> b;
inDegrees[b]++;
Adj[a].push_back(b);
}
topSort();
int max = -1;
for(int i = 1; i <= n; i++){
if(fi[i] + weight[i]> max){
max = fi[i] + weight[i];
}
}
while(!topSortStack.empty()){
int top = topSortStack.top();
topSortStack.pop();
if(Adj[top].size() == 0){
gi[top] = max - weight[top];
}else{
gi[top] = INF;
}
for(int i = 0; i < Adj[top].size(); i++){
int v = Adj[top][i];
gi[top] = min(gi[top], gi[v] - weight[top]);
}
}
max = -1;
for(int i = 1; i <= n; i++){
if(gi[i] + weight[i] > max){
max = gi[i] + weight[i];
}
}
long long result = 1;
long long constant = 1000000000 + 7;
for(int i = 1; i <= n; i++){
result = (result * (gi[i] - fi[i] + 1)) % (constant);
}
cout << max << endl;
cout << result << endl;
return 0;
}