通常一个工程可以分成若干个子工程,这些子工程被称为活动(activity),完成这些活动,整个工程就完成了。给一个简单的例子,如下图,大学专业课程存在依赖关系,对于一些课程必须选修其他课程,完成整个工程就是学习所有的课程,每门课程的学习都是一个活动。
图1
整个工程可以通过工程图表示:
图2
工程图为有向图,顶点表示活动,例如有向边表示活动u必须先于活动v,这种有向图称为顶点表示活动的网络(Activity On Vertices),通常叫作AOV网络。活动u必须在活动v之前进行,u被称为v的直接前驱(immediate predecessor),v是u的直接后继(immediate successor)。如果是有向路径,则称u是v的前驱(predecessor),v是u的后继(successor) 。前驱和后继的关系具有传递性(transitivity),比如v2是v1的后继,v3是v2的后继,那么v3也是v1的后继。任何活动不能以自己为前驱或者后继。
由上面分析可以看到,AOV网络不能出现有向回路(回环),不含有向回路的有向图称为有向无环图(Directed Acyclic Graph, DAG)。对于AOV网络是不允许出现回路的,因为这样会陷入死循环,导致工程无法进行,故对于给定的AOV网络,必须要判断它是否是有向无环图。
判断有向无环图的方法是对AOV网络构造它的拓扑有序序列(topological order sequence),即所有的顶点排列成一个线性有序的序列,使得AOV网络中所有前驱和后继关系都能满足,如下图:
图3
这种构造AOV网络全部顶点的拓扑有序序列的运算称为拓扑排序(topological sort)。如果能够通过拓扑排序将AOV网络的所有顶点都排入一个拓扑有序的序列中,那么该AOV网络必定不存在有向环;反之,如果得不到所有顶点的拓扑有序序列,则说明该AOV网络存在有向环,此AOV网络所代表的工程是不可行的。例如对于图2的拓扑排序结果为:
C1,C2,C3,C4,C5,C6,C8,C9,C7 或者 C1,C8,C9,C2,C5,C3,C4,C7,C6。
可见一个AOV网络的拓扑有序序列可能并不是唯一的。
拓扑排序实现方法
AOV网络进行拓扑排序的算法如下:
1)从AOV网络中选择一个入度为0(没有直接前驱)的顶点并输出;
2)从AOV网络中删除该顶点及该顶点发出的所有边;
3)重复步骤1)和2),直到找不到入度为0的顶点。
结果:1.所有顶点都被输出,即整个拓扑排序完成;2.仍有顶点没有输出,且剩下的图再也没有入度为0的顶点,那么说明此图有环。
整个代码具体流程如下:
1)建立存放入度为0的顶点的栈,初始时将所有入度为0的顶点存入栈;
2)若当前栈不为空,重复执行:
(1)栈中弹出栈顶,将该顶点存入拓扑排序序列;
(2)删除弹出的顶点和它发出的每一条边,并且每一个边的终点节点入度减1
(3)如果顶点的入度减为0,则将该顶点压栈。
3)拓扑顶点个数小于实际顶点个数则存在环,否则可得到拓扑排序。
下面给出基于之前表示方法给出的数据结构和文件的完整代码:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef struct Vertex{
string name;
int index;
Vertex(string inputName, int inputIndex):name(inputName),index(inputIndex){}
Vertex():name(""),index(0){}
} VertexNode;
// graph表示邻接表,id表示每个节点入度统计,topSortId存储拓扑排序的索引。
bool topSort(const vector >& graph, vector& id, vector& topSortId) {
topSortId.clear();
topSortId.reserve(graph.size());
stack S; // 栈,存放入度为0的顶点
for(int i = 0; i < graph.size(); i++) {
if (id[i] == 0) {
S.push(i);
}
}
for (int i = 0; i < graph.size(); i++) {
if(S.empty()) {
return false;
}
int j = S.top();
S.pop(); //弹出栈顶存储的顶点j
topSortId.push_back(j); //存入拓排序序列
for(int k = 0; k < graph[j].size(); k++) {
int currentIndex = graph[j][k].index;
if(--id[currentIndex] == 0) {
S.push(currentIndex);
}
}
}
return true; // 如果前面所有顶点全部循环没问题,那么说明可以拓扑排序故返回true.
}
int main() {
unordered_map graphMap; // 图节点名和编号的Map
vector > adjGraph; // 图的连接表表示法
ifstream graphRdFile("graph_struct.txt");
if(!graphRdFile.good()) {
cout << "open graph file failed!" << endl;
return -1;
}
string line;
int index = 0;
string vertexName;
// 首先对Vertex Name进行编码
while(graphRdFile >> vertexName) {
if (graphMap.find(vertexName) == graphMap.end()) {
graphMap.insert(make_pair(vertexName, index++));
}
}
// 编码与Vertex的反映射
vector indexName = vector(graphMap.size(),"");
for(auto itr=graphMap.begin();itr!=graphMap.end();itr++) {
indexName[itr->second] = itr->first;
}
// 重新读
graphRdFile.clear();
graphRdFile.seekg(0,std::ios::beg);
adjGraph.resize(graphMap.size());
int currentIndex = 0; // 当前图节点的编号
vector id(adjGraph.size(), 0);
while (getline(graphRdFile, line)) { //按行读,每一行是一个图节点的连接情况
istringstream ss(line);
string tmp;
bool firstFlag = true;
while(ss >> tmp) {
if (firstFlag) {
if (graphMap.find(tmp) != graphMap.end()) {
currentIndex = graphMap[tmp];
} else {
break;
}
firstFlag = false;
continue;
}
if (graphMap.find(tmp) != graphMap.end()) {
adjGraph[currentIndex].emplace_back(VertexNode(tmp,graphMap[tmp]));
id[graphMap[tmp]]++;
}
}
}
// 拓扑排序测试:
vector topSortId;
if(topSort(adjGraph, id, topSortId)) {
for(int i = 0; i < topSortId.size(); i++) {
cout << indexName[topSortId[i]] << " ";
}
cout << endl;
} else {
cout << "Network has a cycle!" << endl;
}
return 0;
}
测试图结构文件graph_struct.txt内容为:
1 2 4
2 6
3 2 6
4
5 1 2 6
对应的有向无环图为:
输出结果为:
5 1 4 3 2 6