输出dag的所有拓扑排序序列_L3图论第03课 拓扑排序

L3-图论-第03课 拓扑排序

有向无环图 (DAG) 和 拓扑排序

定义

如果一个有向图不存在环,也就是任意结点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图。英文名叫 Directed Acyclic Graph,缩写是 DAG。

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

性质

  • 能拓扑排序的图,一定是有向无环图;

如果有环,那么环上的任意两个节点在任意序列中都不满足条件了。

  • 有向无环图,一定能拓扑排序;

如何判定一个图是否是有向无环图呢?

检验它是否可以进行 拓扑排序 即可。

用途

拓扑排序有何作用?

拓扑排序主要用来解决有向图中的依赖解析(dependency resolution)问题。

游戏升级的先后顺序也是一个拓扑序.

e13234345ce58d98f394d69756ee8bb3.png

用计算机专业的几门课程的学习次序来描述拓扑关系 ,显然对于一门课来说,必须先学习它的先导课程才能更好地学习这门课程,比如学数据结构必须先学习C语言和离散数学,而且先导课程中不能有环,否则没有尽头了

de841a9c840ca3f1b60af56a23c6a4fc.png

拓扑排序的结果不唯一,比如“C语言”和“离散数学”就可以换下顺序,又或者把“计算机导论”向前放在任何一个位置都可以。总结一下就是,如果某一门课没有先导课程或是所有的先导课程都已经学习完毕,那么这门课就可以学习了。

算法描述

对于一个有向无环图

  1. 初始化一个 int[] inDegree 保存每一个结点的入度。
  2. 对于图中的每一个结点的子结点,将其子结点的入度加1。
  3. 选取入度为 0 的结点开始遍历,并将该节点加入输出。
  4. 对于遍历过的每个结点,更新其子结点的入度:将子结点的入度减1。
  5. 重复步骤3,直到遍历完所有的结点。
  6. 如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。

解释一下,假设A为一个入度为0的结点,就表示A结点没有前驱结点,可以直接做,把A完成后,对于A的所有后继结点来说,前驱结点就完成了一个,入度进行−1。

3c281beec73ab37a73fb94ec38a3e92b.png

时间复杂度

如果 DAG 网络有 n 个顶点,m 条边,在拓扑排序的过程中,搜索入度为零的顶点所需的时间是 O(n)。在正常情况下,每个顶点进一次队列,出一次队列,所需时间 O(n)。每个顶点入度减 1 的运算共执行了 m 次。所以总的时间复杂为 O(n+m)。

因为拓扑排序的结果不唯一,所以题目一般会要求按某种顺序输出,就需要使用优先级队列,这里采取了最小字典序输出。

模板

void tpsort() {
    queue  q;for (int i = 1; i <= n; i++) {if (!in[i]) {
            q.push(i);
        }
    }while (!q.empty()) {
        int u = q.front();
  q.pop();
        ans[++idx] = u;for (int i = hd[u]; i ; i = e[i].nxt) {
            int to = e[i].to;in[to]--;if (!in[to])
                q.push(to);
        }
    }
}

DFS 拓扑排序

#include
using namespace std;

const int maxn = 1000 + 10;
const int INF = 1e9 + 7;
int T, n, m, cases;

vector<int>Map[maxn];

// 标记数组c[i] = 0 表示还未访问过点
// c[i] = 1表示已经访问过点i,并且还递归访问过它的所有子孙?
// c[i] = -1表示正在访问中,尚未返回
int c[maxn];
int topo[maxn], t;

bool dfs(int u)//从u出发{
    c[u] = -1;//访问标志
    for(int i = 0; i     {
        int v = Map[u][i];

     //如果子孙比父亲先访问,说明存在有向环,失败退出
      if(c[v] 0)return false;
  else if(!c[v] && !dfs(v))
   return false;
   //如果子孙未被访问,访问子孙返回假,说明也是失败
    }

    c[u] = 1;
    topo[--t] = u;
 //在递归结束才加入topo排序中?
 // 这是由于在最深层次递归中,已经访问到了尽头,
 //此时才是拓扑排序中的最后一个元素
    return true;
}

