图论——AOV网络及拓扑排序

引入

有向无环图(DAG)

如果一个有向图不存在环,也就是任意结点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图

AOV 网络

有向图中,用顶点表示活动,用有向边 < V i , V j > <V_i, V_j> <Vi,Vj>表示活动 i i i 是活动 j j j 的必须条件。这种有向图称为用顶点表示活动的网络(Active on vertices),简称AOV网络。

在AOV网络中,如果活动 V i V_i Vi必须在 V j V_j Vj之前进行,则存在有向边 < V i , V j > <V_i, V_j> <Vi,Vj>,并称 V i V_i Vi V j V_j Vj的直接前驱, V j V_j Vj V i V_i Vi的直接后继。这种前驱与后继的关系具有传递性 V i V_i Vi 对于 V j V_j Vj 的后继依然具有前导性)和反自反性(AOV网络中不能出现回路,即有向环)。因此,对于给定的AOV网络,必须先判断它是否存在有向环。

拓扑排序

检测有向环可以通过对AOV网络进行拓扑排序

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

在这里插入图片描述

而且还可以发现,如果两门课程之间没有直接或间接的先导关系,那么这两门课的学习先后是任意的(比如“C语言”和“离散数学”的学习顺序就是任意的),于是上述课程就可以排成一个水平展开的先后顺序,如下图

在这里插入图片描述

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

如果拓扑排序能够将AOV网络的所有顶点都排入一个拓扑有序的序列中,则说明该AOV网络中没有有向环。

算法描述

假设A是一个入度为0的结点,也就表示A结点没有前驱结点,可以“直接做”,把A完成后,对于A的所有后继结点来说,前驱结点就完成了一个,它们的入度数 − 1 -1 1 ,再选择一个入度为0的点重复。

对于一个有向无环图

(1)统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向所有的节点的入度 − 1 -1 1

(2)重复(1),直到所有的节点都被分离出来,拓扑排序结束。

(3)如果最后不存在入度为0的节点,那就说明有环,无解。

在这里插入图片描述

时间复杂度

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

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

vector<int>head[505], ans;
int n, m, in[505];//入度序列

void topologicalSorting() {
        cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int c1, c2;
        scanf("%d%d", &c1, &c2);
        head[c1].push_back(c2);
        in[c2]++;
    }
    priority_queue<int, vector<int>, greater<int>>q;
    for (int i = 1; i <= n; i++) {
        if (!in[i]) {
            q.push(i);
        }
    }
    while (!q.empty() && ans.size() < n) {
        int v = q.top(); q.pop();
        ans.push_back(v);
        for (int i = 0; i < head[v].size(); i++) {
            in[head[v][i]]--;
            if (!in[head[v][i]])
                q.push(head[v][i]);
        }
    }

    if (ans.size() == n) {
        //找到拓扑排序序列
    }
    else {
        //图中有环
    }
}    

练习

模板题

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1285

#include <iostream>
#include <algorithm>
#include <queue>
#include <stdio.h>
#include <vector>
using namespace std;

vector<int>head[505];
int in[505];

int main() {
    int n, m;
    while (cin >> n >> m) {
        priority_queue<int, vector<int>, greater<int>>q;
        vector<int>ans;
        for (int i = 0; i < m; i++) {
            int c1, c2;
            scanf("%d%d", &c1, &c2);
            head[c1].push_back(c2);
            in[c2]++;
        }
        for (int i = 1; i <= n; i++) {
            if (!in[i]) {
                q.push(i);
            }
        }

        while (!q.empty()) {
            int temp = q.top(); q.pop();
            ans.push_back(temp);
            for (int i = 0; i < head[temp].size(); i++) {
                in[head[temp][i]]--;
                if (!in[head[temp][i]])
                    q.push(head[temp][i]);
            }
        }
        if (ans.size() == n) {
            for (int i = 0; i < n; i++) {
                head[i + 1].clear();
                cout << ans[i];
                if (i != n - 1)cout << ' ';
            }
            cout << endl;
        }
        q.emplace();
        ans.clear();
        
    }
    
    return 0;
}

反向拓扑排序

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4857

