拓扑排序
拓扑排序要解决的问题是给一个有向无环图
的所有节点排序
。
即在 A O E AOE AOE网中找关键路径
。
前置芝士!
- 有向图:有向图中的每一个边都是
有向边
,即其中的每一个元素都是有序二元组。在一条有向边 ( u , v ) (u,v) (u,v)中,称 u u u是 v v v的直接前驱
, v v v是 u u u的直接后继
。- 有向无环图 ( D i r e c t e d A c y c l i c G r a p h ) (Directed\ Acyclic\ Graph) (Directed Acyclic Graph):没有
有向环
,但不保证原图变成无向图时无环。
- D A G DAG DAG的性质:能拓扑的图一定是 D A G DAG DAG, D A G DAG DAG一定能拓扑。( A O E 和 A O V AOE和AOV AOE和AOV网都是 D A G DAG DAG)
- 度:顶点 v v v的度数是与 v v v关联的边数。有向图中没有度的概念。
- 入度、出度:入度是指以该顶点为
终点
的有向边数量;顶点的出度是指以顶点为起点
的有向边数量。无向图中没有出入度的概念。
举个例子!
我们可以拿大学排课的例子来描述这个过程,比如学习大学课程中有:程序设计,算法语言,高等数学,离散数学,编译技术,数据结构,数据库系统等。当我们想要学习
数据结构
的时候,就必须先学会离散数学
、编译技术
和算法语言
。这些课程就相当于几个顶点 u u u, 顶点之间的有向边 ( u , v ) (u,v) (u,v)就相当于学习课程的顺序。教务处安排这些课程,使得在逻辑关系符合的情况下排出课表,就是拓扑排序的过程。
但是如果某一天排课的老师打瞌睡了,说想要学习
数据结构
,还得先学操作系统
,而操作系统
的前置课程又是数据结构
,那么到底应该先学哪一个(不考虑同时学习的情况)?在这里数据结构
和操作系统
间就出现了一个环,显然你现在没办法弄清楚你需要先学什么了,于是你也没办法进行拓扑排序了。
拓扑排序即,在一个 D A G DAG DAG(有向无环图)中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点u到v的有向边 ( u , v ) (u,v) (u,v), 都可以有 u u u在 v v v的前面。
严谨来说,给定一个 D A G DAG DAG,如果从 i i i到 j j j有边,则认为 j j j依赖于 i i i。如果 i i i到 j j j有路径( i i i可达 j j j ),则称 j j j间接依赖
于 i i i。拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点
。
理论存在!
- 从图中选择一个
入度为零
的点。- 记录该顶点,从图中删除此顶点及其所有的出边。
重复上面两步,直到所有顶点都输出,拓扑排序完成,此时我们可以得到一个点的出队顺序(遍历顺序)这个序列叫
拓扑序
;或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成。拓扑排序需要遍历整张图,假设该图为 G ( V , E ) G(V,E) G(V,E),获得拓扑序的时间复杂度大约是
O(V+E)
。
实践开始!
void topu_sort() {queue<int>q;for (int i = 1; i <= n; ++i) {if (!d[i])q.push(i);}while (!q.empty()) {int now = q.front();q.pop();ans.push_back(now);int len = vec[now].size();for (int i = 0; i < len; ++i) {int to = vec[now][i];d[to]--;if (!d[to]) q.push(to);}}/*for(auto x:vec[now]){d[x]--;if(!d[x]) q.push(x);}*/ }
例题
拓扑模板:B3644 【模板】拓扑排序 / 家谱树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 150;
int n;
vector<int> e[N];
int d[N];
vector<int> ans;void topu() {queue<int> q;for (int i = 1; i <= n; ++i) {if (d[i] == 0) q.push(i);}while (!q.empty()) {int now = q.front();q.pop();ans.push_back(now);for (auto x: e[now]) {d[x]--;if (d[x] == 0) q.push(x);}}
}int main() {cin >> n;for (int i = 1; i <= n; ++i) {int x;while (cin >> x) {if (x == 0) break;d[x]++;e[i].push_back(x);}}topu();for (auto x: ans) {cout << x << " ";}cout << '\n';return 0;
}
提高
拓扑构造:Problem - E - Codeforces
解析:
先忽略所有输入的无向边(但是不忽略点),对当前的有向图拓扑排序,如果给定的有向图中已经不是 D A G DAG DAG显然是一定不成立也无法构造的;如果当前的图是 D A G DAG DAG,那么拓扑完我们可以得到一个或多个拓扑序,每个拓扑序都是
线性
的,这也代表当选定一个拓扑序,在所有点中任选两个点,他们一定有已知确定先后顺序,我们按照这样的顺序去构造,建边(每次令拓扑序靠前的指向靠后的)即可保证不会出现拓扑序靠前的点依赖于靠后的点,则不会出现有向环。AC代码:
pii ans[N]; vector<int>e[N]; int d[N]; int n,m,order=0; int ord[N];bool topu(){queue<int>q;for(int i=1;i<=n;++i){if(!d[i]){q.push(i);}}while(!q.empty()){int t=q.front();q.pop();ord[t]=++order;for(auto x:e[t]){d[x]--;if(!d[x])q.push(x);}}if(order!=n)return 0;return 1; }void work() {cin>>n>>m;int tot=0;order=0;for(int i=1;i<=n;++i){d[i]=0;e[i].clear();ans[i].fi=ans[i].se=0;}for(int i=1;i<=m;++i){int z,u,v;cin>>z>>u>>v;if(z){e[u].pb(v);d[v]++;}else{ans[++tot].fi=u;ans[tot].se=v;}}if(!topu()){cout<<"No\n";return;}cout<<"Yes\n";for(int i=1;i<=tot;++i){if(ord[ans[i].fi]>ord[ans[i].se])swap(ans[i].se,ans[i].fi);cout<<ans[i].fi<<" "<<ans[i].se<<'\n';}for(int i=1;i<=n;++i){if(e[i].size()){for(auto x:e[i]){cout<<i<<" "<<x<<'\n';}}} }
课后练习:P1347 排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
总结:
拓扑排序是图论中非常基础的算法,大多时候作为工具,或者一些进阶算法的预处理内容,非常简单,同时需要熟练掌握。