6.4.3 拓扑排序

好吧,笔者开单章开上瘾了,话不多说,先来一个拓扑排序,笔者最初自己写的时候并没有使用拓扑排序吧

Ordering_Tasks题解

点击查看笔者代码
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;

const int maxl = 100+5;
int cntr[maxl], cntc[maxl], mat[maxl][maxl];
bool isOut[maxl];

bool check(int (&a)[maxl], int& n){
  for(int i = 1; i <= n; i++) 
    if(a[i]) return true;
  return false;
}

void del(int& val, int& n) {
  cntr[val] = cntc[val] = 0;
  for(int i = 1; i <= n; i++) {
  	if(mat[val][i]) {
  	  mat[val][i] = 0;
	  cntc[i]--;	
	} 
	if(mat[i][val]) {
	  mat[i][val] = 0;
	  cntr[i]--;
	}
  }	
}

int main() {
//  freopen("test.in", "r", stdin);
//  freopen("test.out", "w", stdout);
  int n, m;
  while(scanf("%d%d", &n, &m) == 2 && (n||m)) {
  	memset(mat, 0, sizeof(mat));
  	memset(isOut, 0, sizeof(isOut));
  	memset(cntr, 0, sizeof(cntr));
  	memset(cntr, 0, sizeof(cntc));
	int x, y, flag = 0;
  	for(int i = 0; i < m; i++) {
  	  cin >> x >> y;
	  if(!mat[x][y]) {
	  	mat[x][y] = 1;
	  	cntr[x]++;
	    cntc[y]++;
	  }	
	}
	vector<int> output;
    while(check(cntr, n)) {
      output.clear();
      for(int i = 1; i <= n; i++) 
        if(cntr[i] && !cntc[i]) output.push_back(i);
      for(int i = 0; i < output.size(); i++) {
        isOut[output[i]] = 1;
		if(flag) cout << " ";
		if(!flag) flag = 1;
		cout << output[i];
		del(output[i], n);	
	  }
	}
	for(int i = 1; i <= n; i++) 
	  if(!isOut[i]) {
	  	if(flag) cout << " ";
	  	if(!flag) flag = 1;
	  	cout << i;
	  } 	
	  
	cout << endl;
  }
  return 0;
}

很暴力的一种simulate思想,这题的难点不是很多,笔者的思想类似一个大擂台,没有出现的数字就是最大的,后面随便排就好了,而出现的数字,不断地找到最小的数字,淘汰掉就可以了,是的,优胜劣汰,又是一种新的递归思想,不过,需要注意的是本题的一大坑点,m可以为0,这题不能以n && m作为终止条件,而是需要考虑到m==0的特殊情况,呜呜,我的first blood

作者的分析:
这里可以把每个变量看成一个点,“小于”关系看成有向边,则得到了一个有向图,这样,我们的任务实际上是把一个图的所有结点排序,使得每一条有向边(u,v)对应的u都排在v前面,在图论中,这个问题称为拓扑排序(topological sort)
不难发现,如果图中存在有向环,则不存在拓扑排序(会成为一个死循环),反之则存在,不包含有向环的有向图称之为有向无环图(Directed Acyclic Graph, DAG),可以借助DFS完成拓扑排序,在访问完一个结点之后把它加到当前拓扑序的首部
笔者在这边发现了一个比较有趣的描述,拓扑排序实质上做的工作就是将拓扑树拓扑变换成一条链,属实有意思,关于拓扑排序最容易想到的就是前面笔者的思想的优化版,不过肯定有很多种实现形式
那么这边作者采用的是bfs,很容易想到因为根部一定是小于1,2,..,n子树上的内容的,因此这边的bfs可以实现,至于为什么访问完结点之后是添到首部,而不是尾部,是因为这是dfs,所以是尾结点(叶子节点)先被发现,输出,所以此时如果添加再尾部,那么这个序列就会变成从大到小,只有添加到头部才会是从小到大,这边涉及到一个顺序问题,当然如果搞不清楚的读者,也可以随意选择一个喜欢的方向进行拓扑排序,在最后的时候随便拿个样例测试,决定后面是否需要进行逆序输出

点击查看代码
//作者的拓扑排序模板 
const int maxn = 100 + 5;
int n, m, G[maxn][maxn];
int c[maxn];
int topo[maxn], t;
bool dfs(int u) {
	cout << u << endl;
  c[u] = -1; //访问标志
  for(int v = 0; v < n; v++) if(G[u][v]) { //通过G[u][v]这个矩阵来存储各边之间的关系 
  	if(c[v]<0) return false; //存在有向环,失败退出,简单来说就是,在这个栈帧中v已经被访问过了,如果还能访问到v,那么就会形成一个有向环,不满足ADG了 
	else if(!c[v] && !dfs(v)) return false;//如果该节点没有被访问过,也就是说不构成有向环的递归判断是v不构成有向环,同时v指向的子树不构成有向环 
    //作者妙哉,本题确保不重不漏的关键在于作者利用了短路机制,只有该点没有被访问过的时候才会执行
	//递归dfs,否则如果被访问过,将不会执行递归,以来判断了是否是有向图,而来递归,一举两得,属实妙 
  }
  c[u] = 1; topo[--t]=u;//访问完节点后从头部插入 ,完成后进行标记 
  return true; 
}
//这里用到了一个c数组,c[u]=0表示从来没有访问过(从来没有调用过dfs(u))
//c[u]=1表示已经访问过,并且还递归访问过它的所有子孙(即dfs(u)曾被调用过,并已返回)
//c[u]=-1表示正在访问(即递归调用dfs(u)正在栈帧中,尚未返回) 这是本题拓扑排序的关键,有点像java中的同步 
bool toposort() {
  t = n;
  memset(c, 0, sizeof(c));
  for(int u = 0; u < n; u++) if(!c[u])//这边是对所有的结点进行拓扑排序 
    if(!dfs(u)) return false;//如果该图不是ADG,那么就不能进行拓扑排序进行报错 
  return true;
}

可以用DFS求出有向无环图(DAG)的拓扑排序,如果排序失败,说明该有向图存在有向环,不是DAG,这边注意c[n]的使用如果值为0,表示没有赋值过,如果为1,表示赋值过,如果为-1,表示正在被调用

posted @   banyanrong  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示