bool tpsort(){
    t = n;
    memset(c, 0, sizeof(c));
    for(int u = 1; u <= n; u++)if(!c[u])
        if(!dfs(u))return false;
    return true;
}
int main(){
    while(cin >> n >> m)
    {
        if(!n && !m)break;
        int u, v;
        for(int i = 0;  i <= n; i++)Map[i].clear();
        for(int i = 0; i         {
            cin >> u >> v;
            Map[u].push_back(v);
        }
        if(tpsort())
        {
            cout<<"Great! There is not cycle."<<endl;
            for(int i = 0; i cout<" ";cout<<endl;
        }else cout<<"Network has a cycle!"<<endl;
    }return 0;
}

拓扑排序 + 字典序

U107394 拓扑排序模板

题目描述

有向无环图上有n个点,m条边。求这张图字典序最小的拓扑排序的结果。字典序最小指希望排好序的结果中,比较靠前的数字尽可能小。

输入格式

第一行是用空格隔开的两个整数n和m,表示n个点和m条边。

接下来是m行,每行用空格隔开的两个数u和v,表示有一条从u到v的边。

输出格式

输出一行,拓扑排序的结果,数字之间用空格隔开

输入输出样例
  • 输入 #1复制
5 3
1 2
2 4
4 3
  • 输出 #1复制

1 2 4 3 5

说明/提示
模板代码
#include 
#include 
#include 
#include 
using  namespace std;

const int N = 1e5 + 6;

int n, m, ecnt;

struct Edge
{
    int to, nxt;
} e[N];

int hd[N], in[N], ans[N], idx;

void add(int u, int v){
    e[++ecnt].to = v;
    e[ecnt].nxt = hd[u];
    hd[u] = ecnt;
}

void tpsort() {
    priority_queue<int, vector<int>, greater<int> > q;

 for (int i = 1; i <= n; i++) {
        if (!in[i]) {
            q.push(i);
        }
    }

    while (!q.empty()) {
        int u = q.top();
  q.pop();
        ans[++idx] = u;
        for (int i = hd[u]; i ; i = e[i].nxt) {
            int to = e[i].to;
   in[to]--;
            if (!in[to])
                q.push(to);
        }
    }
}

int main(){
 scanf("%d %d", &n, &m);
    for (int i = 0; i         int u, v;
        scanf("%d%d", &u, &v);
        add(u, v);
        in[v]++;
    }

 tpsort();

 for(int i = 1; i <= idx; i++)
  printf("%d ", ans[i]);

 return 0;
}

拓扑排序 + 动规

P4017 最大食物链计数

题目背景

你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧。

题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 11 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 80112002 的结果。

输入格式

第一行,两个正整数 n、m,表示生物种类 n 和吃与被吃的关系数 m。

接下来 m 行,每行两个正整数,表示被吃的生物 A 和吃 A 的生物 B。

输出格式

一行一个整数,为最大食物链数量模上 80112002 的结果。

输入输出样例
  • 输入 #1复制
5 7
1 2
1 3
2 3
3 5
2 5
4 5
3 4
  • 输出 #1复制

5

说明/提示

各测试点满足以下约定:

0bf6665b8d22a8959a7ba258ffc1d019.png
参考代码
#include
using namespace std;

const int MOD = 80112002;
const int N = 5002;
int a, b, n, m, ecnt;
int hd[N], in[N], out[N], f[N], ans;

struct Edge {
   int to, nxt;
} e[5000002];

void add(int u, int v){
    e[++ecnt].to = v;
    e[ecnt].nxt = hd[u];
    hd[u] = ecnt;
}

void tpsort(){
 queue<int> q;
 for(int i = 1; i <= n; i++) {
  if(in[i] == 0) {
   f[i] = 1;
   q.push(i);
  }
 }

 while(!q.empty()) {
  int a = q.front();
  q.pop();

  for(int i = hd[a]; i; i = e[i].nxt){
   int b = e[i].to;
   f[b] += f[a];
   f[b] %= MOD;
   in[b]--;
   if(in[b] == 0) {
    if(out[b] == 0){
     ans += f[b];
     ans %= MOD;
    }
    else q.push(b);
   }
  }
 }
}

int main(){
   int i;

   scanf("%d %d", &n, &m);

 for(i = 1; i <= m; i++) {
  scanf("%d %d", &a, &b);
  add(a, b);
  out[a]++;
  in[b]++;
 }


 tpsort();

 printf("%d\n", ans);
}

拓扑排序 + 数学

P7113 排水系统

题目描述

对于一个城市来说,排水系统是极其重要的一个部分。

有一天,小 C 拿到了某座城市排水系统的设计图。排水系统由 n 个排水结点(它们从 编号)和若干个单向排水管道构成。每一个排水结点有若干个管道用于汇集其他排水结点的污水(简称为该结点的汇集管道),也有若干个管道向其他的排水结点排出污水(简称为该结点的排出管道)。

排水系统的结点中有 m 个污水接收口,它们的编号分别为 ,污水只能从这些接收口流入排水系统,并且这些结点没有汇集管道。排水系统中还有若干个最终排水口,它们将污水运送到污水处理厂,没有排出管道的结点便可视为一个最终排水口。

现在各个污水接收口分别都接收了 1 吨污水,污水进入每个结点后,会均等地从当前结点的每一个排出管道流向其他排水结点,而最终排水口将把污水排出系统。

现在小 C 想知道,在该城市的排水系统中,每个最终排水口会排出多少污水。该城市的排水系统设计科学,管道不会形成回路,即不会发生污水形成环流的情况。

输入格式

第一个两个用单个空格分隔的整数 n, m。分别表示排水结点数与接收口数量。

接下来 n 行,第 i 行用于描述结点 i 的所有排出管道。其中每行第一个整数 表示其排出管道的数量,接下来 个用单个空格分隔的整数 依次表示管道的目标排水结点。

保证不会出现两条起始结点与目标结点均相同的管道。

输出格式

输出若干行,按照编号从小到大的顺序,给出每个最终排水口排出的污水体积。其中体积使用分数形式进行输出,即每行输出两个用单个空格分隔的整数 p,q,表示排出的污水体积为 。要求 p 与 q 互素,q = 1q=1 时也需要输出 qq。

输入输出样例
  • 输入 #1复制
5 1
3 2 3 5
2 4 5
2 5 4
0
0
  • 输出 #1复制
1 3
2 3

【样例 #1 解释】

1 号结点是接收口,4, 5 号结点没有排出管道,因此是最终排水口。

1 吨污水流入 1 号结点后,均等地流向 2,3,5 号结点,三个结点各流入 吨污水。2 号结点流入的 吨污水将均等地流向 4,5 号结点,两结点各流入 吨污水。3 号结点流入的 吨污水将均等地流向 4,5 号结点,两结点各流入 吨污水。最终,4 号结点排出 吨污水 5 号结点排出 \frac{1}{3} + \frac{1}{6} + \frac{1}{6} = \frac{2}{3} 吨污水。

【数据范围】

0f1201be64e760f01ec1bbf3879a5e16.png

对于全部的测试点,保证 ,,

数据保证,污水在从一个接收口流向一个最终排水口的过程中,不会经过超过 1010 个中间排水结点(即接收口和最终排水口不算在内)。

分析
  1. 深搜, 从每个入口点开始, 计算流过的每个结点所能分配到的水量, 最后在出口处累加.
  2. 拓扑排序, 从头开始, 按层次依次计算每个结点所能得到的水量, 直到最终出水口
参考代码
#include 
using namespace std;

#define ll __int128

const int N = 1e5 + 5, M = 5e5 + 5;

int n, m;

ll gcd(ll a, ll b){
 return b == 0 ? a : gcd(b, a % b);
}

ll lcm(ll a, ll b){
 return a / gcd(a, b) * b;
}

struct Node {
    ll p, q;
    Node(ll p1 = 0, ll q1 = 1) {
        p = p1;
  q = q1;
    }

    Node operator * (const Node &x) const {
        Node res;
        res.p = p * x.p;
  res.q = q * x.q;
        ll g = gcd(res.p, res.q);
        res.p /= g, res.q /= g;
        return res;
    }

    Node operator + (const Node &x) const {
        Node res;
        res.q = lcm(q, x.q);
        res.p += p * (res.q / q);
        res.p += x.p * (res.q / x.q);
        ll g = gcd(res.p, res.q);
        res.p /= g, res.q /= g;
        return res;
    }
} val[N];

struct Edge {
    int to, nxt;
} e[M];

int hd[N], ecnt, in[N], out[N], ans[N], idx;

void add(int from, int to) {
    e[++ecnt].to = to;
 e[ecnt].nxt = hd[from];
    hd[from] = ecnt;
}

void tpsort(){
 queue<int> q;
    for (int i = 1; i <= m; i++)
        if (in[i] == 0)
            q.push(i);

    while(!q.empty()) {
        int u = q.front();
        q.pop();

     Node uout = Node(1, out[u]);
     uout = uout * val[u];

     for (int i = hd[u]; i; i = e[i].nxt) {
            int to = e[i].to;
            val[to] = val[to] + uout;
            in[to]--;
            if (in[to] == 0)
                q.push(to);
        }
    }
}

void print(ll n) {
    if(n > 9)
  print(n / 10);
    putchar(n % 10 + '0');
}

void printval(Node &x){
 print(x.p);
 putchar(' ');
 print(x.q);
 putchar('\n');
}

int main(void) {
    scanf("%d %d", &n, &m);

 for (int i = 1; i <= n; i++) {
        if (i <= m)
   val[i].p = 1;

        scanf("%d", &out[i]);

  if (out[i] == 0)
   ans[++idx] = i;

        for (int j = 1; j <= out[i]; j++) {
            int to;
   scanf("%d", &to);
            add(i, to);
            in[to]++;
        }
    }

 tpsort();

    for (int i = 1; i<= idx; i++) {
     printval(val[ans[i]]);
 }

    return 0;
}

题单 拓扑排序 + 各种算法

  • U107394 拓扑排序模板
  • P1113 杂务
  • P1983 车站分级
  • P1347 排序
  • P1807 最长路
  • P4017 最大食物链计数
  • P7077 函数调用
  • P7113 排水系统

云帆编程老师联系方式:

云帆老师

微信:

d8912a6b418ac692299934e5a1ccbaec.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值