目录

题目描述

问题分析

拓扑排序

关键路径

问题解决

拓扑排序

关键路径


题目描述

说明: AOE 网络是有向无环加权图,其中顶点表示事件,弧表示活动,权表示活动持续的时间,通常可以用来估算工程完成的时间,即图中从开始点到结束点之间最长的路径对应的时间。请完成一个程序,完成下列任务:

1 、计算 AOE 网络对应的拓扑排序。如果排序结果不唯一,请输出按照从小到大的顺序排列的结果。从小到大的顺序就是输入的节点序列顺序(参见下面关于输入格式的说明)。如图1中满足要求的拓扑排序是: a-b-c-d-e-f-g-h-k ,图2中满足要求的拓扑排序是:v1-v3-v5-v2-v6-v4-v7-v8-v9

2 、计算 AOE 网络的关键路径。注意关键路径可能不唯一,要求输出所有的关键路径。同样,按照是按照从小到大的顺序输出。例,如果得到两条关键路径,分别是0-1-3-6-8-9和0-1-3-4-5-8-9,那么先输出后一条路径,因为两条路径中前三个节点相同,而后一条路径第四个节点的编号小。

测试用例的输入输出格式说明:

输入:

节点的个数,边的条数;

各个节点的名称序列

边: < 起点 , 终点 , 权值 > 。说明起点和终点是在各个点在输入序列中的位置,如图1中边 <a,b> 表示为 <0,1,6> 。

输出:

拓扑排序;

关键路径

lua 节点的属性_数据结构

lua 节点的属性_lua 节点的属性_02

测试用例0是与图1相对应的,测试用例1是与图2相对应的。

期待的输出时间限制内存限制额外进程

测试用例 1

以文本方式显示


  1. 9,11↵
  2. a,b,c,d,e,f,g,h,k↵
  3. <0,1,6>,<0,2,4>,<0,3,5>,<1,4,1>,<2,4,1>,<4,6,8>,<4,7,7>,<3,5,2>,<5,7,4>,<6,8,2>,<7,8,4>↵

以文本方式显示


  1. a-b-c-d-e-f-g-h-k↵
  2. a-b-e-h-k↵

1秒

64M

0

测试用例 2

以文本方式显示


  1. 9,11↵
  2. v1,v2,v3,v4,v5,v6,v7,v8,v9↵
  3. <0,4,6>,<0,2,4>,<0,5,5>,<4,1,1>,<2,1,1>,<1,6,8>,<1,7,7>,<5,3,2>,<3,7,4>,<6,8,2>,<7,8,4>↵

以文本方式显示


  1. v1-v3-v5-v2-v6-v4-v7-v8-v9↵
  2. v1-v5-v2-v8-v9↵

1秒

64M

0

测试用例 3

以文本方式显示


  1. 4,4↵
  2. A,B,C,D↵
  3. <0,1,2>,<1,3,3>,<3,2,5>,<2,1,1>↵

以文本方式显示


  1. NO TOPOLOGICAL PATH↵

1秒

64M

0

问题分析

这道题可以分成两部分求解。

一是输出拓扑序列,而是输出关键路径。

拓扑排序

首先讨论拓扑排序。

我们需要先找到入度为0的结点,将其输出,接着将其所有边都删去(也就是将与它所连的所有顶点的入度都-1),然后再次寻找入度为0的结点。直到所有顶点都被找到为止。

由以上分析可知,我们需要一个邻接表存储各个顶点所连接的边。在顶点里需要存储入度信息。在边里需要存储所连的另一个顶点及边的权重。

另外我们需要注意,输出顶点的顺序也是有要求的,所以我们需要保证每个顶点所连的边要按输入顺序存储。

关键路径

当查找关键路径时,我们可以借助关键路径上活动的特点,即e==l(e为活动的最早开始时间,l为活动的最晚开始时间)。而e和l的求法如下:

假设我们有一个由j指向k的路径活动,ve为j开始的最早状态,vl为k开始的最晚状态,weight为活动i的权重。则有

e[i]=ve[j],l[i]=vl[k]-weight[i]

由此我们可以找出关键路径。

需要注意的是,题目要求找出所有关键路径。我们可以用回溯法解决。从第一个点开始遍历,找e==l的连接点,顺次往后,当找到最后一个点时证明次路径可以作为一条关键路径。

保存所有关键路径,按顺序输出即可。

问题解决

拓扑排序

首先做一些准备工作。

struct edge  //存储边的结构体
{
    int to;  //存储所连另一顶点的输入下标
    int weight; //边的权重
};
struct vec  //顶点结构体
{
    char id[5];  //顶点名字
    int inlink=0;  //顶点入度
    int linknum=0;  //顶点后面所连的边数(即出度)
    edge link[300];//顶点所连边构成的数组
    int ve=0;  //顶点的最早开始时间
    int vl;  //顶点的最晚开始时间
};
bool cmp(const edge& a,const edge& b) //排序依据函数,按顶点的输入顺序排序
{
    return a.to<b.to;
}
vec V[100];  //顶点构成的数组
int n,m;  //顶点数和边数

