拓扑排序
拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。——OI wiki
实现流程
首先遍历整张图上的顶点,如果一个顶点的入度为 \(0\),将它加入 \(S\);
当 \(S\) 不为空时:
在 \(S\) 中任取一个顶点 \(x\),将 \(x\) 加入到 \(L\) 的队尾,并把 \(x\) 从 \(S\) 中删去;
遍历从 \(x\) 出发的边 \(x → y\),把这条边删掉,如果 \(y\) 的入度变成了 \(0\),则将其加入到 \(S\) 中;
循环结束时,如果所有点都加入了 \(L\),
那么我们就找到了一个合法的拓扑序列,否则可以证明图中存在环;
代码实现
#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()
,可以返回队列大小。
只需要在刚才的代码上加一行特判就可以了。
#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
事实上,带权拓扑不是说权重会影响拓扑排序先后,而是说我们在处理拓扑排序的同时,也要处理题面上给的一些权值,并在最后输出题目上要求的路径权值和、点权值和、权值差等等。
我们注意到,再进行拓扑排序的过程中,事实上我们肯定会将这个有向无环图遍历一遍,所以我们就不需要排序完后再去跑一遍深搜或广搜了,直接在拓扑过程中处理就好了,相对来说时间复杂度肯定是更优的。
我们看一道例题。(虽然这部分知识每一种类型题肯定是千变万化的,所以一道例题代表不了什么,具体还得按具体题目客制化分析)
#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);
}