拓扑排序Topological Sorting

标签: 拓扑排序Topological SortingDAG有向无环图AOV
616人阅读 评论(0) 收藏 举报
分类:

http://blog.csdn.net/pipisorry/article/details/71125207

拓扑排序Topological Sorting

在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:

  1. 每个顶点出现且只出现一次。
  2. 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

也可以定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径,那么在排序中B出现在A的后面。[wekipedia]

Note: 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。即一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。

一个图的拓扑排序可以看成是图中所有顶点沿水平线排列而成的一个序列。使得所有的有向边均从左指向右。因此,拓扑排序不同于通常意义上的排序。

早晨穿衣的过程


拓扑排序:Kahn算法

维基百科上关于Kahn算法的伪码描述:

L← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is non-empty do
    remove a node n from S
    insert n into L
    foreach node m with an edge e from nto m do
        remove edge e from thegraph
        ifm has no other incoming edges then
            insert m into S
if graph has edges then
    return error (graph has at least onecycle)
else
    return L (a topologically sortedorder)

简单来说就是:

记录每个点的入度。
将入度为0的顶点加入队列。
依次对入度为0的点进行删边操作,同时将新得到的入度为零的点加入队列。
重复上述操作,直至队列为空。

Note:

1 该算法的实现十分直观,关键在于需要维护一个入度为0的顶点的集合:每次从该集合中取出(没有特殊的取出规则,随机取出也行,使用队列/栈也行,下面示例使用的是queue)一个顶点,将该顶点放入保存结果的List中。
紧接着循环遍历由该顶点引出的所有边,从图中移除这条边,同时获取该边的另外一个顶点,如果该顶点的入度在减去本条边之后为0,那么也将这个顶点放到入度为0的集合中。然后继续从集合中取出一个顶点。。。当集合为空之后,检查图中是否还存在任何边,如果存在的话,说明图中至少存在一条环路。不存在的话则返回结果List,此List中的顺序就是对图进行拓扑排序的结果。

2  lz提示:这不就是BFS算法吗,说到下面的DFS实现就应该马上知道这个是BFS了!

算法分析

1 唯一性
如果一个DAG的拓扑排序中任意连续的两点都是可连通的,那么这个序列也就是DAG的Hamiltonian路径,而且如果DAG图的Hamiltonian路径存在,那么拓扑排序就是唯一的。否则如果一个拓扑排序结果不是Hamiltonian路径,那么就存在多个拓扑排序结果。

通常,一个有向无环图可以有一个或多个拓扑排序序列。取顶点的顺序不同会得到不同的拓扑排序序列,当然前提是该图存在多个拓扑排序序列。

拓扑排序中,结果具有唯一性的条件也是其所有顶点之间都具有全序关系。如果没有这一层全序关系,那么拓扑排序的结果也就不是唯一的了。如果拓扑排序的结果唯一,那么该拓扑排序的结果同时也代表了一条哈密顿路径。

复杂度分析
初始化入度为0的集合需要遍历整张图,检查每个节点和每条边,因此复杂度为O(E+V);
然后对该集合进行操作,又需要遍历整张图中的每条边(输出每个顶点的同时还要删除以它为起点的边),复杂度也为O(E+V);
因此Kahn算法的复杂度即为O(E+V)。

拓扑排序示例

例如,下面这个图:


它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:

  1. 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
  2. 从图中删除该顶点和所有以它为起点的有向边。
  3. 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。

拓扑排序的DFS方法实现

拓扑排序还可以采用 深度优先搜索(DFS)的思想来实现,这个时候需要使用到栈结构来记录拓扑排序的结果。详见《topological sorting via DFS》。

维基百科上的伪码
L ← Empty list that will contain the sorted nodes
S ← Set of all nodes with no outgoing edges
for each node n in S do
    visit(n)
function visit(node n)
    if n has not been visited yet then
        mark n as visited
        for each node m with an edgefrom m to ndo
            visit(m)
        add n to L

