数据结构学习(二):AOE算法求解关键路径

AOE算法求解关键路径

问题描述:

输入顶点和边,得到有向图,求出关键路径。

.

关键路径通常(但并非总是)是决定项目工期的进度活动序列。它是项目中最长的路径,即使很小浮动也可能直接影响整个项目的最早完成时间。关键路径的工期决定了整个项目的工期,任何关键路径上的终端元素的延迟在浮动时间为零或负数时将直接影响项目的预期完成时间(例如在关键路径上没有浮动时间)。 但特殊情况下,如果总浮动时间大于零,则有可能不会影响项目整体进度。一个项目可以有多个、并行的关键路径。另一个总工期比关键路径的总工期略少的一条并行路径被称为次关键路径。—-维基百科

关键路径的求解在工程项目中的运用是十分重要的,而在数据结构课程程里,老师讲的最多的是使用AOE算法来对其进行求解。在这个程序中,数据使用数组链表存储,对节点进行操作,完成关键路径的查找。
.

算法设计及分析

整个程序包括两部分:main函数和头文件,而头文件里是封装好的类定义函数、构造函数以及init()、read_graph()、toposort()、CriticalPath()、read_case()等五个函数。

Main函数中主要是使用aoe来调用头文件中的方法,循环读取输入的节点数和边数,来寻找关键路径并输出。主要功能是实现全在头文件中。
头文件使用类封装,包括了输入路径、读取路径、拓扑排序以及计算关键路径并输出等函数。其中,定义class AOE及构造函数struct Edge,以下所有函数的参数全部被包括在内。

public:
        AOE();
        void init();
        void read_graph(int u, int v, int w, int id);
        void toposort();
        void CriticalPath();
        void read_case();
        void get(int p,int q);
    private:
        int n, m; //顶点个数,边条数 
        int v,w,id,u;
        int cnt; 
        int first[MAXN], topo[MAXN]; //表头指针 
        int ind[MAXN], outd[MAXN]; //顶点入度,出度 
        int tot; 
        int Ee[MAXN], El[MAXN], E[MAXN], L[MAXN]; 

其中,Ee表示事件最早可能发生时间,El表示事件最迟允许发生时间,E表示活动最早可能发生时间,L表示活动最迟允许发生时间。

更写了一个get()函数,用于接收从main函数中输入的参数,在头文件内的函数中进行计算:

template<class T>
void AOE<T>::get(int p,int q)
{
    n=p;
    m=q;
}

init():init()函数的作用是将目标数组中的数据替换为特定数据。其中调用了memset()函数,这个一个char型初始化函数,需要头文件string.h 或 memory.h,函数原型:void *memset(void *s , int ch , size_t n ),memset(结构体/数组名 , “用于替换的字符“ , 前n个字符 );函数解释:将s中的前n个字节用ch替换并且返回s,函数作用:在一段内存块中填充某一个给定的值,常用于较大的对结构体和数组的清零操作。

read_graph():这个函数的作用就是对每条边进行再定义。

template<class T>
void AOE<T>::read_graph(int u, int v, int w, int id) //int u, int v, int w, int id
{ 
    edge[cnt].v = v, edge[cnt].w = w, edge[cnt].id = id; 
    edge[cnt].next = first[u], first[u] = cnt++; 
} 

toposort():这个函数就是进行拓扑排序,首先,若(u,v)∈E(e),则 u在线性序列中出现在v之前,每次从该集合中取出(没有特殊的取出规则,随机取出也行,使用队列/栈也行,下同)一个顶点,将该顶点放入保存结果的List中。

紧接着循环遍历由该顶点引出的所有边,从图中移除这条边,同时获取该边的另外一个顶点,如果该顶点的入度在减去本条边之后为0,那么也将这个顶点放到入度为0的集合中。然后继续从集合中取出一个顶点…………

当集合为空之后,检查图中是否还存在任何边,如果存在的话,说明图中至少存在一条环路。不存在的话则返回结果List,此List中的顺序就是对图进行拓扑排序的结果。

template<class T>
void AOE<T>::toposort() //拓扑排序  
{                       //(u,v)∈E(e)  u在线性序列中出现在v之前 
    queue<int> q; 
    for(int i = 0; i < n; i++) if(!ind[i]) q.push(i); //在有向图中选一个入度为0的顶点,并输出 
    while(!q.empty())          //重复操作,直至图中不含顶点 
    {                          //若图中仍含顶点,所有顶点入度均不为0,则该图中有环 
        int x = q.front(); 
        q.pop(); 
        topo[++tot] = x; 
        for(int e = first[x]; e != -1; e = edge[e].next) //删除所有与其有关的边 
        { 
            int v = edge[e].v, w = edge[e].w; 
            if(--ind[v] == 0) q.push(v); 
            if(Ee[v] < Ee[x] + w) //求出各个顶点Ee值  
            { 
                Ee[v] = Ee[x] + w; 
            } 
        } 
    } 
} 

