图论--------拓扑排序相关讲解以及题目

前言:

引用百度百科的话:

对一个有向无环图(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的路径。

那么就存在拓扑序列!

演示过程
在这里插入图片描述

(必须明白一件事,一个图的拓扑排序可以有很多个,有可能不是唯一的,具体应该看题目要求,后面会讲到这些问题,既然有很多个拓扑序列,那么我们一般最关心的就是按照字典序最小的那个序列

对了,有个非常重要的点,我们平时所遇到最常见的就是正序建立图,在本章的后面会出现一道题,是反向建图,请读者好好理解反向建立图的意义所在!!!


解题套路:

先来了解一下一些性质:

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";
    }
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值