拓扑排序及其应用

一、问题概述

         题目源自leetcode207题,对事件发生的先后顺序进行规定,最终,判断是否能够合理安排时间,完成一系列事件而不产生冲突。

二、问题分析 

        该类问题的核心内容是其有顺序要求,即一件事情必须在另一件事情完成之后才能进行,因此,这种十分强调事件的先后顺序的问题使用拓扑排序即可解决。

        下面,给出标准的拓扑排序的解法,同时给出另一种基于DFS后向边判断的另一种算法。

三、问题求解

解法一:拓扑排序

        经典的拓扑排序问题,强调事件的先后顺序,某些元素一定要在前驱元素都访问完了之后才能开始访问。利用这种特性,我们建立一个队列去维护所有可以访问的节点,建立一个入度表判断该节点是否可以访问,同时,将题目中所给的vector二维数组说明的前驱后继关系,转化为邻接表ADJList,便于访问节点之间的关系。
        具体思路及算法流程如下:


        1.构造邻接表
        将题目中所给的矩阵转化为图的邻接表的形式,便于利用边之间的关系。


        2.构造入度表
        将所有节点的入度初始化为0,在步骤1构造邻接表的同时,令degree[j]++,即j节点的入度+1。(注意,题目中所给的关系顺序是反向的,要将其倒转)


        3.构造初始情景
        在步骤2完成之后,遍历入度表,将所有入度为0的节点压入队列,便于下一步操作的进行。


        4.出入表循环
        如果队列不空,弹出一个节点,将其压入结果队列中,同时使用count对弹出过的节点进行计数,便于返回,而后遍历该节点的邻接表,将所有邻接的节点的入度-1(因为该节点已经pop掉了),在每次-1的同时,判断该节点是否满足入队的要求,是则压入,否则不管。


        5.结果判断
        最后,判断count计数和节点个数的关系,如果拓扑排序存在有环图,那么某些节点必然是无法去掉度成为可入队的节点的,访问过的节点数自然少于节点的总数;相反,如果count和节点总数相同,即所有的节点都可以通过拓扑排序遍历。

        C++代码如下(要在leetcode解题环境中运行)


//使用拓扑排序的方法求解
class Solution{
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites){
        //构造邻接矩阵和入度表
        vector<vector<int>> adj;
        vector<int> degree;
        adj.resize(numCourses);
        degree.resize(numCourses);
        for(int i=0;i<numCourses;i++){
            degree[i] = 0;
        }
        for(int i=0;i<prerequisites.size();i++){
            int _i = prerequisites[i][1];
            int _j = prerequisites[i][0];
            degree[_j]++;
            adj[_i].push_back(_j);//初始化
        }
        //初始化 从入度为0的节点开始
        queue<int> q;
        int count = 0;//记录访问过的节点的个数
        for(int i=0;i<numCourses;i++){
            if(!degree[i]){
                q.push(i);//入队
            }
        }
        //直到队列为空为止
        while(!q.empty()){
            int u = q.front();
            q.pop();
            count++;
            for(int i=0;i<adj[u].size();i++){
                //减少节点的度数 如果为0则加入
               int v = adj[u][i];
               degree[v]--;
               //如果节点degree为0 则表示可以访问
               if(!degree[v]){
                   q.push(v);
               }
            }
        }
        return count == numCourses;
    }
};

        算法运行时间:

解法二:DFS后向边的判断 

        《算法导论》中对DFS生成的深度优先树定义了三种不同的边,树边、后向边、前向边、横向边,本题目中强调顺序不能反,即为判断一个图是否含有后向边的过程。《导论》中对DFS过程中节点的颜色定义了三种类型,一种是白色(White),即从未访问过,节点的初始颜色;一种是灰色(Gray),节点正在被访问中;另一种是黑色(Black),节点已经被访问完了。
        我们要的正常顺序就是在访问一个节点的时候,它的邻接节点没有正在被访问(即不是灰色),相反,如果为灰色,那么该边就是一条后向边。如果该节点完成了所有的访问工作,即所有的子节点都被访问完了,我们就将其着为黑色,表示该节点已经完成了任务,可以安息了。
        通过遍历所有节点,如果为白色则访问,为黑色,则跳过,在遍历过程中寻找有没有指向灰色节点的邻接边,此边即后向边,在每次查找过程中,都将节点push_back入stack中,栈前的元素即为该节点的满足拓扑关系的子元素。如果下次又存在比该节点更靠前的元素,则一定会push在该节点的back。这样,我们的结果stack就是满足拓扑关系逆序的了。
        最后,判断是否存在后向边,存在则返回空数组;不存在则将res结果栈逆转(reverse)后返回即可。

        C++代码如下:


class Solution {
private:
    vector<vector<int>> edges;//边集
    vector<int> visited;//访问标记
    bool valid = true;//用于保存结果 用于通信
    vector<int> res;
public:
    void DFS(int u){
        visited[u] = 1;//正在被访问
        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;//访问结束
        //从u向下的节点都访问完了 满足拓扑排序的顺序 如果还有u的上级节点 次序一定都是在u的后方(最后进行逆转就可以了)
        res.push_back(u);
    }
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites){
        edges.resize(numCourses);
        visited.resize(numCourses);
        for(const auto& info:prerequisites){
            edges[info[1]].push_back(info[0]);
        }
        for(int i=0;i<numCourses&&valid;++i){
            if(!visited[i]){
                DFS(i);
            }
        }
        if(!valid){
            return {};//返回空
        }
        reverse(res.begin(),res.end());
        return res;
    }
};

        算法运行时间:

         感谢大家指正!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值