前言:
引用百度百科的话:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
拓扑排序最有用的一点就是判断当前图是否存在环
简单来说:
一个图的所有节点排序,使得每一条有向边(u,v)对应的u都排在v的前面,实质上它就是对有向图的顶点排成一个线性序列。
举个例子:某校的选课系统规定,每门课可能有若干个先修课,如果要修读某一门课程,则必须要先 修完所有的先修课才能修读。假设一个学生同时只能报一门课程,那么选课系统允许他修完所有课程的顺序就是一个拓扑序。
相关知识点
-
入度,出度:
入度就是:有向图的某个顶点作为终点的次数和。
出度就是:有向图的某个顶点作为起点的次数和。 -
有向无环图 (DAG)
在图论中,有向图用边来描述结点与结点之间的方向关系, 如果一个有向图从任意顶点出发无法经过若干条边回到这个点,则称这个图是一个有向无环图。如图所示,箭头代表边,圆圈代表结点:
-
拓扑排序
- 有向无环图(DAG)才有拓扑排序,非 DAG 图没有拓扑排序。 当有向无环图满足以下条件时:
- 每一个顶点出现且只出现一次
- 若A在序列中排在B的前面,则在图中不存在从B到A的路径。
- 有向无环图(DAG)才有拓扑排序,非 DAG 图没有拓扑排序。 当有向无环图满足以下条件时:
那么就存在拓扑序列!
演示过程
(必须明白一件事,一个图的拓扑排序可以有很多个,有可能不是唯一的,具体应该看题目要求,后面会讲到这些问题,既然有很多个拓扑序列,那么我们一般最关心的就是按照字典序最小的那个序列)
对了,有个非常重要的点,我们平时所遇到最常见的就是正序建立图,在本章的后面会出现一道题,是反向建图,请读者好好理解反向建立图的意义所在!!!
解题套路:
先来了解一下一些性质:
1、 拓扑排序在有向无环图中才能排出有效的序列,否则能判断该有向图有环。
2、如果一开始输入的有向图中的点,不存在入度为0的点,那我们就可以直接判断该有向图存在回路
3、如果存在的入度为0的点大于一个,则该有向图存在多个拓扑序列;但如果每次队列(这里我们用队列存储入度为0的点)中只有一个入度为0的点,那么这个拓扑序列就是唯一的
拓扑排序步骤:
-
首先根据题目中的点进行建图, 这里要注意一下重边的情况,下面的代码中会说到。
-
在有向图中选入度为0的顶点,然后入队列;
-
每次出队后的那个点,可以用vector存储,然后在图中删除该顶点和以它为尾的弧;
-
重复这两步骤,直至全部顶点输出,此时得到无环有向图的所有顶点的拓扑序列;或者图中剩下顶点中再也没有入度为0的顶点(也就是vector容器内的元素个数不等于顶点个数),此时拓扑序列不存在,且图中必有环。
-
输出vector容器内的元素即可,这就是我们所求的拓扑序
我们拿一道简单题目练练手,如下:
# include <iostream>
# include <string>
# include <algorithm>
# include <stack>
# include <queue>
using namespace std;
const int N = 510;
int head[N],to[N*2],nex[N*2],idx;
int du[N];
void add(int x,int y){
nex[++idx] = head[x];
to[idx] = y;
head[x] = idx;
du[y]++;
}
struct cmp{
bool operator()(const int& a,const int& b)const{
return a>b;
}
};
int main(void)
{
int n,m;
while(cin>>n>>m){
//初始化变量以及数组,因为多组输入
for(int i=1;i<=n;++i){
head[i] = 0;
du[i]=0;
}
idx = 0;
//建图,这里用链式前向星,不懂的百度看看
for(int i=1;i<=m;++i){
int x,y;
cin>>x>>y;
add(x,y);
//这里如果重边的话,你也可以不用理睬,就这样写
//但是会引来时间效率的问题;当然你可以去重,
//把元素放进容器内去重,如map,set或者开个二维数组都行
}
//因为这道题要字典序最小,所以用优先队列,每次出来的肯定是最小的字母
priority_queue<int,vector<int>,cmp> q;
//循环遍历,把入度为0的点入队
for(int i=1;i<=n;++i){
if(du[i]==0) q.push(i);
}
vector<int> p;
while(q.size()){
int x = q.top();
q.pop();
p.push_back(x);
for(int i=head[x];i;i=nex[i]){
//去除与该点相连的所有边的入度
int y = to[i];
if( (--du[y]) == 0){
//如果某点的入度为0,继续入队
q.push(y);
// cout<<y<<endl;
}
}
}
//输出拓扑序列
for(int i=0;i<p.size()-1;++i){
cout<<p[i]<<' ';
}cout<<p.back();
cout<<endl;
}
return 0;
}
例题集合
例题一:poj1270 - Following Orders(所有拓扑序列)
题目大意:给出两行字符串,第一行代表有哪些点,第二行有很多个字符,每两个字符A,B代表A<B
问满足条件的所有答案
比如有a b f g四个点
然后a b b f,代表a<b并且b<f
现在要求输出的一个由所有点组成的字符串,并且不能出现b>a和b>f的情况
思路:这道题其实就是求拓扑排序的全排序,即求所有的拓扑序列,也是我们要掌握的知识点,想想之前我们是怎么求全排列的?然后只不过在那思想上增加拓扑序列的要求即可。
这里要注意重边的情况,显然重边我们可以去除掉的,这里我们用二维数组记录每条边,只记录一次,然后后续如果再次出现之前记录过的边,我们就直接continue。(当然你也可以用map或者set去重)
变量声明:
- flag[]数组记录哪个字母是出现过的(这里我们转换成数字来记录)
- used[]数组就是记录当前点是否遍历过
- in[]数组就是记录是否可以走,因为只有入度为0的点才有资格放进目标数组里
- path[]数组存放目标值
#include <cstdio>
#include <cstring>
#define MAXN 30
using namespace std;
char str[1005];
int n,in[MAXN],path[MAXN];
bool g[MAXN][MAXN],flag[MAXN],used[MAXN];
void dfs(int cnt)
{
if(cnt==n+1){
for(int i=1;i<=n;i++)
printf("%c",path[i]+'a');
printf("\n");
}
for(int i=0;i<26;i++){
if(flag[i]&&!used[i]&&!in[i]){
for(int j=0;j<26;j++)
if(g[i][j])in[j]--;
used[i]=true;
path[cnt]=i;
dfs(cnt+1);
used[i]=false;
for(int j=0;j<26;j++)
if(g[i][j])in[j]++;
}
}
}
int main()
{
while(gets(str)){
memset(g,false,sizeof(g));
n=0;
for(int i=0;i<=26;i++)in[i]=flag[i]=used[i]=0; //初始化
for(int i=0;str[i];i++){
if(str[i]!=' '){
flag[str[i]-'a']=true;
n++;
}
}
gets(str);
for(int i=0;str[i];i++){
if(str[i]!=' '){
int x=str[i]-'a',y=str[i+2]-'a';
i=i+2;
if(g[x][y])continue; //重边
g[x][y]=true;
in[y]++;
}
}
dfs(1);
printf("\n");
}
}
例题二:D - 逃生 HDU - 4857 (反向建图)
思路:这道题有点小坑,题目意思中需要特别注意一点,那就是如果a和b没有约束的情况下,应该让编号小的在前面。
那怎么才能实现这一点呢?之前我们是直接正序建图,用优先队列输出字典序最小的拓扑序列,那这里大家是不是也想这么做,那我举个例子:
3 1 3个点,一个关系
3 1 3必须在1前面
如果按照上面的来走,那么输出的答案就是231,很明显是错的,正确答案是312。
这里我们必须反向建图,其实无论你正向建图还是反向建图,输出的序列一定是拓扑序列,只不过这两种输出的拓扑序列有可能是不一样的,我们得根据题目要求去输出人家需要的拓扑序列。
反向建图,用优先队列来做,这里我们让队列每次出来的是字典序大的,就拿上面那个例子来说,由于反向建图,那么1就是在3 前面,队列中就有2和1,这时出来的应该是2,然后1,经过遍历,3入队,3出队,最终答案是213,然后我们反向遍历,得到我们的最终正确答案312。
#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<stdlib.h>
#include<vector>
#include<queue>
#include<string.h>
#include<math.h>
using namespace std;
struct list
{
int u,v,w;
int next;
}edge[110000];
int head[33000];
int nums;
void add(int u,int v,int w)
{
edge[nums].u=u;
edge[nums].v=v;
edge[nums].w=w;
edge[nums].next=head[u];
head[u]=nums++;
}
int du[33000];
void init()
{
memset(head,-1,sizeof(head));
nums=1;
memset(du,0,sizeof(du));
}
priority_queue<int>que;
vector<int>vec;
int n;
void dos()
{
vec.clear();
while(!que.empty())que.pop();
for(int i=1;i<=n;i++)
{
if(du[i]==0)que.push(i);
}
while(!que.empty())
{
int x=que.top();
que.pop();
for(int i=head[x];i!=-1;i=edge[i].next)
{
int y=edge[i].v;
du[y]--;
if(du[y]==0)
{
que.push(y);
}
}
vec.push_back(x);
}
for(int i=n-1;i>=0;i--)
{
if(i!=n-1)printf(" ");
printf("%d",vec[i]);
}
cout<<endl;
}
int main()
{
int T,m,a,b;
scanf("%d",&T);
while(T--)
{
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
add(b,a,1);
du[a]++;
}
dos();
}
return 0;
}
例题三:F - Reward HDU - 2647(反向建图)
思路:这道题也是反向建图,跟上一道题不一样的事,这里没用到优先队列,之所以要反向建图,那是因为要知道最后面的人有哪些,他们的工资是最低的,在他们前面的人工资是+1累加的,所以这里要反向建图,然后bfs层序搜索,因为同一层人的工资是一样的。
# include <iostream>
# include <algorithm>
# include <queue>
using namespace std;
const int N = 1e4+10;
int head[N*2],to[N*2],nex[N*2],idx;
int in_du[N*2];
void add(int x,int y){
nex[++idx] = head[x];
to[idx] = y;
head[x] = idx;
++in_du[y];
}
void inio(int n){
for(int i=1;i<=n;++i){
head[i] = 0;
in_du[i] = 0;
}
idx=0;
}
int main(void)
{
int n,m;
while(cin>>n>>m){
inio(max(n,m));
for(int i=1;i<=m;++i){
int x,y;
cin>>x>>y;
add(y,x);
}
queue<int> que;
for(int i=1;i<=n;++i){
if(in_du[i]==0) que.push(i);
}
long long ans = 0;
int num = 0;
int sk = 888;
while(que.size()){
int len = que.size();
num+=len;
for(int i=1;i<=len;++i){
int k = que.front();
que.pop();
for(int j=head[k];j;j=nex[j]){
int y = to[j];
if((--in_du[y]) == 0) que.push(y);
}
}
ans+=len*(sk++);
}
if(num==n)
cout<<ans<<endl;
else cout<<"-1"<<endl;
}
return 0;
}
# include <iostream>
# include <algorithm>
# include <queue>
using namespace std;
const int N = 1e4+10;
int head[N*2],to[N*2],nex[N*2],idx;
int in_du[N*2];
void add(int x,int y){
nex[++idx] = head[x];
to[idx] = y;
head[x] = idx;
++in_du[y];
}
void inio(int n){
for(int i=1;i<=n;++i){
head[i] = 0;
in_du[i] = 0;
}
idx=0;
}
int main(void)
{
int n,m;
while(cin>>n>>m){
inio(max(n,m));
for(int i=1;i<=m;++i){
int x,y;
cin>>x>>y;
add(y,x);
}
queue<int> que;
for(int i=1;i<=n;++i){
if(in_du[i]==0) que.push(i);
}
long long ans = 0;
int num = 0;
int sk = 888;
while(que.size()){
int len = que.size();
num+=len;
for(int i=1;i<=len;++i){
int k = que.front();
que.pop();
for(int j=head[k];j;j=nex[j]){
int y = to[j];
if((--in_du[y]) == 0) que.push(y);
}
}
ans+=len*(sk++);
}
if(num==n)
cout<<ans<<endl;
else cout<<"-1"<<endl;
}
return 0;
}
例题四:E - Rank of Tetris HDU - 1811 (并查集+拓扑排序)
思路:这里用到并查集是要判断在集合里的点是否有不符合的情况,比如a==b,但后面又出现a>b,很明显就是错的;之后我们只要拿并查集当中集合的老大以及那些x>y这些拿去建图,然后用拓扑排序解答。
这里其实是求此拓扑序是否唯一(如果存在的话),那么我们在那个队列出队的情况下加上一个判断语句,如果每次当前的队列的长度是1,那么就可以证明此拓扑序列是唯一的
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
const ll INF=0x7fffffffffffffff;
const int mod=1e9+7;
const int maxn = 1e6+5;
int re[maxn],a[maxn],b[maxn],in[maxn];
char ch[maxn];
int n,m;
vector<int>edge[maxn];
int flag_CONFLICT,flag_UNCERTAIN;
//冲突 拓扑排序不唯一
void init()
{
flag_CONFLICT=flag_UNCERTAIN=0;
for(int i=1; i<=n; i++)
{
re[i]=i;
in[i]=0;
edge[i].clear();
}
}
int fin(int x)
{
return re[x]==x?x:re[x]=fin(re[x]);
}
void merg(int x,int y)
{
int fx=fin(x);
int fy=fin(y);
if(fx!=fy)
re[fy]=fx;
}
void add(int x,int y)
{
edge[x].push_back(y);
}
void tupo()
{
int sum=0;//节点数
queue<int>q;
for(int i=1;i<=n;i++)
{
if(fin(i)==i)
{
sum++;
if(in[i]==0)
q.push(i);
}
}
while(!q.empty())
{
if(q.size()>1) flag_UNCERTAIN=1;
int now=q.front();q.pop();
sum--;
for(auto v:edge[now])
{
if(--in[v]==0)
q.push(v);
}
}
if(sum!=0)
flag_CONFLICT=1;
}
int main()
{
ios;
while(cin>>n>>m)
{
init();
for(int i=1;i<=m;i++)
{
cin>>a[i]>>ch[i]>>b[i];
a[i]++,b[i]++;
if(ch[i]=='=')
merg(a[i],b[i]);
}
for(int i=1;i<=m;i++)//建立图
{
if(ch[i]!='=')
{
int fx=fin(a[i]),fy=fin(b[i]);
if(fx==fy)
{
flag_CONFLICT=1;
break;
}
if(ch[i]=='<')
swap(fx,fy);
add(fx,fy);
in[fy]++;
}
}
if(flag_CONFLICT)
{
cout<<"CONFLICT\n";
continue;
}
tupo();
if(flag_CONFLICT)
cout<<"CONFLICT\n";
else if(flag_UNCERTAIN)
cout<<"UNCERTAIN\n";
else
cout<<"OK\n";
}
}