拓扑排序算法及应用详解1(原理与模板)

拓扑排序算法及应用详解1(原理与模板)

什么是拓扑排序?

排序是确定某序列的顺序,之前我们学过很多的排序,他们基本上是对数组进行排序,意在把一组元素序列按照某种自定义的顺序进行排序(广义)。它们的特点是,被排序的序列的元素之间存在某种大小关系,这种大小关系我们可以用数学意义上的**“大于”“小于”来体现。但是如果这种“大小关系”**我们不能用数学逻辑里的:’>’ 和 ‘<’ 来表示,而是一种内在的联系(顺序)的时候,我们就难以自定义一个compare()函数来说明我们的比较法则,而是只能用表示他们内在联系的方式:有向图来表达。
举个最直观的例子:我们在安排大学四年的上课顺序的时候,每一门课,都有它的先修课和后续继续深入的课,就像是上网站设计之前要学一些编程语言和技巧,例如C/C++/sql,这样才有利于PHP和MySQL的学习,学网站设计之后还要深入学习web系统设计。在这个关系种我们可以建立有向图,先修课将指向后继课程!所以大家可以想到,我们的拓扑排序是针对有向图的,但是光是有向图其实不够,还必须是有向无环图,从抽象的角度去理解,可以这么认为,如果有环,那那个环中的元素,到底谁先谁后是不清楚的,后面我会用代码来告诉大家,其实如果有环,那个环根本无法被处理!

图解拓扑排序

拓扑排序算法及应用详解1(原理与模板)_第1张图片
我们不妨以这个为例,我们可以看到,这是一个有向无环图!

基于BFS的拓扑排序算法步骤:

还是看之前的上课安排的例子,什么课会第一个被安排??很明显,最基础的课会被先安排,那什么课是最基础的呢?就是没有先修课的课!那这个没有先修课,就可以理解为当前有向无环图的点,它的入度为0!在上面这个图种我们很容易看到,有两个入度为0的点:v1、v6,算法步骤如下:
第一步:
度为0的点:v1 v6入队列;
队列状态:v1、v6;
第二步:v1出队列,v1直接指向的点:v2、v3、v4入队列
队列状态:v6、v2、v3、v4;
第三步:v6出队列,v6直接指向的点:v5入队列
队列状态:v2、v3、v4、v5;
第四步:v2出队列,v2直接指向的点:无入队列
……
接下来的步骤自己去模拟就好了,实际上大家可以看得出,这个拓扑排序我使用的是bfs的思想!

基于BFS的代码实现:

#include 
#include  
using namespace std;

const int maxn = 1e4 + 5;
vector<int> G[maxn];
int n, m, u, v, in[maxn], q[maxn];//数组模拟队列 
int Sort(){
	int l = 0, r = 0;
	for(int i = 1;i <= n;i++){
		if(!in[i])
			q[r++] = i;//第一批入队 
	}
	while(l < r){
		int val = q[l++];// 出队列
		for(int i = 0;i < G[val].size();i++){
			in[G[val][i]]--;
			if(!in[G[val][i]])
				q[r++] = G[val][i];// 直接指向的点入队列 
		} 
	}// l < r 相当于队列不空 
	return r;// 检查是否存在环路! 
} 

int main(){
	cin >> n >> m;
	for(int i = 1;i <= m;i++){
		cin >> u >> v;
		G[u].push_back(v);// 邻接表存储 
		in[v]++;// v的入度+1 
	}
	int cnt = Sort();
	if(n == cnt){
		for(int i = 0;i < cnt;i++)
			cout << q[i] << ' ';
	}
	else
		cout << "It is a Circle!";
	return 0;
}

值得注意的是:我这里用数组来模拟队列操作,然后用邻接表来存图。用数组是因为STL提供的队列很容易爆(类内开辟空间不是在自由区开辟的,不能开那么大),其实我这样写也还是很容易爆,在竞赛种最好还是用循环队列模拟、并且用链式前向星来存图,这里我只是为了来描述拓扑排序,于是就不搞那么复杂了!

基于DFS的拓扑排序算法:

回顾DFS的特点:沿着一条可行路径,一直搜素到最底层,然后逐层回溯,这个过程实际上正好反映了点与点之间的先后关系,天然符合拓扑排序!
算法分析
在一个有向无环图种,如果有一个点u,它的入度是0,那么我们就从这个点开始做dfs。dfs返回的顺序恰好就是一个拓扑序!
处理一些细节
我们应该是以入度为0的点为dfs的开始,那我们该如何找到这个点?如果有很多入度为0的点,我们要一个个去做dfs吗?有这么一种方法可以解决这个问题,我们可以假设一个虚拟的点,这个点是图中唯一一个入度为0的点,图中其他的所有点都是它的下一层递归!然而在实际编程的时候是不需要考虑这个点的,我们只需要在main()函数中把每一个点轮流执行一遍dfs就可以了!这样做就相当于显式地递归了虚拟假象点的下一层点!

基于DFS的代码实现:

#include 
#include 
#include 
#include 
#include 
using namespace std;

const int maxn = 1e4 + 5;
vector<int> G[maxn];
stack<int> s;
int n, m, u, v, vis[maxn];
bool dfs(int pos){
	vis[pos] = -1;// 表示当前点正在访问中
	for(int i = 0;i < G[pos].size();i++){
		if(-1 == vis[G[pos][i]])
			return false;
		else if(!vis[G[pos][i]] && !dfs(G[pos][i]))
			return false;
	} 
	vis[pos] = 1;// 表示成功被处理
	s.push(pos);
	return true; 
}

bool Sort(){
	memset(vis, 0, sizeof(vis));// 该点未被处理 
	for(int i = 1;i <= n;i++){
		if(!vis[i] && !dfs(i))
			return false;
	}
	return true;
}

int main(){
	cin >> n >> m;
	for(int i = 1;i <= m;i++){
		cin >> u >> v;
		G[u].push_back(v);
	}
	if(!Sort())
		cout << "It has a Circle!";
	else{
		while(!s.empty()){
			cout << s.top() << ' ';
			s.pop();
		}
	}
	return 0;
}

你可能感兴趣的:(算法)