序言:
当你自始至终都用孩子可以理解的简单的语言写出一个想法,那么你便迫使自己在更深层次上理解了该概念,并简化了观点之间的关系和联系。
--《费曼学习法》
1.拓扑排序的方法
选取入度为0的点,加入队列,并删掉所有以该点为起点的边
如图所示:
从第二幅图开始选择B或者C都可以,因为此时他们入度都为,所以拓扑排序不唯一
引入题目:
给定一个 n个点 m 条边的有向图,点的编号是 1到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x在 A 中都出现在 y之前,则称 A 是该图的一个拓扑序列。
输入格式
第一行包含两个整数 n 和 m。
接下来 m行,每行包含两个整数 x和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1。
数据范围
1≤n,m≤1e5
输入格式:
3 3 1 2 2 3 1 3
输出:
1 2 3
2.来一个错误示范
void solve()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
cin >> a >> b;
if(a != b)
{
add(a, b);
d[b]++;
}
}
topsort();
}
笔者之前都是记住理解记忆模板,然而很久过后再凭借自己的理解和记忆来做,就出了一个很难发现的问题,因为我注意到题目的自环问题所以我在代码中加了if(a!=b)的自环判断但这是不必要且错误的
理由:
在拓扑排序问题中,自环是可以的,因为它们不会影响到排序的结果。实际上,如果你的输入数据中包含自环,但你的代码却不处理自环,那么你的代码可能会得到错误的结果。因为你没有把自环添加到图中,所以你的
d
数组(记录每个节点的入度)可能会比实际的值小,这可能会导致你的代码错误地认为某个节点没有前驱节点,从而提前把它加入到队列中。
3.正确代码及其注释
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 2e5 + 10;//经验规律开两倍以上
int h[N], e[N], ne[N], d[N], top[N], idx;
int n, m;
int cnt = 0;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void topsort()
{
queue<int>q;
for(int i=1;i<=n;i++)
if(d[i]==0)//遍历入度为0的点入队
q.push(i);
while(q.size())
{
int t=q.front();
q.pop();
top[++cnt]=t;//注意这里是前置++
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(--d[j]==0)//前置--,偷懒先自减了再判断入度是否为0
{
q.push(j);
}
}
}
//判断是否所有节点都被访问过
if(cnt==n)//因为前置++,所以下标从1开始,所以是cnt==n,从0开始的话cnt==n-1
{
for(int i=1;i<=n;i++)
cout<<top[i]<<" ";
}
else
cout<<-1<<endl;
}
void solve()
{
cin >> n >> m;
memset(h, -1, sizeof h);//切记勿忘
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
d[b]++;//统计入度为0的点
}
topsort();
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);//经典三件套解除绑定,加快cin的输入,不过不能用scanf,printf
solve();
return 0;
}
注:如果错误还请斧正