其中weight,ve和vl是求关键路径的时候用的,现在可以先不用关注。

然后进行读入数据处理

void Initial()
{
    scanf("%d,%d",&n,&m); 
    char str[200] = { '\0' };  // 初始化存储整行读入数据的数组
    scanf("%s",str);  
    int len = strlen(str), q = 0, k = 0;  
    for (int i = 0; i < len; i++)  
    {  
        if (str[i] != ',')  //如果不是逗号就继续读入
            V[k].id[q++] = str[i];  
        else  
        {  
            V[k].id[q] = '\0';  //如果是逗号就跳过,另开一个V[k]存储
            k++;  
            q = 0;  //q重新置0(从新名字的第一位开始保存)
        }  
    }  
    getchar();  
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("<%d,%d,%d>",&a,&b,&c); //读入边的数据
        getchar();
        V[b].inlink++;  //b位置的结点入度++
        V[a].link[V[a].linknum].to=b; //把b位置存到a位置的结点连接的点的位置数组里
        V[a].link[V[a].linknum].weight=c; //保存相应权重
        V[a].linknum++; //a所在位置的结点所连接的边数++
    }
    for(int i=0;i<n;i++)
    {
        sort(V[i].link, V[i].link + V[i].linknum,cmp); //对a位置处的结点所连的各个结点位置下标从小到大排序,即按输入顺序对其排序
    }
}

然后进行拓扑排序

int topu(int n)
{
    priority_queue<int, vector<int>,greater<int>> q; //定义优先队列
    vector<int> T; //保存拓扑序列
    for(int i=0;i<n;i++) //按下标从小到大遍历各个结点
    {
        if(V[i].inlink==0) //找到入度为0的结点,加入优先队列
        {
            q.push(i);
        }
    }
    while(!q.empty()) //如果优先队列非空
    {
        int u=q.top(); //取出队首元素
        q.pop();  //弹出队首元素
        T.push_back(u); //把它加入拓扑序列
        for(int i=0;i<V[u].linknum;i++) //按队首元素存储的下标信息找到结点,
                                         并将它所连的其他节点入度-1
        {
            int v=V[u].link[i].to;
            V[v].inlink--;
            if(V[v].inlink==0)  若入度刚-1的结点入度变为0,立即将其加入优先队列
            {
                q.push(v);
            }
            if(V[v].ve<V[u].ve+V[u].link[i].weight) V[v].ve=V[u].ve+V[u].link[i].weight;
               这句是为了关键路径用的,更新各个结点最早开始时间
        }
    }
    if(T.size()<n) //如果拓扑序列中的顶点数小于总顶点数说明有环,拓扑序列不存在
    {
        cout<<"NO TOPOLOGICAL PATH"<<endl;
    }
    else
    {
        for(int i=0;i<n;i++) //否则拓扑序列存在,按顺序输出vector T中的下标对应的结点名称即可
        {
            V[i].vl=V[n-1].ve; //这句是为了关键路径,把所有结点的最晚开始时间都先初始化为
                                                             最后一个结点的最早开始时间。
            if(i>0) cout<<"-";
            cout<<V[T[i]].id;
        }
        cout<<endl;
    }

可以先写个main函数看看对错

int main()
{
    Initial();
    topu(n);
    return 0;
}

关键路径

首先定义查找关键路径函数。

vector<vector<string>> paths; // 存储所有的关键路径

void findCriticalPath(int node, vector<string>& path)
{
    path.push_back(V[node].id); // 将当前结点加入路径

    if (node == n-1) // 达到终点事件
    {
        paths.push_back(path); // 将完整路径加入结果
    }
    else
    {
        for (int i = 0; i < V[node].linknum; i++) //遍历这个点所连的所有边
        {
            int nextNode = V[node].link[i].to;//记当前边关联到的点为nextNode
            int e = V[node].ve; // 当前事件的最早发生时间
            int l = V[nextNode].vl - V[node].link[i].weight; // 下一个事件的最迟发生时间减去边的权值
            if (e == l) //证明可能在关键路径上
            {
                findCriticalPath(nextNode, path); // 递归进入下一个事件
            }
        }
    }
    path.pop_back(); // 回溯时,将当前结点从路径中移除
}

然后输出关键路径

vector<string> pathing;//定义一个数组用来存储路径结点
void Putout()
{
    // 计算事件的最迟发生时间 vl
    for (int i = n - 2; i >= 0; i--)
    {
        int u=T[i]; //一定要注意!!从拓扑序列的倒数第二个元素开始遍历,而不是从V[]的倒数第二个元素
                     //开始遍历,否则会导致遍历的结点不按连接顺序从后往前,而是按下标大小从后往前
        for (int j = 0; j < V[u].linknum; j++)
        {
            int v = V[u].link[j].to;
            V[u].vl = min(V[u].vl, V[v].vl - V[u].link[j].weight); //更新结点的vl,便于关键路
                                                                            //径结点的计算
        }
    }
    findCriticalPath(0,pathing);
    for(int i=0;i<paths.size();i++)  //输出关键路径
    {
        for(int j=0;j<paths[i].size();j++)
        {
            if(j!=paths[i].size()-1){
            cout<<paths[i][j]<<"-";
            }
            else cout<<paths[i][j]<<endl;
        }
    }
}

这里一定要注意, 要从拓扑序列数组的倒数第二个元素开始遍历,而不是从V[]的倒数第二个元素开始遍历,否则会导致遍历的结点不按连接顺序从后往前,而是按下标大小从后往前遍历。

因为如果按V里存的下标遍历的话,如果下标大的活动在图中连接很靠前的地方,那么从后往前遍历的时候,它后面连接的比它下标小的点的vl还没来得及更新,所以计算出的vl不是最晚的活动开始时间,可能会偏小。

拿题图2举例

lua 节点的属性_lua 节点的属性_02

若按下标从倒数第二个往前遍历,那么轮到v6的时候要用v4的最晚开始时间减去v6到v4的权重,但是此时v4的最晚开始时间还未更新,所以计算出的v6的最晚开始时间也是不准确的。

所以我们要用拓扑序列,因为拓扑序列的顺序就是结点连接顺序,从v9的前一个结点开始往前遍历,每次都是当前结点连接的上一个结点。

所以我们可以把上面写的int topu(int n)函数中定义的T拿到全局变量里来,方便关键路径函数使用

最后赋完整代码:

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <cstring>

using namespace std;
struct edge
{
    int to;
    int weight;
};
struct vec
{
    char id[5];
    int inlink=0;
    int outlink=0;
    int linknum=0;
    edge link[300];
    int ve=0;
    int vl;
};
bool cmp(const edge& a,const edge& b)
{
    return a.to<b.to;
}
vec V[100];
int n,m;
void Initial()
{
    scanf("%d,%d",&n,&m);
    char str[200] = { '\0' };  
    scanf("%s",str);  
    int len = strlen(str), q = 0, k = 0;  
    for (int i = 0; i < len; i++)  
    {  
        if (str[i] != ',')  
            V[k].id[q++] = str[i];  
        else  
        {  
            V[k].id[q] = '\0';  
            k++;  
            q = 0;  
        }  
    }  
    getchar();  
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("<%d,%d,%d>",&a,&b,&c);
        getchar();
        V[b].inlink++;
        V[a].link[V[a].linknum].to=b;
        V[a].link[V[a].linknum].weight=c;
        V[a].linknum++;
        V[a].outlink++;
    }
    for(int i=0;i<n;i++)
    {
        sort(V[i].link, V[i].link + V[i].linknum,cmp);
    }
}
vector<int> T;
int topu(int n)
{
    priority_queue<int, vector<int>,greater<int>> q;
    for(int i=0;i<n;i++)
    {
        if(V[i].inlink==0)
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int u=q.top();
        q.pop();
        T.push_back(u);
        for(int i=0;i<V[u].linknum;i++)
        {
            int v=V[u].link[i].to;
            V[v].inlink--;
            if(V[v].inlink==0)
            {
                q.push(v);
            }
            if(V[v].ve<V[u].ve+V[u].link[i].weight) V[v].ve=V[u].ve+V[u].link[i].weight;
        }
    }
    if(T.size()<n)
    {
        cout<<"NO TOPOLOGICAL PATH"<<endl;
    }
    else
    {
        for(int i=0;i<n;i++)
        {
            V[i].vl=V[n-1].ve;
            if(i>0) cout<<"-";
            cout<<V[T[i]].id;
        }
        cout<<endl;
    }
}

