拓扑排序(邻接矩阵,邻接表,附4例题)


有向无环图

有向无环图指的是一个无回路的有向图。如果有一个非有向无环图,且A点出发向B经C可回到A,形成一个环。将从C到A的边方向改为从A到C,则变成有向无环图。有向无环图的生成树个数等于入度非零的节点的入度积

拓扑排序

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
加油

例题

例1:

P4017 最大食物链计数

思路

  • 当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队(当一个点的入度变为零,即所有它能吃的东西都已经搜索过了,这是它的数值就不会发生变化,就可以入队了。这样保证了队列里的所有数值都不会发生变化)
  • 从入度为零的点开始查找,出度为零的点的路径总数和就是答案

例2:

可达性统计

思路:

设从点 x 出发能够到达的点构成的集合是 c(x),从点 x 出发能够到达的点,是从 x 的各个后继节点 y 出发能够到达的点的并集,再加上点 x 自身,先按照拓扑排序算法求出拓扑序,然后按照拓扑序的倒叙进行计算------因为在拓扑序中,任意一条边 (x , y),x 都排在 y 之前。

例3:

Following Orders

题意:

给定几种点之间的顺序,要你按字典序输出所有可能的拓扑排序

例4:

Legal or Not

题意:

给你一个有向图,判断他是否有环路

邻接矩阵

适用条件

顶点数目不太大(一般不超过1000)的题目

板子

int n,m,in[maxn],out[maxn],a,b;//n个点,m条边
int mp[maxn][maxn],ar[maxn];//邻接矩阵存有向图 
for(int i=1;i<=m;i++){
    scanf("%d%d",&a,&b);
    mp[a][b]=1;//a->b
    out[a]++; in[b]++;
}
void toposort(){
    static int cnt=0;
    queue<int> q;
    for(int i=1;i<=n;i++)
        if(in[i]==0) q.push(i);//把所有入度为0的节点(起始点)加入队列 
    while(!q.empty()){
        int a=q.front(); q.pop();
        ar[++cnt] = a;//求出拓扑序
        for(int k=1;k<=n;k++){//遍历所有从起始点出发的边 
            if(mp[a][k]==0)
                continue;        
            in[k]--;//令边到达的顶点的入度-1
            if(in[k]==0)q.push(k);//当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队     
        }
    }
}

例1code

#include<bits/stdc++.h>
#define maxn 5010
#define mod 80112002
using namespace std;
int n,m,in[maxn],out[maxn],a,b,f[maxn],ans;
int mp[maxn][maxn];//邻接矩阵存有向图 
queue<int> q;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&a,&b);
        mp[a][b]=1;
        out[a]++; in[b]++;
    }
    for(int i=1;i<=n;i++){
        if(in[i]==0) {
            f[i]=1;//每一个起点代表路径的开始 
            q.push(i);
        }
    }//把所有入度为0的节点(起始点)加入队列 
    while(!q.empty()){
        int a=q.front(); q.pop();
        for(int k=1;k<=n;k++){//遍历所有从起始点出发的边 
            if(mp[a][k]==0)
                continue;           
            f[k]=(f[k]+f[a])%mod;
            in[k]--;//令边到达的顶点的入度-1
            if(in[k]==0){//当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队
                if(out[k]==0){//发现终点 
                    ans=(ans+f[k])%mod;
                    continue;
                }
                q.push(k);
            }
        }
    }
    printf("%d",ans); 
    return 0;
}

例3code

#include<iostream>
#include<cstdio>
#include<map>
#include<cstring>
#include<algorithm>
using namespace std;
const int ma=50;
char a[ma],b[ma],c[ma];
char d[ma],e[ma];
char mp[ma][ma];//邻接矩阵 
map<char,int> m;
void init(){
 memset(d,0,sizeof(d));
    memset(e,0,sizeof(e));
    memset(mp,0,sizeof(mp));
}
void f(int rk,int k){
    if(rk==k)
        printf("%s\n",e);//搜索完毕    
    else{
        for(int i=0; i<k; i++){
            if(d[i]==0){//这个点是起点 
                d[i]--;
                e[rk]=c[i];//储存结果,类似八皇后 
                for(int j=0; j<k; j++)if(mp[i][j])d[j]--;//令边到达的顶点的入度-1                                    
                f(rk+1,k);
                d[i]++;
                for(int j=0; j<k; j++)if(mp[i][j])d[j]++;//回溯                                 
            }
        }
    }
}
int main(){
    while(gets(a)){
        init();
        int len=strlen(a),k=0;
        for(int i=0; i<len; i++){
            if(a[i]>='a'&&a[i]<='z'){
                c[k++]=a[i];
            }
        }//分离字母的一种方法
        sort(c,c+k);//保证后面字典序输出 
        for(int i=0; i<k; i++)m[c[i]]=i;//给字母依次编号      
        gets(b);
        len=strlen(b);
        for(int i=0; i<len; i+=4){
            mp[m[b[i]]][m[b[i+2]]]=1;//有向边 
            d[m[b[i+2]]]++;//记录点0,1,2,3,…的入度数
        }
        f(0,k);
        printf("\n");
    }
}