或者参考算法导论上的:

1、 调用dfs_travel();
2、  在dfs_travel()每次调用dfs()的过程中,都记录了顶点s的完成时间,将顶点s按完成顺序保存在存放拓扑排序顺序的数组topoSort[]中。这样,该数组就存放了按先后顺序访问完成的所有顶点。
3、  最后拓扑排序得到的线性序列,即为topoSort[]的逆序。

DFS的实现更加简单直观,使用递归实现。利用DFS实现拓扑排序,实际上只需要添加一行代码,即上面伪码中的最后一行:add n to L。
需要注意的是,将顶点添加到结果List中的时机是在visit方法即将退出之时。

关键在于为什么在visit方法的最后将该顶点添加到一个集合中,就能保证这个集合就是拓扑排序的结果呢?因为添加顶点到集合中的时机是在dfs方法即将退出之时,而dfs方法本身是个递归方法,只要当前顶点还存在边指向其它任何顶点,它就会递归调用dfs方法,而不会退出。因此,退出dfs方法,意味着当前顶点没有指向其它顶点的边了,即当前顶点是一条路径上的最后一个顶点。
下面简单证明一下它的正确性:
考虑任意的边v->w,当调用dfs(v)的时候,有如下三种情况:
    dfs(w)还没有被调用,即w还没有被mark,此时会调用dfs(w),然后当dfs(w)返回之后,dfs(v)才会返回
    dfs(w)已经被调用并返回了,即w已经被mark
    dfs(w)已经被调用但是在此时调用dfs(v)的时候还未返回
需要注意的是,以上第三种情况在拓扑排序的场景下是不可能发生的,因为如果情况3是合法的话,就表示存在一条由w到v的路径。而现在我们的前提条件是由v到w有一条边,这就导致我们的图中存在环路,从而该图就不是一个有向无环图(DAG),而我们已经知道,非有向无环图是不能被拓扑排序的。
那么考虑前两种情况,无论是情况1还是情况2,w都会先于v被添加到结果列表中。所以边v->w总是由结果集中后出现的顶点指向先出现的顶点。为了让结果更自然一些,可以使用栈来作为存储最终结果的数据结构,从而能够保证边v->w总是由结果集中先出现的顶点指向后出现的顶点。

实现参考

[拓扑排序的原理及其实现]

复杂度分析
复杂度同DFS一致,即O(E+V)。具体而言,首先需要保证图是有向无环图,判断图是DAG可以使用基于DFS的算法,复杂度为O(E+V),而后面的拓扑排序也是依赖于DFS,复杂度为O(E+V)。Time Complexity: The above algorithm is simply DFS with an extra stack. So time complexity is same as DFS which is O(V+E).

Note: DFS算法性能分析

1 . 空间复杂度:DFS算法是一个递归算法,需要借助一个递归工作栈,故它的空间复杂度为O(|V|)。

2 . 时间复杂度:当以邻接表存储时,时间复杂度为O(|V|+|E|)。当以邻接矩阵存储时,时间复杂度为O(|V|^2)。

两种实现算法的总结

这两种算法分别使用链表和栈来表示结果集。

对于基于DFS的算法,加入结果集的条件是:顶点的出度为0。这个条件和Kahn算法中入度为0的顶点集合似乎有着异曲同工之妙,这两种算法的思想犹如一枚硬币的两面,看似矛盾,实则不然。一个是从入度的角度来构造结果集,另一个则是从出度的角度来构造。

实现上的一些不同之处:Kahn算法不需要检测图为DAG,如果图为DAG,那么在出度为0的集合为空之后,图中还存在没有被移除的边,这就说明了图中存在环路。而基于DFS的算法需要首先确定图为DAG,当然也能够做出适当调整,让环路的检测和拓扑排序同时进行,毕竟环路检测也能够在DFS的基础上进行。

