Offer必备算法30_BFS解决拓扑排序(三道力扣题详解)

目录

拓扑排序和图的介绍

①力扣207. 课程表

解析代码

②力扣210. 课程表 II

解析代码

③力扣LCR 114. 火星词典

解析代码

本篇完。


拓扑排序和图的介绍

        拓扑排序简单来说就是找到做事情的先后顺序(拓扑排序的结果可能不是唯一的)。

学习拓扑排序前先简单学习图的基本概念:

 图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

  • 顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;
  • E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。
  • (x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。
  • 顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
  • 有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。

入度和出度

图中的度:所谓顶点的度(degree),就是指和该顶点相关联的边数。在有向图中,度又分为入度和出度。

  • 入度 (in-degree) :以某顶点为弧头,终止于该顶点的边的数目称为该顶点的入度。
  • 出度 (out-degree) :以某顶点为弧尾,起始于该顶点的弧的数目称为该顶点的出度。

邻接表

        邻接表存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。

  • 在有向图中,描述每个点向别的节点连的边(点a->点b这种情况)。
  • 在无向图中,描述每个点所有的边(点a-点b这种情况)

有向图邻接表存储


下面是拓扑排序的概念:

        一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。

        在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。

对于有向图的拓扑排序,我们可以使用如下思路输出拓扑序(BFS 方式):

  1. 起始时,将所有入度为 0 的节点进行入队(入度为 0,说明没有边指向这些节点,将它们放到拓扑排序的首部,不会违反拓扑序定义)。
  2. 从队列中进行节点出队操作,出队序列就是对应我们输出的拓扑序。对于当前弹出的节点 x,遍历x 的所有出度y,即遍历所有由 x直接指向的节点y,对y做入度减一操作(因为x节点已经从队列中弹出,被添加到拓扑序中,等价于x节点从有向图中被移除,相应的由x发出的边也应当被删除,带来的影响是与 x相连的节点y的入度减一)。
  3. 对y进行入度减一之后,检查 y的入度是否为0,如果为0则将y入队(当y的入度为0,说明有向图中在y前面的所有的节点均被添加到拓扑序中,此时 可以作为拓扑序的某个片段的首部被添加,而不是违反拓扑序的定义)。
  4. 循环流程 2、3 直到队列为空。

①力扣207. 课程表

207. 课程表

难度 中等

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

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

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {

    }
};

解析代码

原问题可以转换成一个拓扑排序问题。用BFS 解决拓扑排序即可。

  • 将所有入度为 0 的点加入到队列中。
  • 当队列不空的时候,一直循环以下三步:
  1. 取出队头元素。
  2. 将于队头元素相连的顶点的入度 - 1。
  3. 然后判断是否减成 0。如果减成 0,就加入到队列中。
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> edges; // 领接表
        vector<int> in(numCourses, 0); // 存储每一个结点的入度
        for(auto& e : prerequisites) // 1. 建图
        {
            int a = e[0], b = e[1];; // b指向a
            edges[b].push_back(a);
            in[a]++;
        }

        queue<int> q; // 2. BFS解决拓扑排序
        for(int i = 0; i < numCourses; ++i)
        {
            if(in[i] == 0)
                q.push(i);
        }
        while(!q.empty()) // 层序遍历
        {
            int tmp = q.front();
            q.pop();
            for(auto& e : edges[tmp]) // 得到tmp指向的顶点, 删掉一个入度
            {
                in[e]--;
                if(in[e] == 0)
                    q.push(e);
            }
        }
        for(auto& e : in) // 3. 判断是否有环
        {
            if(e != 0)
                return false;
        }
        return true;
    }
};


②力扣210. 课程表 II

210. 课程表 II

难度 中等

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

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • 所有[ai, bi] 互不相同
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {

    }
};

解析代码