CriticalPath():这个函数的作用是求关键路径。求关键路径的算法思想是:
1、在结点的定义中增加Ee和El 字段用于记录个事件的最早开始时间和最迟开始时间,同时得到关键路径和最小工期
2、邻接矩阵中使用边权代替原来连接标志
3、进行拓扑排序,形成拓扑序列
4、按拓扑序列顺序,从前向后搜索寻找个活动(即边),若存在该活动,则计算相应事件的最早开始时间。
5、按拓扑序列顺序,从后向前搜索寻找个活动(即边),若存在该活动,则计算相应事件的最迟开始时间。
6、计算各活动的最早开始时间和最迟开始时间。

//找关键路径并输出 
template<class T>
void AOE<T>::CriticalPath() 
{ 
    toposort(); 
    int top = tot; 
    for(int i = 0; i < n; i++) El[i] = Ee[n-1]; //初始化顶点事件的最迟发生时间  
    while(top) //逆拓扑排序求顶点El的值  
    { 
        int x = topo[top--]; 
        for(int e = first[x]; e != -1; e = edge[e].next) 
        { 
            int v = edge[e].v, w = edge[e].w; 
            if(El[x] > El[v] - w) 
            { 
                El[x] = El[v] - w; 
            } 
        } 
    } 
    for(int u = 0; u < n; u++) //求出E,L关键活动  
    { 
        for(int e = first[u]; e != -1; e = edge[e].next) 
        { 
            int v = edge[e].v, id = edge[e].id, w = edge[e].w; //id代表活动的标号  
            E[id] = Ee[u], L[id] = El[v] - w; 
            if(E[id] == L[id]) //相等  是关键活动  
            { 
                printf("a%d : %d->%d\n", id, u, v); 
            } 
        } 
    } 
} 

辨别关键活动就是要找e(i)=l(i)的活动。为了求得e(i)和l(i),首先应求得事件的最早发生时间ve(j)和最迟发生时间vl(j)。如果活动ai由弧< j,k >表示,其持续时间记为dut(< j,k >),则有如下关系
e(i) = ve(j)
l(i) = vl(k) - dut(< j,k >)
求解ve(j)和vl(j)需分两个步进行:
1) 从ve(0)=0开始向前推进求得ve(j)
Ve(j) = Max{ve(i) + dut(< i,j >) };< i,j >属于T,j=1,2…,n-1
其中T是所有以第j个顶点为头的弧的集合。