vector<vector<string>> paths; // 存储所有的关键路径

void findCriticalPath(int node, vector<string>& path)
{
    path.push_back(V[node].id); // 将当前结点加入路径

    if (node == n-1) // 达到终点事件
    {
        paths.push_back(path); // 将完整路径加入结果
    }
    else
    {
        for (int i = 0; i < V[node].linknum; i++)
        {
            int nextNode = V[node].link[i].to;
            int e = V[node].ve; // 当前事件的最早发生时间
            int l = V[nextNode].vl - V[node].link[i].weight; // 下一个事件的最迟发生时间减去边的权值
            if (e == l)
            {
                findCriticalPath(nextNode, path); // 递归进入下一个事件
            }
        }
    }
    path.pop_back(); // 回溯时,将当前结点从路径中移除
}
vector<string> pathing;
void Putout()
{
    // 计算事件的最迟发生时间 vl
    for (int i = n - 2; i >= 0; i--)
    {
        int u=T[i];
        for (int j = 0; j < V[u].linknum; j++)
        {
            int v = V[u].link[j].to;
            V[u].vl = min(V[u].vl, V[v].vl - V[u].link[j].weight);
        }
    }
    findCriticalPath(0,pathing);
    for(int i=0;i<paths.size();i++)
    {
        for(int j=0;j<paths[i].size();j++)
        {
            if(j!=paths[i].size()-1){
            cout<<paths[i][j]<<"-";
            }
            else cout<<paths[i][j]<<endl;
        }
    }
}

int main()
{
    Initial();
    topu(n);
    Putout();
    return 0;
}