原问题可以转换成一个拓扑排序问题。用BFS 解决拓扑排序即可。

  • 将所有入度为 0 的点加入到队列中。
  • 当队列不空的时候,一直循环以下三步:
  1. 取出队头元素,并加入ret数组。
  2. 将于队头元素相连的顶点的入度 - 1。
  3. 然后判断是否减成 0。如果减成 0,就加入到队列中。
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> edges;
        vector<int> in(numCourses, 0), ret;
        for(auto& e : prerequisites) // 建表
        {
            int a = e[0], b = e[1]; // b指向a
            edges[b].push_back(a);
            in[a]++;
        }

        queue<int> q;
        for(int i = 0; i < numCourses; ++i) // 处理入度为0的点
        {
            if(in[i] == 0)
                q.push(i);
        }
        while(!q.empty()) // BFS
        {
            int tmp = q.front(); // 度为0的点指向的点的度减一
            q.pop();
            ret.push_back(tmp);
            for(auto& e : edges[tmp])
            {
                in[e]--;
                if(in[e] == 0)
                    q.push(e);
            }
        }
        if(ret.size() == numCourses) // 无环大小就相等
            return ret;
        else
            return {};
    }
};


③力扣LCR 114. 火星词典

LCR 114. 火星词典

难度 困难

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

  • 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
  • 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

示例 1:

输入:words = ["wrt","wrf","er","ett","rftt"]
输出:"wertf"

示例 2:

输入:words = ["z","x"]
输出:"zx"

示例 3:

输入:words = ["z","x","z"]
输出:""
解释:不存在合法字母顺序,因此返回 "" 。

提示:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 100
  • words[i] 仅由小写英文字母组成
class Solution {
public:
    string alienOrder(vector<string>& words) {

    }
};

解析代码

将题意理解清楚之后,这道题就变成了判断有向图是否有环,可以用拓扑排序解决。

如何如搜集信息(如果建图):

  1. 两层 for 循环枚举出所有的两个字符串的组合。
  2. 然后利用指针,根据字典序规则找出信息。
class Solution {
    unordered_map<char, unordered_set<char>> edges; // 邻接表
    unordered_map<char, int> in; // 统计入度信息
    bool flag; // 处理边界情况, 类似str1 = a, b, c && str2 = a, b
    
public:
    string alienOrder(vector<string>& words) {
        for(auto& str : words) // 初始化入度哈希表
        {
            for(auto& ch : str)
            {
                in[ch] = 0;
            }
        }
        int sz = words.size();
        for(int i = 0; i < sz; ++i) // 建图
        {
            for(int j = i + 1; j < sz; ++j)
            {
                Add(words[i], words[j]);
                if(flag) // 不合法
                    return "";
            }
        }

        queue<char> q; // 拓扑排序
        for(auto& [a, b] : in) // 入度为0的点入队列
        {
            if(b == 0)
                q.push(a);
        }
        string ret;
        while(!q.empty())
        {
            char tmp = q.front();
            q.pop();
            ret += tmp;
            for(auto& ch : edges[tmp]) // 度为0的点指向的点的度减1
            {
                if(--in[ch] == 0)
                    q.push(ch);
            }
        }

        for(auto& [a, b] : in) // 返回
        {
            if(b != 0)
                return "";
        }
        return ret;
    }

    void Add(string& str1, string& str2) // 收集信息
    {
        int sz = min(str1.size(), str2.size()), i = 0;
        for(; i < sz; ++i)
        {
            if(str1[i] != str2[i])
            {
                char a = str1[i], b = str2[i]; // 前大后小
                if(!edges.count(a) || !edges[a].count(b)) // 防止存入重复信息   
                {
                    edges[a].insert(b);  // a -> b
                    in[b]++;
                }
                break;
            }
        }
        if(i == str2.size() && i < str1.size())
            flag = true; // 类似str1 = a, b, c && str2 = a, b
    }
};

本篇完。

下一篇:递归中偏难的递归搜索与回溯类型的OJ。

下下篇是就开始就更新贪心算法的内容了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GR鲸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值