题意: n n n个结点,给定 m m m个拓扑关系 ( a , b ) (a,b) (a,b)表示 a a a必须排在 b b b前面,在满足 m m m个拓扑关系关系的同时,使得小的结点尽可能的排在前面。
乍一看好像直接拓扑排序就行,但是看一个例子:
6 → 3 → 1 5 → 4 → 2 6 \rightarrow 3\rightarrow 1\\5 \rightarrow 4 \rightarrow 2 631542
直接拓扑排序的结果是: 5   4   2   6   3   1 5\ 4\ 2\ 6\ 3\ 1 5 4 2 6 3 1 ,是错误的,因为我们可以把1号安排到更前面的位置 即: 6   3   1   5   4   2 6\ 3\ 1\ 5\ 4\ 2 6 3 1 5 4 2(正确答案)。所以直接拓扑排序是不行的,为什么会出现这样的状况,对于多个拓扑关系,我们本来的策略是优先删除首结点较小的拓扑序列(比如5号结点比6号结点小,我们先删除了5号结点),但我们希望的是优先删除尾结点较小的拓扑序列(比如1号结点比2号结点小,应当先删除1号结点)。问题找到了,我们可以尝试一下逆向思维,即我们先考虑哪些点应该靠后释放,就是把原来的拓扑关系反过来

1 → 3 → 6 2 → 4 → 5 1 \rightarrow 3 \rightarrow 6\\2 \rightarrow 4 \rightarrow 5 136245
这样我们按照优先删除首结点较大的拓扑序列得到的结果是 2   4   5   1   3   6 2\ 4\ 5\ 1\ 3\ 6 2 4 5 1 3 6,好像还是不太对,把它逆序输出就对啦!

#include <iostream>
#include <algorithm>
#include <queue>
#include <stdio.h>
#include <vector>
using namespace std;

vector<int>head[30005];
int in[30005];

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        priority_queue<int>q;
        vector<int>ans;
        for (int i = 0; i < m; i++) {
            int c1, c2;
            scanf("%d%d", &c1, &c2);
            /*head[c1].push_back(c2);
            in[c2]++;*/
            head[c2].push_back(c1);
            in[c1]++;
        }
        for (int i = 1; i <= n; i++) {
            if (!in[i]) {
                q.push(i);
            }
        }

        while (!q.empty()) {
            int temp = q.top(); q.pop();
            ans.push_back(temp);
            for (int i = 0; i < head[temp].size(); i++) {
                in[head[temp][i]]--;
                if (!in[head[temp][i]])
                    q.push(head[temp][i]);
            }
        }

        if (ans.size() == n) {
            for (int i = n - 1; i >= 0 ; i--) {
                head[i + 1].clear();
                cout << ans[i];
                if (i != 0)cout << ' ';
            }
            cout << endl;
        }
        q.emplace();
        ans.clear();
    }
    return 0;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拓扑排序是一种常用的有向无环(DAG)的排序算法,其主要思想是将有向中的节点按照一定的顺序进行排序,使得所有的有向边从排在前面的节点指向排在后面的节点。在AOV中,节点代表活动,有向边代表活动之间的先后关系。 以下是一个使用C语言实现拓扑排序的示例代码: ```c #include <stdio.h> #define MAXVEX 100 //节点最大数目 int main() { int i, j, k, n, e; int flag[MAXVEX]; int in[MAXVEX]; int topo[MAXVEX]; int arc[MAXVEX][MAXVEX]; //邻接矩阵 //输入节点数和边数 printf("请输入节点数和边数:"); scanf("%d%d", &n, &e); //初始化邻接矩阵和入度数组 for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { arc[i][j] = 0; } in[i] = 0; flag[i] = 0; } //输入每条有向边的起始节点和结束节点 for (k = 0; k < e; k++) { printf("请输入第%d条有向边的起始节点和结束节点:", k + 1); scanf("%d%d", &i, &j); arc[i][j] = 1; in[j]++; } //拓扑排序 for (i = 0; i < n; i++) { if (in[i] == 0) { flag[i] = 1; topo[0] = i; break; } } for (i = 1; i < n; i++) { for (j = 0; j < n; j++) { if (flag[j] == 0 && in[j] == 0) { flag[j] = 1; topo[i] = j; for (k = 0; k < n; k++) { if (arc[j][k] == 1) { in[k]--; } } break; } } } //输出拓扑排序结果 printf("拓扑排序结果为:"); for (i = 0; i < n; i++) { printf("%d ", topo[i]); } printf("\n"); return 0; } ``` 在该代码中,使用邻接矩阵来表示有向,使用入度数组来表示每个节点的入度。在拓扑排序过程中,依次遍历每个节点,如果该节点的入度为0,则将其加入到拓扑序列中,并将其后继节点的入度减1。重复这个过程直到所有节点都被遍历过。最终得到的拓扑序列就是一种满足所有先后关系的顺序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值