相关推荐recommended
算法沉淀——BFS 解决拓扑排序(leetcode真题剖析)
作者:mmseoamin日期:2024-02-28

算法沉淀——BFS 解决拓扑排序(leetcode真题剖析),在这里插入图片描述,第1张

算法沉淀——BFS 解决拓扑排序

  • 01.课程表
  • 02.课程表 II
  • 03.火星词典

    Breadth-First Search (BFS) 在拓扑排序中的应用主要是用来解决有向无环图(DAG)的拓扑排序问题。拓扑排序是对有向图中所有节点的一种线性排序,使得对于每一条有向边 (u, v),节点 u 在排序中都出现在节点 v 的前面。如果图中存在环路,则无法进行拓扑排序。

    BFS 解决拓扑排序的步骤如下:

    1. 统计每个节点的入度(in-degree),即指向该节点的边的数量。
    2. 将所有入度为 0 的节点加入队列。
    3. 对于每个入度为 0 的节点,依次出队,更新其相邻节点的入度,将入度变为 0 的节点加入队列。
    4. 重复步骤 3 直到队列为空。

    如果最终遍历过的节点数等于图中的节点数,说明图是有向无环图,可以得到一个拓扑排序。

    01.课程表

    题目链接:https://leetcode.cn/problems/course-schedule/

    你这个学期必须选修 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>& prerequisites) {
                unordered_map> edges;
                vector in(numCourses,0);
                for(vector& e:prerequisites){
                    int a=e[0],b=e[1];
                    edges[b].push_back(a);
                    in[a]++;
                }
                queue q;
                for(int i=0;i
                    int t=q.front();
                    q.pop();
                    for(int e:edges[t]){
                        in[e]--;
                        if(in[e]==0) q.push(e);
                    }
                }
                for(int i:in) if(i) return false;
                return true;
            }
        };
        
        1. 使用一个哈希表 edges 存储有向图的边,其中 edges[b] 表示节点 b 指向的所有节点。
        2. 使用数组 in 记录每个节点的入度。初始化时,将所有节点的入度设为 0。
        3. 遍历先修课程的关系,构建有向图并更新入度数组。
        4. 将入度为 0 的节点加入队列 q。
        5. 使用 BFS 进行拓扑排序,从入度为 0 的节点开始,依次出队,并将其邻接节点的入度减 1。如果邻接节点的入度减为 0,将其加入队列。
        6. 如果最终所有节点的入度都为 0,说明图中不存在环,可以完成所有课程,返回 true;否则,返回 false。

        02.课程表 II

        题目链接:https://leetcode.cn/problems/course-schedule-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]
          

          思路

          总体思路和上面一致,我们只需要在每次将入度为0的点顺序保存即为拓扑排序的顺序。

          代码

          class Solution {
          public:
              vector findOrder(int numCourses, vector>& prerequisites) {
                  unordered_map> edges;
                  vector in(numCourses,0);
                  for(vector& e:prerequisites){
                      int a=e[0],b=e[1];
                      edges[b].push_back(a);
                      in[a]++;
                  }
                  queue q;
                  vector ret;
                  for(int i=0;i
                          q.push(i);
                          ret.push_back(i);
                      } 
                  while(!q.empty()){
                      int t=q.front();
                      q.pop();
                      for(int e:edges[t]){
                          in[e]--;
                          if(in[e]==0){ 
                              q.push(e);
                              ret.push_back(e);    
                          }
                      }
                  }
                  for(int i:in) if(i) return {};
                  return ret;
              }
          };
          
          1. 使用一个哈希表 edges 存储有向图的边,其中 edges[b] 表示节点 b 指向的所有节点。
          2. 使用数组 in 记录每个节点的入度。初始化时,将所有节点的入度设为 0。
          3. 遍历先修课程的关系,构建有向图并更新入度数组。
          4. 将入度为 0 的节点加入队列 q,同时将这些节点加入结果数组 ret 中。
          5. 使用 BFS 进行拓扑排序,从入度为 0 的节点开始,依次出队,并将其邻接节点的入度减 1。如果邻接节点的入度减为 0,将其加入队列和结果数组。
          6. 如果最终所有节点的入度都为 0,说明图中不存在环,返回拓扑排序结果;否则,返回空数组表示无法完成拓扑排序

          03.火星词典

          题目链接:https://leetcode.cn/problems/Jf1JuT

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

          给定一个字符串列表 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] 仅由小写英文字母组成

              思路

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

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

              a. 两层for循环枚举出所有的两个字符串的组合;

              b. 然后利用指针,根据字典序规则找出信息。

              1. 使用哈希表 edges 存储字母之间的顺序关系,其中 edges[a] 表示字母 a 后面可以跟随的字母集合。
              2. 使用哈希表 in 记录每个字母的入度,即有多少字母在它之前。
              3. 使用布尔变量 cheak 标记是否出现了无效的字母顺序。
              4. 定义 add 函数,该函数比较两个单词 s1 和 s2,找到它们第一个不相同的字母,然后将这个字母的顺序关系添加到 edges 中。如果 s2 是 s1 的前缀,则将 cheak 设置为 true。
              5. 在构建字母之间的顺序关系时,遍历相邻的两个单词,调用 add 函数,如果 cheak 为 true,则直接返回空字符串。
              6. 使用队列 q 存储入度为 0 的字母,初始化队列时将所有入度为 0 的字母加入。
              7. 使用 BFS 进行拓扑排序,不断将入度为 0 的字母出队,并将其后面可以跟随的字母的入度减 1。将入度为 0 的字母加入结果字符串 ret 中。
              8. 最后检查所有字母的入度是否都为 0,如果不为 0,则说明有环,返回空字符串;否则,返回结果字符串 ret。

              代码

              class Solution {
                  unordered_map> edges;
                  unordered_map in;
                  bool cheak=false;
                  void add(string& s1,string& s2){
                      int n=min(s1.size(),s2.size());
                      int i=0;
                      while(i
                          if(s1[i]!=s2[i]){
                              char a=s1[i],b=s2[i];
                              if(!edges.count(a)||!edges[a].count(b)){
                                  edges[a].insert(b);
                                  in[b]++;
                              }
                              break;
                          }
                          i++;
                      }
                      if(i==s2.size()&&i& words) {
                      for(auto& s:words)
                          for(auto& ch:s)
                              in[ch]=0;
                      int n=words.size();
                      for(int i=0;i
                              add(words[i],words[j]);
                              if(cheak) return "";
                          }
                      
                      queue q;
                      for(auto& [a,b]:in)
                          if(b==0) q.push(a);
                      
                      string ret;
                      while(!q.empty()){
                          char t=q.front();
                          q.pop();
                          ret+=t;
                          for(char ch:edges[t])
                              if(--in[ch]==0) q.push(ch);
                      }
                      for(auto& [a,b]:in) if(b) return "";
                      return ret;
                  }
              };