拓扑排序基础讲解

拓扑排序(TopSort)

2021年8月5日

1、算法原理

1.1 一个问题

在日常生活中,我们的某些行为需要有先后顺序。比如,你需要先穿袜子再穿鞋,先拿起手表再戴手表,顺序无法改变。

同理在一个工程中,每一步的工作都需要先后顺序,某些工作的完成是另一些工作的前置条件。我们称这每一步的工作为事件。

那么,若已知每个事件的前置事件,怎么确定其完成的先后顺序?

我们可以构建一个网络,将每个事件的前置事件放置在这个事件之前,并用有向线段连接,由此可以直观地看出我们需要完成事件的先后顺序。

此时构建的网络,通常被称为顶点活动网(Activity On Vertex network),简称AOV网。

1.2 如何通过计算机输出排序结果?

拓扑排序就是针对这一类问题进行的排序算法。

建立上述AOV网之后,我们可对每个事件所构成的结点进行入度的记录。

首先,将所有入度为0的结点加入队列(这些事件不需要前置事件),对其后续结点进行遍历。遍历到一个结点后,此结点的入度就减1,当入度减为0,加入队列(前置事件均已完成),对其后续结点进行遍历。

当所有结点遍历过后,就可以知道其先后顺序了。

1.3 图示

在这里插入图片描述

首先,将入度为0的1,2,4结点加入队列,首先遍历1后的结点:

在这里插入图片描述

此时,结点3的入度减去1,变为1,不等于0,先不进行操作,随后,对2后的结点进行遍历:

在这里插入图片描述

此时,结点3的入度减为0,加入队列。

接着,如下操作:

在这里插入图片描述

在这里插入图片描述

所有结点遍历完毕,其先后顺序即为 1 2 4 3 5 6。

1.4 不稳定性

拓扑排序无其他排序条件时是不稳定的,与你加入队列的顺序与方式有关,比如上面图示样例中也可写成1 4 2 3 5 6,4 1 2 3 5 6等等。

因为其在实际问题中,同层的先后顺序是无关事件,因此不影响其正确性,若对排序有另外的要求,则按照要求的方式加入队列即可。

1.5 无法排序?

当图是有环图,意味着一定有结点无法将其入度减为0,因此无法进行其后续结点的排序,如图:

在这里插入图片描述

此时不存在入度为0的点,因此无法得知其先后顺序。

因此,我们需要在进行排序时,判断所有遍历过的点数目是否等于所有点的数目,若不等于,则表示其存在环,排序失败,若等于,则排序成功。

1.6 时间复杂度

因每个结点只需加入队列一次,而每个结点加入队列前需要进行入度数量次的操作,因此时间复杂度为O(n+m)。

2、算法实现

2.1 题目

给你n个有向边和m个点,若能进行拓扑排序,输出排序后的序列,若不能,输出-1.

输出为1行,答案可能不唯一,输出正解中任意一种。

2.2 解法分析

首先,记录所有点的入度,此时设置数组(或其他记录方式)inp[],在每次输入时,对后一个事件的入度进行加1,并记录前一个事件的后续事件:

for(int i = 0; i < n; i++){
    cin>>a>>b;
    inp[b]++;
    v[a].push_back(b);
}

随后,将每个入度为0的结点加入队列:

queue<int>q;
for (int i = 1; i <= m; i++) {//注意点的标号从几开始
	if (!inp[i])q.emplace(i);//为0则加入队列
}

接着,对图按照BFS进行遍历:

while (!q.empty()) {
    int fa = q.front();
    q.pop();
    pd++;//点数量的记录
    ans[num++] = fa;//记录顺序
    for (auto now : v[fa]) {
        inp[now]--;//入度减一
        if (!inp[now]) {//为0则加入队列
            q.emplace(now);
        }
    }
}

最后对是否排序成功进行判断:

if(pd==m)return 1;
else return 0;
2.3 完整代码
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<vector>
#include<queue>

using namespace std;