邻接表(前向星)

邻接表的形式说明

  • 邻接表是一个二维容器,第一维描述某个点,第二维描述这个点所对应的边集们
  • 实现邻接表的方法绝对有100种以上,即使是前向星这种东西也是邻接表,因为它还是描述某个点和这个点所对应的边集们。
  • 第一维是描述点的。可以用vector,list,forward_list,deque,map,multimap,unordered_map,unordered_multimap等(一般不能用set,mutiset,unordered_set,unordered_multiset).按照要求去选择。一般来讲存完图以后不涉及点的加入与删除优先使用vector.map,multimap,unordered_map,unordered_multimap.
  • 第二维是描述这个点的边集,可以用全部的容器。同样,一般来讲存完图以后,不涉及点的加入与删除优先使用vector,空间充足可以考虑deque.涉及点的删除用forward_list或者是list,map,multimap,unordered_map,unordered_multimap.

异或(仅科普)

  • 异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
  • 如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。

成对变换(仅科普)

  • 通过计算可以发现,对于非负整数n:
    当n为偶数时,n xor 1等于n+1。
    当n为奇数时,n xor 1等于n-1。
    因此,“0与1”“2与3”“4与5”…关于xor 1运算构成“成对变换”。这一性质经常用于图论邻接表中边集的存储。在具有无向边(双向边)的图中把一对正反方向的边分别存储在邻接表数组的第n与n+1位置(其中n为偶数),就可以通过xor 1的运算获得与当前边(x,y)反向的边(y,x)的存储位置。

  • 对于无向图,我们把每条无向边看作两条有向边插入即可。有一个小技巧是,结合“成对变换”的位运算性质,我们可以在程序最开始时,初始化变量tot=1。这样每条无向边看成的两条有向边会成对存储在ver 和edge 数组的下标“2和3” “4和5” “6和7”…的位置上。通过对下标进行xor 1的运算,就可以直接定位到与当前边反向的边。换句话说,如果ver[i]是第i条边的终点,那么ver[i xor 1]就是第i条边的起点。

前向星(重点)

一种数据结构,以储存边的方式来存储图。构造方法如下:读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序(可以使用基数排序等)。通常用在点的数目太多,或两点之间有多条弧的时候。一般在别的数据结构不能使用的时候才考虑用前向星。除了不能直接用起点终点定位以外,前向星几乎是完美的。
在这里插入图片描述
前向星(用数组模拟链表的方式存储带权有向图的邻接表结构)code:

//加入有向边(x,y),权值为z 
void add(int x,int y,int z){
    ver[++tot]=y,edge[tot]=z;//真实数据 
    next[tot]=head[x],head[x]=tot;//在表头x处插入 
} 
//访问从x出发的所有边
for(int i=head[x];i;i=next[i]){
    int y=ver[i],z=edge[i];
    //找到了一条有向边(x,y),权值为z 
} 

板子

int head[maxn],in[maxn],out[maxn];
struct Edge{
    int to,nxt;
}edge[maxe];

void add(int x,int y){
    cnt++; 
    edge[cnt].to=y;
    edge[cnt].nxt=head[x];//巧妙之处:将一个顶点的多个边关联起来 
    head[x]=cnt;//head[x]是以点x为起点的最后输入的边的序号
    in[y]++;out[x]++;
}//可以通过边找同源边 

for(int i=head[v];i;i=edge[i].nxt){//i是以点v为起点的边的序号 
    int k=edge[i].to;
    in[k]--;
    if(in[k]==0) q.push(k);
}

例1code