二者的复杂度均为O(V+E)。

拓扑排序的应用

1 拓扑排序通常用来“排序”具有依赖关系的任务。

比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边 表示在做任务 B 之前必须先完成任务 A。故在这个工程中,任意两个任务要么具有确定的先后关系,要么是没有关系,绝对不存在互相矛盾的关系(即环路)。

Note: 我们把顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行。

2 其他图算法的预处理
    DAG的强连通分支问题 先得到拓扑排序,形成逆向图(所有边与原来方向相反),然后根据拓扑排序依次再进行DFS。
    DAG的最短路径问题,这可以在O(V+E)复杂度解决最短路径问题。同样类似的算法适用与DAG的最长路径问题,给定一个点求DAG中的各个点与给定点之间的最长路径。最长路径问题要比最短路径问题难,因为最长路径问题没有最优子结构,对于通用的图的最长路径算法还是NP难的问题。

拓扑排序的实现

根据上面讲的方法,我们关键是要维护一个入度为0的顶点的集合。

图的存储方式有两种:邻接矩阵和邻接表。这里我们采用邻接表来存储图。

实现示例

测试如下DAG图:

C++代码如下:

#include<iostream>
#include <list>
#include <queue>
using namespace std;

/************************类声明************************/
class Graph{
	int V;             // 顶点个数
	list<int> *adj;    // 邻接表
	queue<int> q;      // 维护一个入度为0的顶点的集合
	int* indegree;     // 记录每个顶点的入度
public:
	Graph(int V);                   // 构造函数
	~Graph();                       // 析构函数
	void addEdge(int v, int w);     // 添加边
	bool topological_sort();        // 拓扑排序
};

/************************类定义************************/
Graph::Graph(int V){
	this->V = V;
	adj = new list<int>[V];

	indegree = new int[V];  // 入度全部初始化为0
	for(int i=0; i<V; ++i)
		indegree[i] = 0;
}

Graph::~Graph(){
	delete [] adj;
	delete [] indegree;
}

void Graph::addEdge(int v, int w){
	adj[v].push_back(w); 
	++indegree[w];
}

bool Graph::topological_sort(){
	for(int i=0; i<V; ++i)
		if(indegree[i] == 0)
			q.push(i);         // 将所有入度为0的顶点入队

	int count = 0;             // 计数,记录当前已经输出的顶点数 
	while(!q.empty()){
		int v = q.front();      // 从队列中取出一个顶点
		q.pop();

		cout << v << " ";      // 输出该顶点
		++count;
		// 将所有v指向的顶点的入度减1,并将入度减为0的顶点入队
		list<int>::iterator beg = adj[v].begin();
		for( ; beg!=adj[v].end(); ++beg)
			if(!(--indegree[*beg]))
				q.push(*beg);   // 若入度为0,则入队
	}

	if(count < V)
		return false;           // 没有输出全部顶点,有向图中有回路
	else
		return true;            // 拓扑排序成功
}
int main(){
	Graph g(6);   // 创建图
	g.addEdge(5, 2);
	g.addEdge(5, 0);
	g.addEdge(4, 0);
	g.addEdge(4, 1);
	g.addEdge(2, 3);
	g.addEdge(3, 1);

	g.topological_sort();
	return 0;
}

输出结果是 4, 5, 2, 0, 3, 1。这是该图的拓扑排序序列之一。

[算法专题:拓扑排序]

from: http://blog.csdn.net/pipisorry/article/details/71125207

ref: [拓扑排序的原理及其实现]

[拓扑排序(Topological Sorting)]*

[图基本算法 拓扑排序(基于dfs)]

[拓扑排序, BFS 做法]


1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:2907652次
    • 积分:27162
    • 等级:
    • 排名:第227名
    • 原创:556篇
    • 转载:30篇
    • 译文:5篇
    • 评论:254条
    Welcome to 皮皮blog~

    博客专栏
    最新评论