int inp[1005];
int n, m, pd;
vector<int>v[1005];
int ans[1005];

bool bfs() {
	queue<int>q;
	for (int i = 1; i <= m; i++) {
		if (!inp[i])q.emplace(i);//为0则加入队列
	}
    while (!q.empty()) {
        int fa = q.front();
        q.pop();
        ans[pd++] = fa;
        for (auto now : v[fa]) {
            inp[now]--;//入度减一
            if (!inp[now]) {//为0则加入队列
                q.emplace(now);
            }
        }
    }
    if (pd == m)return true;
    else return false;
}

int main() {
    int a, b;
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        cin >> a >> b;
        inp[b]++;
        v[a].push_back(b);
    }
    if (bfs()) {
        cout << ans[0];
        for (int i = 1; i < m; i++){
            cout << ' ' << ans[i];
        }
    }
    else {
        cout << -1;
    }
    cout << endl;
	return 0;
}

样例输入1:

5 6
1 3
2 3
3 5
4 5
5 6

预期结果1:

1 2 4 3 5 6

测试结果1:

在这里插入图片描述

样例输入2:

6 6
1 2
2 3
3 4
4 5
5 2
4 6

预期结果2:

-1

测试结果2:

在这里插入图片描述

以上样例均通过。

几道例题

A:POJ - 2367 Genealogical tree

Description

The system of Martians’ blood relations is confusing enough. Actually, Martians bud when they want and where they want. They gather together in different groups, so that a Martian can have one parent as well as ten. Nobody will be surprised by a hundred of children. Martians have got used to this and their style of life seems to them natural.
And in the Planetary Council the confusing genealogical system leads to some embarrassment. There meet the worthiest of Martians, and therefore in order to offend nobody in all of the discussions it is used first to give the floor to the old Martians, than to the younger ones and only than to the most young childless assessors. However, the maintenance of this order really is not a trivial task. Not always Martian knows all of his parents (and there’s nothing to tell about his grandparents!). But if by a mistake first speak a grandson and only than his young appearing great-grandfather, this is a real scandal.
Your task is to write a program, which would define once and for all, an order that would guarantee that every member of the Council takes the floor earlier than each of his descendants.

Input

The first line of the standard input contains an only number N, 1 <= N <= 100 — a number of members of the Martian Planetary Council. According to the centuries-old tradition members of the Council are enumerated with the natural numbers from 1 up to N. Further, there are exactly N lines, moreover, the I-th line contains a list of I-th member’s children. The list of children is a sequence of serial numbers of children in a arbitrary order separated by spaces. The list of children may be empty. The list (even if it is empty) ends with 0.

Output

The standard output should contain in its only line a sequence of speakers’ numbers, separated by spaces. If several sequences satisfy the conditions of the problem, you are to write to the standard output any of them. At least one such sequence always exists.

Sample Input

5
0
4 5 1 0
1 0
5 3 0
3 0

Sample Output

2 4 5 3 1

分析

此题输入第i个结点的后续结点,直接建图拓扑排序即可。

代码

#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<vector>
#include<queue>

using namespace std;

int inp[1005], n, pd, num;
vector<int>v[1005];
int ans[1005];

bool bfs() {
    queue<int>q;
    for (int i = 1; i <= n; i++) {
        if (!inp[i])q.emplace(i);//为0则加入队列
    }
    while (!q.empty()) {
        int fa = q.front();
        q.pop();
        ans[pd++] = fa;
        for(int i = 0; i < v[fa].size(); i++){
            int now = v[fa][i];
            inp[now]--;//入度减一
            if (!inp[now]) {//为0则加入队列
                q.emplace(now);
            }
        }
    }
    if (pd == n)return true;
    else return false;
}

int main() {
    int a;
    cin >> n;
    for (int i = 0; i < n; i++) {
        while (cin >> a, a != 0) {
            v[i + 1].push_back(a);
            inp[a]++;
        }
    }
    if (bfs()) {
        cout << ans[0];
        for (int i = 1; i < num; i++) {
            cout << ' ' << ans[i];
        }
    }
    else {
        cout << -1;
    }
    cout << endl;
    return 0;
}
B:HDU - 1285 确定比赛名次