2) 从vl(n-1) = ve(n-1)起向后推进求得vl(j)
vl(i) = Min{vl(j) - dut(< i,j >};< i,j >属于S,i=n-2,…,0
其中,S是所有以第i个顶点为尾的弧的集合。
这两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提先进行。也就是说,ve(j-1)必须在vj的所有前驱的最早发生时间求得之后才能确定,而vl(j-1)必须在Vj的所有后继的最迟发生时间求得之后才能确定。因此可以在拓扑排序的基础上计算ve(j-1)和vl(j-1)。

图片描述如下

read_case():在这个函数里又再次调用了read_graph()函数,目的就是输入出,入,权值,然后得到输出结果。

template<class T>
void AOE<T>::read_case() 
{ 
    init();
    for(int i = 1; i <= m; i++) 
    { 
        int u, v, w; //出,入,权值
        cout<<"a"<<i<<":"; 
        scanf("%d%d%d", &u, &v, &w); //输入出,入,权值 
        read_graph(u,v,w,i); //read_graph 
        outd[u]++, ind[v]++; 
    } 
} 

.

完整代码如下:

.cpp文件

#include <iostream> 
#include <cstdlib> 
#include <cstdio> 
#include <cstring> 
#include <queue> 
#include <stack> 
#include "AOE.h"
using namespace std; 

int n, m; //顶点个数,边条数 
int cnt; 

int main() 
{ 
    cout<<"请输入顶点个数和边的条数:"<<endl; 
    AOE<int> aoe; 
    while(~scanf("%d%d", &n, &m)) 
    { 
        aoe.get(n,m);
        aoe.read_case(); 
        printf("\n关键路径:\n"); 
        aoe.CriticalPath(); 
    } 
    return 0; 
} 

.h头文件

#ifndef CIRCLE_H
#define CIRCLE_H
#include <iostream> 
#include <cstdlib> 
#include <cstdio> 
#include <cstring> 
#include <queue> 
#include <stack> 
using namespace std; 

const int MAXN = 1010; //顶点个数最大  
const int MAXM = 10010; //边数最大  
template<class T>
class AOE
{
    public:
        AOE();
        void init();
        void read_graph(int u, int v, int w, int id);
        void toposort();
        void CriticalPath();
        void read_case();
        void get(int p,int q);
    private:
        int n, m; //顶点个数,边条数 
        int v,w,id,u;
        int cnt; 
        int first[MAXN], topo[MAXN]; //表头指针 
        int ind[MAXN], outd[MAXN]; //顶点入度,出度 
        int tot; 
        int Ee[MAXN], El[MAXN], E[MAXN], L[MAXN]; 
        /*Ee表示事件最早可能发生时间,El表示事件最迟允许发生时间*/ 
        /*E表示活动最早可能发生时间,L表示活动最迟允许发生时间*/
    struct Edge 
{ 
    int v, w; 
    int id; 
    int next; 
}edge[MAXM];  
} ;


template<class T>
AOE<T>::AOE()
{

}

template<class T>
void AOE<T>::get(int p,int q)
{
    n=p;
    m=q;
}
template<class T>
void AOE<T>::init() 
{ 
    cnt = 0; 
    tot = 0; 
    memset(first, -1, sizeof(first));  //char型初始化函数
    memset(ind, 0, sizeof(ind));       //头文件:<string.h> 或 <memory.h>
    memset(outd, 0, sizeof(outd));     //函数原型:void *memset(void *s , int ch , size_t  n )
    memset(Ee, 0, sizeof(Ee));         //memset(结构体/数组名 , "用于替换的字符“ , 前n个字符 );
    memset(E, 0, sizeof(E));           //函数解释:将s中的前n个字节用ch替换并且返回s
    memset(L, 0, sizeof(L));           //函数作用:在一段内存块中填充某一个给定的值,常用于较大的对结构体和数组的清零操作。

} 

template<class T>
void AOE<T>::read_graph(int u, int v, int w, int id) //int u, int v, int w, int id
{ 
    edge[cnt].v = v, edge[cnt].w = w, edge[cnt].id = id; 
    edge[cnt].next = first[u], first[u] = cnt++; 
} 

template<class T>
void AOE<T>::toposort() //拓扑排序  
{                       //(u,v)∈E(e)  u在线性序列中出现在v之前 
    queue<int> q; 
    for(int i = 0; i < n; i++) if(!ind[i]) q.push(i); //在有向图中选一个入度为0的顶点,并输出 
    while(!q.empty())          //重复操作,直至图中不含顶点 
    {                          //若图中仍含顶点,所有顶点入度均不为0,则该图中有环 
        int x = q.front(); 
        q.pop(); 
        topo[++tot] = x; 
        for(int e = first[x]; e != -1; e = edge[e].next) //删除所有与其有关的边 
        { 
            int v = edge[e].v, w = edge[e].w; 
            if(--ind[v] == 0) q.push(v); 
            if(Ee[v] < Ee[x] + w) //求出各个顶点Ee值  
            { 
                Ee[v] = Ee[x] + w; 
            } 
        } 
    } 
} 
//找关键路径并输出 
template<class T>
void AOE<T>::CriticalPath() 
{ 
    toposort(); 
    int top = tot; 
    for(int i = 0; i < n; i++) El[i] = Ee[n-1]; //初始化顶点事件的最迟发生时间  
    while(top) //逆拓扑排序求顶点El的值  
    { 
        int x = topo[top--]; 
        for(int e = first[x]; e != -1; e = edge[e].next) 
        { 
            int v = edge[e].v, w = edge[e].w; 
            if(El[x] > El[v] - w) 
            { 
                El[x] = El[v] - w; 
            } 
        } 
    } 
    for(int u = 0; u < n; u++) //求出E,L关键活动  
    { 
        for(int e = first[u]; e != -1; e = edge[e].next) 
        { 
            int v = edge[e].v, id = edge[e].id, w = edge[e].w; //id代表活动的标号  
            E[id] = Ee[u], L[id] = El[v] - w; 
            if(E[id] == L[id]) //相等  是关键活动  
            { 
                printf("a%d : %d->%d\n", id, u, v); 
            } 
        } 
    } 
} 

template<class T>
void AOE<T>::read_case() 
{ 
    init();
    for(int i = 1; i <= m; i++) 
    { 
        int u, v, w; //出,入,权值
        cout<<"a"<<i<<":"; 
        scanf("%d%d%d", &u, &v, &w); //输入出,入,权值 
        read_graph(u,v,w,i); //read_graph 
        outd[u]++, ind[v]++; 
    } 
} 
#endif

关键路径的求取有两个难点,一个是拓扑排序,一个是求关键路径。对于AOE算法求解关键路径的代码,我写的应该是比较麻烦的,其实还有更好更简单的代码实现,只不过现在水平有限,只能完成这种程度。
.


再附上两张从维基百科拿来的图
这里写图片描述
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/Godsolve/article/details/80598457