拓扑排序

拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。——OI wiki

实现流程

首先遍历整张图上的顶点,如果一个顶点的入度为 \(0\),将它加入 \(S\)

\(S\) 不为空时:

\(S\) 中任取一个顶点 \(x\),将 \(x\) 加入到 \(L\) 的队尾,并把 \(x\)\(S\) 中删去;

遍历从 \(x\) 出发的边 \(x → y\),把这条边删掉,如果 \(y\) 的入度变成了 \(0\),则将其加入到 \(S\) 中;

循环结束时,如果所有点都加入了 \(L\)

那么我们就找到了一个合法的拓扑序列,否则可以证明图中存在环;

代码实现

B3644 【模板】拓扑排序 / 家谱树

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 386;

struct node{
	ll next , to;
}edge[N];

ll first[N],cnt;
inline void add(ll u,ll v){
	cnt++;
	edge[cnt].next = first[u];
	edge[cnt].to = v;
	first[u] = cnt;
}

ll n;
ll indegree[100086];
queue<ll> que;
inline void toposort(){
	ll front = 1 , real = 0;
	for(ll i = 1; i <= n;i++){
		if(!indegree[i]) que.push(i);
	}
	while(!que.empty()){
		ll u = que.front();
		que.pop();
		cout << u << " ";
		for(ll i = first[u];i;i = edge[i].next){
			ll v = edge[i].to;
			indegree[v]--;
			if(!indegree[v]) que.push(v);
		}
	}
}

int main() {
	
	cin >> n;
	for(ll i = 1;i <= n;i++){
		ll j;
		while(cin >> j && j){
			add(i,j);
			indegree[j] ++;
		}
	}
	
	toposort();

	return 0;
}


其他技巧及注意事项

拓扑序列的可重性

我们将拓扑排序后,节点按出现的先后顺序组成的序列称为拓扑序列。

注意,拓扑序列可能并不唯一,对于一个DAG(有向无环图)来说,至多有 \(n!\)(即每个点都是单独的,此时拓扑序所有可能为 \(n\) 的全排列),至少则有 \(1\) 种。

那么我们如何判断拓扑序列是否唯一呢?

其实很简单,我们是使用了一个队列来处理入度为 \(0\) 的点,那么我们尝试思考,是不是只要检查任何时刻,是否存在多个入度为 \(0\) 且还未处理的点,就能判断是否拓扑唯一了。

那入度为 \(0\) 的点在哪里存着?不就是队列中嘛。所以我们就只需要实时判断队列大小是否大于 \(1\) 就可以了。

又众所周知,std::queue 自带一个函数 size() ,可以返回队列大小。

只需要在刚才的代码上加一行特判就可以了。

P1960 郁闷的记者

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5+86;
ll n , m;
struct node{
	ll next , to ;
}edge[N];

ll first[N] , cnt;
inline void add(ll u ,ll v){
	cnt++;
	edge[cnt].next = first[u];
	edge[cnt].to = v;
	first[u] = cnt;
}

ll indegree[N];
queue<ll> que;
bool book;
inline void toposort(){
	for(ll i = 1;i <= n;i++){
		if(!indegree[i]){
			que.push(i);
		}
	}
	
	while(!que.empty()){
		if(que.size() > 1) book = 1; 
		// 一旦队列里出现了两个及以上的点,说明拓扑序不唯一
		ll u = que.front();
		que.pop();
		cout << u << endl;
		for(ll i = first[u] ;i;i =edge[i].next){
			ll v = edge[i].to;
			indegree[v] --;
			if(!indegree[v]) que.push(v);
		}
	}
	if(book) cout << 1;
	else cout << 0;
}

int main() {
	cin >> n >> m;
	for(ll i = 1;i <= m;i++){
		ll u,v;
		cin >> u >> v;
		add(u,v);
		indegree[v] ++;
	}
	toposort();


	return ~~ (0 ^ 0);
}

DAG上动态规划

作者习惯叫带权拓扑排序,也就是说图这次要给出权值,或者类似权值的其他需要处理的量(事实上拓扑排序的问题一般并不会直接设置成图论,而是给你一个实际情景解决现实问题,这种能用拓扑排序解决的问题,就是完成一个事件需要完成他所有的前缀事件的这类问题,又叫 AOV 网)。

日常生活中,一项大的工程可以看作是由若干个子工程组成的集合,这些子工程之间必定存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始。
我们用有向图来表现子工程之间的先后关系,子工程之间的先后关系为有向边,这种有向图称为顶点活动网络,即 AOV 网 (Activity On Vertex Network)——OI wiki

事实上,带权拓扑不是说权重会影响拓扑排序先后,而是说我们在处理拓扑排序的同时,也要处理题面上给的一些权值,并在最后输出题目上要求的路径权值和、点权值和、权值差等等。

我们注意到,再进行拓扑排序的过程中,事实上我们肯定会将这个有向无环图遍历一遍,所以我们就不需要排序完后再去跑一遍深搜或广搜了,直接在拓扑过程中处理就好了,相对来说时间复杂度肯定是更优的。

我们看一道例题。(虽然这部分知识每一种类型题肯定是千变万化的,所以一道例题代表不了什么,具体还得按具体题目客制化分析)

P1038 [NOIP 2003 提高组] 神经网络

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e3+86;
ll n , m;
ll C[N],U[N];
struct node{
	ll next , to , w;
}edge[N*N];

ll first[N] , cnt;
inline void add(ll u,ll v, ll w){
	cnt++;
	edge[cnt].next = first[u];
	edge[cnt].to = v;
	edge[cnt].w = w;
	first[u] = cnt;
}

ll indegree[N] , outdegree[N];
queue<ll> que;
ll sum;
inline void toposort(){
	for(ll i = 1;i <= n;i++){
		if(!indegree[i]){
			que.push(i);
		}else{
			C[i] -= U[i];
		}
	}
	while(!que.empty()){
		ll u = que.front();
		//cout << u << endl;
		que.pop();
		if(C[u] < 0) continue;
		
		for(ll i = first[u];i;i= edge[i].next){
			ll v = edge[i].to , w = edge[i].w;
			
			C[v] += C[u] * w;
			// 类似于dp状态转移方程,我下一个去到的点,会受当前点的什么影响
			
			indegree[v]--;
			if(!indegree[v]){
				que.push(v);
			}
		}
	}
	bool o = 0;
	for(ll i = 1;i <= n;i++){
		if(!outdegree[i] && C[i] > 0){
			cout << i << " " << C[i] << endl;
			o = 1;
		}
	}
	if(!o) cout << "NULL";
}

int main() {

	
	cin >> n >> m;
	for(ll i = 1;i <= n;i++){
		cin >> C[i] >> U[i];
	}
	for(ll i = 1;i <= m;i++){
		ll u, v, w;
		cin >> u >> v >> w;
		add(u,v,w);
		indegree[v]++;
		outdegree[u]++;
	}
	toposort();

	return ~~ (0 ^ 0);
}


posted @ 2025-10-16 20:39  Justskr  阅读(6)  评论(0)    收藏  举报