Problem Description

有N个比赛队(1<=N<=500),编号依次为1,2,3,。。。。,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道每场比赛的结果,即P1赢P2,用P1,P2表示,排名时P1在P2之前。现在请你编程序确定排名。

Input

输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示队伍的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即P1队赢了P2队。

Output

给出一个符合要求的排名。输出时队伍号之间有空格,最后一名后面没有空格。

其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。

Sample Input

4 3
1 2
2 3
4 3

Sample Output

1 2 4 3

分析:

此题要求队伍编号小的队伍在前,因此需要使用优先队列,然后建图拓扑排序即可。

代码:

#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<vector>
#include<queue>
#include<functional>

using namespace std;

int inp[505], n, m;
vector<int>v[505];
int ans[505];

void bfs() {
    //queue<int>q;
    int pd = 0;
    priority_queue<int, vector<int>, greater<int>>q;//因题目要求,此处采用优先队列
    for (int i = 1; i <= n; i++) {
        if (!inp[i])q.emplace(i);//为0则加入队列
    }
    while (!q.empty()) {
        int fa = q.top();
        q.pop();
        ans[pd++] = fa;
        for(int i = 0; i < v[fa].size(); i++){
            int now = v[fa][i];
            inp[now]--;//入度减一
            if (!inp[now]) {//为0则加入队列
                q.emplace(now);
            }
        }
    }
}

int main() {
    int a, b;
    while (cin>>n>>m) {
        memset(inp, 0, sizeof inp);
        memset(v, 0, sizeof v);
        for (int i = 0; i < m; i++) {
            cin >> a >> b;
            inp[b]++;
            v[a].push_back(b);
        }
        bfs();
        cout << ans[0];
        for (int i = 1; i < n; i++) {
            cout << ' ' << ans[i];
        }
        cout << '\n';
    }
    return 0;
}
C:HDU - 2094产生冠军

Problem Description

有一群人,打乒乓球比赛,两两捉对撕杀,每两个人之间最多打一场比赛。
球赛的规则如下:
如果A打败了B,B又打败了C,而A与C之间没有进行过比赛,那么就认定,A一定能打败C。
如果A打败了B,B又打败了C,而且,C又打败了A,那么A、B、C三者都不可能成为冠军。
根据这个规则,无需循环较量,或许就能确定冠军。你的任务就是面对一群比赛选手,在经过了若干场撕杀之后,确定是否已经实际上产生了冠军。

Input

输入含有一些选手群,每群选手都以一个整数n(n<1000)开头,后跟n对选手的比赛结果,比赛结果以一对选手名字(中间隔一空格)表示,前者战胜后者。如果n为0,则表示输入结束。

Output

对于每个选手群,若你判断出产生了冠军,则在一行中输出“Yes”,否则在一行中输出“No”。

Sample Input

3
Alice Bob
Smith John
Alice Smith
5
a c
c d
d e
b e
a d
0

Sample Output

Yes
No

分析:

此题是考察拓扑排序的性质,即若冠军只有一人的话,则初始化图的入度为0的仅有一个结点,因此直接枚举入度为0结点数量即可。

代码:

#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<string>
#include<map>

using namespace std;

map<string, int>mp;

bool solve(int n) {
	int poc = 0;
	for (auto x : mp) {
		if (!x.second) {
			poc++;
		}
	}
	if (poc == 1)
		return 1;
	else
		return 0;
}
int main() {
	int n;
	while (cin >> n, n != 0) {
		mp.clear();
		string k, l;
		for (int i = 0; i < n; i++) {
			cin >> k >> l;
			mp[k] = mp[k];
			mp[l]++;
		}
		if (solve(n))
			cout << "Yes\n";
		else
			cout << "No\n";
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值