拓扑排序——思想、例题及C++实现(更新中)

目录

一、何为拓扑排序

1.1  有向无环图 DAG

1.2  如何判断 DAG

1.3  拓扑排序 

二、拓扑排序的方法 

2.1  遍历法

2.2  删除入度为0结点法 

三、例题详解 

3.1  课程表 

3.2  课程表2


一、何为拓扑排序

        1.1  有向无环图(DAG)

         一张图若为有向无环图(DAG, Directed Acyclic Graph),则必须满足:

  • 该图为有向图,且每个顶点只出现一次。
  • 图中没有环的存在

        一张DAG图可以有很多种表示方式,如链表表示(用指针存储后继),数组表示(用数组表示后继) 等。故DAG题目多变,但解法与思想都是一样的。

        1.2  如何判断DAG

        对于图的遍历过程而言, 若在遍历A元素的所有后继(包括后继的后继...)的时候,再次遍历到了A,说明图中存在环,非DAG。

        对于删除入度为0的元素的过程而言,若某一时刻不存在入度为0的元素,但依然图中有元素存在,说明图中存在环,非DAG。(“入度”:指向该结点的边的条数)

        1.3  拓扑排序 

        拓扑排序就是遍历一张DAG图,得出遍历元素的先后顺序的过程。若把图中所有顶点视为“事件”,把有向边视为“先后顺序”,则可以将DAG图视为一个表示各事件发生先后顺序的“AOV 网” 。而对DAG进行拓扑排序,就是要找出一个合理的做事情的顺序。与拓扑排序有关的题,基本都是这个类型。

二、拓扑排序的方法 

         2.1  遍历冲突法

         遍历冲突法的基本思想是:若在遍历A元素的所有后继(包括后继的后继...)的时候,再次遍历到了A,说明图中存在环,非DAG。

        具体怎么实现呢?我们可以设置一个 visited 数组,用来表示每个结点的搜索状态,搜索状态有三种——0:未搜索、1:正在搜索其全部后继、2:搜索完成。采用深度优先的递归遍历的方式:

  • 将visited初始值全部设为0
  • 准备搜索A,检查visited[A]:
    • 若visited[A] == 1,说明A已处于搜索状态:A发起的搜索重新搜索到了A,图中存在环,非DAG图,结束拓扑排序。
    • 若visited[A] == 2,说明A及其全部后继已被搜索,不再做任何处理。
    • 若visited[A] == 0, 将visited[A]置为1,并递归搜索A的全部后继。
  • 若在递归搜索的某时刻,发现visited 为 1,说明出现了环,结束拓扑排序 
  • 若所有元素都成功完成搜索,说明这是一张DAG,拓扑排序的结果就是搜索元素的顺序

        例题: 见第三节——3.1 课程表

         2.2  删除入度为0结点法 

         删除入度为0结点法的思路是:每次遍历都删除入度为0的节点,若最后图中节点被全部删除,则说明此位DAG图;若最后图中依然有节点,那么这些节点的入度都不为0,则说明出现了无法打破的环,非DAG图。

        具体的实现方式是,维护三个数组:

  • vector<vector<int>> successors;             // 直接后继
  • vector<int> status;                                   // 搜索状态
  • vector<int> degree;                                 // 入度

        每一次都迭代,都选取入度为0,且搜索状态为未搜索的节点遍历:

  • 将此节点标记为已搜索
  • 将此节点所有直接后继的入度 -1
  • 重新开始迭代

        * 之所以要重新开始迭代,是因为此次搜索可能改变了之前节点的入度,迭代需要从0重新开始

        迭代完成后,检查degree数组,若依然存在不为0的值,说明非DAG图

        例题:见第三节——3.2 课程表2

三、例题详解 

        3.1  课程表 