//通过边找同源边的做法 
#include<bits/stdc++.h>
#define maxn 5010
#define maxe 500010
#define mod 80112002
using namespace std;
int n,m,cnt=0;
struct Edge{
    int to,nxt;
}edge[maxe];
int head[maxn],in[maxn],out[maxn],sum[maxn];
void add(int x,int y){
    cnt++; 
    edge[cnt].to=y;
    edge[cnt].nxt=head[x];//巧妙之处:将一个顶点的多个边关联起来 
    head[x]=cnt;//head[x]是以点x为起点的最后输入的边的序号
}//可以通过边找同源边 
int main(){
    scanf("%d%d",&n,&m);
    int x,y;//x->y 
    for(int i=1;i<=m;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
        in[y]++;out[x]++;
    }
    queue<int> q;
    //找到入度为0的点
    for(int i=1;i<=n;i++){
        if(!in[i])  q.push(i),sum[i]=1;//令到其的路径数量为1 
    }
    //sum数字记录每个点的路径数目
    int ans=0;
    while(!q.empty()){
        int v=q.front(); q.pop();
        for(int i=head[v];i;i=edge[i].nxt){//i是以点v为起点的边的序号 
            int k=edge[i].to;
            sum[k]=(sum[k]+sum[v])%mod; 
            in[k]--;
            if(in[k]==0) q.push(k);
        }
    }//删点时,将需要删除的点的答案 都累加到 它可以到达的点 上面去 
    for(int i=1;i<=n;i++){
        if(out[i]==0)//只记录是极大路径(不能走下去)的数目 
        ans=(ans+sum[i])%mod;
    }
    cout<<ans<<endl;
}

例2code

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <queue>
#include <bitset>
#include <algorithm>
using namespace std;
const int maxn = 30000+7;
int head[maxn],ver[maxn],Next[maxn],deg[maxn];
int a[maxn];
int n,m;
bitset<maxn> c[maxn];
void add(int x, int y){
    static int tot=0;
    ver[++tot] = y;
    Next[tot] = head[x];
    head[x] = tot;
    deg[y]++;//y的入度(degree)
}
void toposort(){
    static int cnt=0;
    queue<int> q;
    for(int i = 1; i <= n; i++)
        if(deg[i] == 0) q.push(i);
    while(q.size()){
        int x = q.front(); q.pop();
        a[++cnt] = x;//求出拓扑序
        for(int i = head[x]; i; i = Next[i]){
            int y = ver[i];
            deg[y]--;
            if(deg[y] == 0) q.push(y);
        }
    }
}
void solve(){
    int x, y;
    for(int i = n; i >= 1; i--){
        x = a[i];
        c[x][x] = 1;
        for(int j = head[x]; j; j = Next[j]) {
            int y = ver[j];
            c[x] |= c[y];
        }
    }
}
int main(){
    int x, y;
    scanf("%d %d", &n, &m);
    while(m--){
        scanf("%d %d", &x, &y);
        add(x, y);
    }
    toposort();
    solve();
    for(int i = 1; i <= n; i++)
        printf("%d\n",c[i].count());//count函数用来求bitset中1的位数
    return 0;
}

例4code

#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector> 
#include <queue>
using namespace std;
const int nmax=110;
int inDegree[nmax];//存每个节点的入度
vector<int>G[nmax];//邻接表 
queue<int>q;
int n,m;//点数,边数 
bool toposort(){
    int num=0;//加到拓扑序列的顶点数  
    while(!q.empty()) q.pop();
    for(int i=0;i<n;i++)
        if(inDegree[i]==0)
            q.push(i);//把入度为0的顶点扔进队列        
    
    while(!q.empty()){
        int u=q.front();
        q.pop();//删除该点
        num++;
        for(int i=0;i<G[u].size();i++){//遍历与点u相邻的所有边,删除之。 
            int v=G[u][i];
            inDegree[v]--;
            if(inDegree[v]==0)
                q.push(v);           
        } 
        G[u].clear();//(若无必要可不写)
    }
    if(num==n) return true;
    else return false;//有环路 
}
int main(){
    while(scanf("%d %d",&n,&m)!=EOF){
        if(n==0)
            break;      
        memset(inDegree,0,sizeof(inDegree));
        for(int i=0;i<n;i++)G[i].clear();
        
        int u,v;
        for(int i=0;i<m;i++){
            scanf("%d %d",&u,&v);
            G[u].push_back(v);
            inDegree[v]++; 
        } 
        if(toposort()) puts("YES");
        else puts("NO");
    } 
    return 0;
}

备注:本文参考

百度:拓扑排序

【P4017 最大食物链计数】拓扑排序

题解 P4017 【最大食物链计数】

CHOJ 2101可达性统计

算法竞赛进阶指南

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值