题目:

        你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

        在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

        例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
        请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

        (题目来源:LeetCode https://leetcode.cn/problems/course-schedule/


思路

        遍历冲突法。

        采用DFS进行遍历,用一个栈来存储所有已经搜索完成的节点。

        对于一个节点 u,当它的所有直接后继都已经搜索完成(入栈),那么 u 便搜索完成。

        假设我们当前搜索到了节点 u,如果它的所有相邻节点都已经搜索完成,那么这些节点都已经在栈中了,此时我们把 u 入栈。此时,我们从栈顶向下看,由于 u 处于栈顶的位置,u 的所有直接后继都在 u 的下方,且每一次更新栈 都满足这个规律。说明栈就是拓扑排序的结果。


算法:

        对于图中的任意一个节点,它在搜索的过程中有三种状态,即:

  • 「未搜索」:我们还没有搜索到这个节点;
  • 「搜索中」:我们搜索过这个节点,但其后继还没有搜索完成(没有入栈),该元素也没有入栈
  • 「已完成」:该元素的所有后继都已搜索完成(入栈),该元素也已入栈。

        在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。

        我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:

  • 如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;
  • 如果 v 为「搜索中」,那么我们就找到了图中的一个环,结束拓扑排序;
  • 如果 v 为「已完成」,直接返回。

当 u 的所有相邻节点都为「已完成」时,我们将 uu 放入栈中,并将其标记为「已完成」。

在整个深度优先搜索的过程结束后,如果我们没有找到图中的环,那么栈中存储这所有的 nn 个节点,从栈顶到栈底的顺序即为一种拓扑排序。


作者:LeetCode-Solution
链接:力扣
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

#include <iostream>
#include <vector>
using namespace std;

vector<vector<int>> edges;  // edges[i]存储元素i的直接后继
vector<int> visited;        // 0未搜索,1搜索中,2搜索完成
bool valid = true;          // DAG判断标志

void dfs(int u) {
    // 搜索开始,将visited置1
    visited[u] = 1;
    // 递归搜索u的所有后继
    for (int v: edges[u]) {
        if (visited[v] == 0) {
            dfs(v);
            if (!valid)
                return;
        }
        // 出现了环
        else if (visited[v] == 1) {
            valid = false;
            return;
        }
    }
    visited[u] = 2;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    edges.resize(numCourses);
    visited.resize(numCourses, 0);
    // 将 1->2, 1->3 整合为 edges[1] = {2,3}
    for (const auto& info: prerequisites)
        edges[info[1]].push_back(info[0]);
    // dfs
    for (int i = 0; i < numCourses && valid; ++i)
        if (!visited[i])
            dfs(i);

    return valid;
}
int main() {
    int numCourses = 10;
    vector<vector<int>> prerequisites = {{5,8},{8,5},{1,9},{4,5},{0,2},{7,8},{4,9}};
    bool flag = canFinish(numCourses, prerequisites);
    if (flag)
        cout << "Topologic sort success, there are no rings." ;
    else    
        cout << "Topologic sort fail, there are rings." ;

    return 0;
}

        3.2  课程表2

 题目:       

        现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

        例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

        (题目来源:https://leetcode.cn/problems/course-schedule-ii/


思路:

        删除入度为0节点法。每一次迭代都“删除”一个入度为0的点,看图中最后是否有余留。


算法:

        维护successor、status、degree三个数组。每一次迭代都只搜索入度为0且未搜索过的节点,搜索工作如下:

  • 将此节点加入res
  • 将此节点标记为已搜索
  • 将此节点的所有后继的入度 -1
  • 将指针置为 -1(此次跌打结束会有i++,i将变为0),重新开始迭代

        在迭代完成后,需要判断是否为DAG图,只需要计较res.size()和numCourses即可。

        这种方法的效率不如遍历法

// 必须每次都选取入度为0的节点搜索
// 不再使用status = 1来判断,而是用入度来判断
class Solution {
private:
    vector<vector<int>> successors;     // 直接后继
    vector<int> status;                 // 搜索状态
    vector<int> degree;                 // 入度
    vector<int> res;
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        // successors
        successors.resize(numCourses);
        for (auto& prerequisite : prerequisites)
            successors[prerequisite[1]].emplace_back(prerequisite[0]);
        // status
        status.resize(numCourses, 0);
        // degree
        degree.resize(numCourses, 0);
        for (auto& prerequisite : prerequisites)
            degree[prerequisite[0]]++;
        // 搜索入度为0且未搜索过的点
        for (int i = 0; i < numCourses; i++) {
            if (degree[i] == 0 && status[i] == 0) {
                res.emplace_back(i);                        // 加入res
                status[i] = 2;                              // 标记已搜索
                for (auto& successor : successors[i])       // 其后继的入度-1
                    degree[successor]--;
                i = -1;                                     // 重新开始搜索
            }
        }
        // 先判断是否所有点都在res中,再返回
        if (res.size() == numCourses)
            return res;
        else
            return {};
        
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaoqizhong7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值