《剑指 Offer》专项突破版 - 面试题 113、114 和 115 : 详解拓扑排序(C++ 实现)

目录

前言

面试题 113 : 课程顺序

面试题 114 : 外星文字典

面试题 115 : 重建序列


 


前言

拓扑排序是指对一个有向无环图的节点进行排序之后得到的序列。如果存在一条从节点 A 指向节点 B 的边,那么在拓扑排序的序列中节点 A 出现在节点 B 的前面。一个有向无环图可以有一个或多个拓扑排序序列,但无向图或有环的有向图不存在拓扑排序

在讨论有向无环图拓扑排序算法之前先介绍两个概念:入度出度节点 v 的入度指的是以节点 v 为终点的边的数目,而节点 v 的出度是指以节点 v 为起点的边的数目。例如,在下图 (a) 的有向图中,节点 2 的入度是 1,出度是 2。

一种常用的拓扑排序算法是每次从有向无环图中取出一个入度为 0 的节点添加到拓扑排序序列之中,然后删除该节点及所有以它为起点的边。重复这个步骤,直到图为空或图中不存在入度为 0 的节点。如果最终图为空,那么图是有向无环图,此时就找到了该图的一个拓扑排序序列;如果最终图不为空并且已经不存在入度为 0 的节点,那么图中一定有环

下面对下图 (a) 中的图进行拓扑排序,该图中节点 1 的入度为 0,将该节点添加到拓扑排序序列中,并删除该节点及所有以该节点为起点的边,如下图 (b) 所示。接下来重复这个步骤,依次找到入度为 0 的节点 2、节点 3、节点 4、节点 5,如下图 (c)、(d)、(e) 所示,在先后删除这些节点之后图为空。因此,下图 (a) 中的图是有向无环图,它的拓扑排序序列为 [1, 2, 3, 4, 5]。

上述算法也可以用来判断一个有向图是否有环。如果执行上述步骤最终得到一个非空的图,并且图中所有节点的入度都大于 0,那么该图一定包含环。例如,下图中的有向图中的 3 个节点的入度都为 1,它们形成一个环。


面试题 113 : 课程顺序

题目

n 门课程的编号为 0 ~ n - 1。输入一个数组 prerequisites,它的每个元素 prerequisites[i] 表示两门课程的先修顺序。如果 prerequisites[i] = [a_i, b_i],那么必须先修完 b_i 才能修 a_i。请根据总课程数 n 和表示先修顺序的 prerequisites 得出一个可行的修课序列。如果有多个可行的修课序列,则输出任意一个可行的序列;如果没有可行的修课序列,则输出空序列。

例如,总共有 4 门课程,先修顺序 prerequisites 为 [[1, 0], [2, 0], [3, 1], [3, 2]],一个可行的修课序列是 0->2->1->3。

分析

将课程看成图中的节点,如果两门课程存在先修顺序那么它们在图中对应的节点之间存在一条从先修课程到后修课程的边,因此这是一个有向图。例如,可以根据先修顺序 prerequisites 为 [[1, 0], [2, 0], [3, 1], [3, 2]] 构建出如下图所示的有向图。例如,课程先修顺序 [1, 0] 对应在图中就有一条从节点 0 到节点 1 的边。

可行的修课序列实际上是图的拓扑排序序列。图中的每条边都是从先修课程指向后修课程,而拓扑排序能够保证任意一条边的起始节点一定排在终止节点的前面,因此拓扑排序得到的序列与先修顺序一定不会存在冲突,于是这个问题转变成如何求有向图的拓扑排序序列

对有向图进行拓扑排序的算法是每次找出一个入度为 0 的节点添加到序列中,然后删除该节点及所有以该节点为起点的边。重复这个过程,直到图为空或图中不存在入度为 0 的节点

代码实现

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> graph;
        for (int i = 0; i < numCourses; ++i)
        {
            graph.insert({ i, vector<int>() });
        }
​
        vector<int> inDegrees(numCourses, 0);
        for (vector<int>& prerequisite : prerequisites)
        {
            graph[prerequisite[1]].push_back(prerequisite[0]);
            ++inDegrees[prerequisite[0]];
        }
​
        queue<int> q;
        for (int i = 0; i < numCourses; ++i)
        {
            if (inDegrees[i] == 0)
                q.push(i);
        }
​
        vector<int> order;
        while (!q.empty())
        {
            int course = q.front();
            q.pop();
            order.push_back(course);
            for (int next : graph[course])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
        return order.size() == numCourses ? order : vector<int>();
    }
};

上述代码先根据先修顺序构建出有向图 graph,graph 用一个 unordered_map 表示邻接表,它的键是先修课程,它的值是必须在键对应的课程之后学习的所有课程。同时,将每个节点的入度保存到数组 inDegrees 中,inDegrees[i] 表示节点 i 的入度

接下来用广度优先搜索算法实现拓扑排序。队列中保存的是入度为 0 的节点。每次从队列中取出一个节点,将该节点添加到拓扑排序序列中,然后找到该课程的后修课程并将它们的节点的入度减 1,这相当于删除从先修课程到后修课程的边。如果发现新的入度为 0 的节点,则将其添加到队列中。重复这个过程直到队列为空,此时要么图中所有节点都已经访问完毕,已经得到了完整的拓扑排序序列;要么剩下的还没有搜索到的节点形成一个环,已经不存在入度为 0 的节点


面试题 114 : 外星文字典

题目

一种外星语言的字母都是英文字母,但字母的顺序未知。给定该语言排序的单词列表,请推测可能的字母顺序。如果有多个可能的顺序,则返回任意一个。如果没有满足条件的字母顺序,则返回空字符串。例如,如果输入排序的单词列表为 ["ac", "ab", "bc", "zc", "zb"],那么一个可能的字母顺序是 "acbz"。

分析

这个题目比较难。如果在面试中遇到比较难的问题,比较有效地分析、解决问题的思路是从具体的例子中总结出解题规律。

在排序的单词列表 ["ac", "ab", "bc", "zc", "zb"] 中,一共出现了 4 个字母,即 'a'、'b'、'c' 和 'z'。需要根据单词的顺序确定这 4 个字母的顺序。由于 "ac" 排在 "ab" 的前面,因此字母 'c' 应该排在字母 'b' 的前面(即 'c' < 'b'),这是因为这两个单词的第 1 个字母相同,第 2 个字母不同,那么它们的第 2 个字母的顺序确定了两个单词的顺序。接下来两个相邻的单词是 "ab" 和 "bc",它们的第 1 个字母就不同,那么它们的顺序由第 1 个字母确定,所以 'a' < 'b'。类似地,可以根据 "bc" 排在 "zc" 的前面得知 'b' < 'z',根据 "zc" 排在 "zb" 的前面得知 'c' < 'b'。

由比较排序的单词列表中两两相邻的单词可知 'c' < 'b'、'a' < 'b' 和 'b' < 'z',现在需要找出一个包含 4 个字母的字母序列满足已知的 3 个字母的大小顺序。这看起来就是一个关于拓扑排序的问题,可以将每个字母看成图中的一个节点,如果已知两个字母的大小关系,那么图中就有一条从较小的字母指向较大的字母的边。根据字母的大小关系 'c' < 'b'、'a' < 'b' 和 'b' < 'z' 构建出的有向图如下图所示,该有向图有两个拓扑排序序列,"acbz" 和 "cabz",相应地输入的单词列表就有两个可能的字母顺序。

如果能够得出该有向图的拓扑排序序列,那么任意一条边的起始节点(较小的字母)在拓扑排序序列中一定出现在终止节点(较大的字母)的前面。因此,这个问题实质上是一个关于拓扑排序的问题。

代码实现

class Solution {
public:
    string alienOrder(vector<string>& words) {
        unordered_map<char, unordered_set<char>> graph;
        unordered_map<char, int> inDegrees;
        for (string& word : words)
        {
            for (char ch : word)
            {
                if (!graph.count(ch))
                {
                    graph.insert({ ch, unordered_set<char>() });
                    inDegrees.insert({ ch, 0 });
                }
            }
        }
​
        for (int i = 0; i < words.size() - 1; ++i)
        {
            string w1 = words[i], w2 = words[i + 1];
            int j = 0;
            while (j < w1.size() && j < w2.size())
            {
                char ch1 = w1[j], ch2 = w2[j];
                if (ch1 != ch2)
                {
                    if (!graph[ch1].count(ch2))
                    {
                        graph[ch1].insert(ch2);
                        ++inDegrees[ch2];
                    }
                    break;
                }
                ++j;
            }
​
            if (j < w1.size() && j == w2.size())
                return "";
        }
​
        queue<char> q;
        for (auto& kv : inDegrees)
        {
            if (kv.second == 0)
                q.push(kv.first);
        }
​
        string order;
        while (!q.empty())
        {
            char ch = q.front();
            q.pop();
            order.push_back(ch);
            for (char next : graph[ch])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
        return order.size() == graph.size() ? order : "";
    }
};

在上述代码中,图用 unordered_map 类型的变量 graph 以邻接表的形式表示。与某节点相邻的节点(即比某字母大的节点)则用一个 unordered_set 保存(在比较排序的单词列表中两两相邻的单词时可能得到相同的两个字母的大小关系,需要快速判断图中是否存在这样的一条边,所以不使用 vector,而是 unordered_set)。unordered_map 类型的变量 inDegrees 保存每个节点的入度(这里也不适合使用 vector)。代码一开始找出单词列表 words 中出现的所有字母并做相应的初始化

接下来比较单词列表 words 中两两相邻的单词,从头找出第 1 组不同的两个字母,在图中添加一条从较小的字母(ch1)指向较大的字母(ch2)的边

这里有一类特殊的输入需要特别注意。如果排在后面的单词是排在前面的单词的前缀,那么无论什么样的字母顺序都是不可能的。例如,如果排序的单词列表是 ["abc", "ab"],不管是什么样的字母顺序,"abc" 都不可能排在 "ab" 的前面,因此这是一个无效的输入,此时可以直接返回空字符串表示无效的字母顺序


面试题 115 : 重建序列

题目

长度为 n 的数组 org 是数字 1 ~ n 的一个排列,seqs 是若干序列,请判断数组 org 是否为可以由 seqs 重建的唯一序列。重建的序列是指 seqs 所有序列的最短公共超序列,即 seqs 中的任意序列都是该序列的子序列

例如,如果数组 org 为 [4, 1, 5, 2, 6, 3],而 seqs 为 [[5, 2, 6, 3], [4, 1, 5, 2]],因为用 [[5, 2, 6, 3], [4, 1, 5, 2]] 可以重建出唯一的序列 [4, 1, 5, 2, 6, 3],所以返回 true。如果数组 org 为 [1, 2, 3],而 seqs 为 [[1, 2], [1, 3]],因为用 [[1, 2], [1, 3]] 可以重建出两个序列,[1, 2, 3] 或 [1, 3, 2],所以返回 false。

分析

超序列和子序列是两个相对的概念。如果序列 A 中的所有元素按照先后的顺序都在序列 B 中出现,那么序列 A 是序列 B 的子序列,序列 B 是序列 A 的超序列

按照题目的要求,如果在 seqs 的某个序列中数字 i 出现在数字 j 的前面,那么由 seqs 重建的序列中数字 i 一定也要出现在数字 j 的前面。也就是说,重建序列的数字顺序由 seqs 的所有序列定义

可以将 seqs 中每个序列的每个数字看成图中的一个节点,两个相邻的数字之间有一条从前面数字指向后面数字的边。例如,由 [[5, 2, 6, 3], [4, 1, 5, 2]] 构建的有向图如下图 (a) 所示,由 [[1, 2], [1, 3]] 构建的有向图如下图 (b) 所示。

如果得到的是有向图的拓扑排序序列,那么任意一条边的起始节点在拓扑排序序列中一定位于终止节点的前面。因此,由 seqs 重建的序列就是由 seqs 构建的有向图的拓扑排序的序列。这个问题就转变成判断一个有向图的拓扑排序序列是否唯一

上图 (a) 中的有向图的拓扑排序序列是唯一的,其中,节点 4 是图中唯一一个入度为 0 的节点,删除该节点和以该节点为起始节点的边之后节点 1 是下一个唯一的入度为 0 的节点,重复这个过程直到图为空,就可以得到唯一的拓扑排序序列 [4, 1, 5, 2, 6, 3]。

而上图 (b) 则不然,其中,节点 1 是图中唯一一个入度为 0 的节点,删除该节点和以它为起始节点的边之后,节点 2 和节点 3 的入度都为 0,因此这个有向图有两个拓扑排序序列,分别为 [1, 2, 3] 和 [1, 3, 2]。

代码实现

class Solution {
public:
    bool sequenceReconstruction(vector<int>& nums, vector<vector<int>>& sequences) {
        unordered_map<int, unordered_set<int>> graph;
        unordered_map<int, int> inDegrees;
        for (vector<int>& sequence : sequences)
        {
            for (int i = 0; i < sequence.size(); ++i)
            {
                if (sequence[i] < 1 || sequence[i] > nums.size())
                    return false;
                
                if (!graph.count(sequence[i]))
                {
                    graph.insert({ sequence[i], unordered_set<int>() });
                    inDegrees.insert({ sequence[i], 0 });
                }
            }
​
            for (int i = 1; i < sequence.size(); ++i)
            {
                int prev = sequence[i - 1], cur = sequence[i];
                if (!graph[prev].count(cur))
                {
                    graph[prev].insert(cur);
                    ++inDegrees[cur];
                }
            }
        }
​
        queue<int> q;
        for (auto& kv : inDegrees)
        {
            if (kv.second == 0)
                q.push(kv.first);
        }
​
        vector<int> reconstruction;
        while (q.size() == 1)
        {
            int num = q.front();
            q.pop();
            reconstruction.push_back(num);
            for (int next : graph[num])
            {
                --inDegrees[next];
                if (inDegrees[next] == 0)
                    q.push(next);
            }
        }
​
        return reconstruction == nums;
    }
};

上述代码首先根据序列列表 seqs 构建有向图,有向图以邻接表的形式用 unordered_map 类型的 graph 保存。同时,统计每个节点的入度并保存到另一个 unordered_map 类型的 inDegrees 中。

接下来对构建的有向图按照广度优先搜索进行拓扑排序。队列 q 中保存的是入度为 0 的节点。每次从队列中取出一个节点添加到拓扑排序序列中,然后将所有与该节点相邻的节点的入度减 1(相当于删除所有以该节点为起始节点的边),如果发现有新的入度为 0 的节点则添加到队列之中。由于目标是判断图的拓扑排序序列是否唯一,而当某个时刻队列中的节点数目大于 1 时,就知道此时有多少个入度为 0 的节点,那么按照任意顺序排列这个入度为 0 的节点都能生成有效的拓扑排序序列,因此拓扑排序的序列不是唯一的。由此可知,上述代码只在队列的大小为 1 的时候重复添加入度为 0 的节点

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/561656.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

关于某次授权的大型内网渗透测试(1)

前期渗透&#xff1a; 打点&#xff1a;&#xff08;任意文件上传&#xff09; 直接发现头像处任意文件上传&#xff0c;这里直接上传冰蝎即可。 tasklist查看杀软 System Idle Process 0 N/A System …

细说postgresql之pg_rman备份恢复 —— 筑梦之路

pg_rman是一款开源的备份恢复软件&#xff0c;支持在线和基于PITR的备份恢复方式。 pg_rman类似于oracle的rman&#xff0c;可以进行全量、增量、归档日志的备份。 运行模式&#xff1a; 安装部署 Releases ossc-db/pg_rman GitHub 1、需要根据PG Server的版本&#xff0c;下…

05 MySQL--字段约束、事务、视图

1. CONSTRAINT 约束 创建表时&#xff0c;可以给表的字段添加约束&#xff0c;可以保证数据的完整性、有效性。比如大家上网注册用户时常见的&#xff1a;用户名不能为空。对不起&#xff0c;用户名已存在。等提示信息。 约束包括&#xff1a; 非空约束&#xff1a;not null检…

6.6 完数(project) educoder项目实训

前言 在最近的Python课上&#xff0c;做到这样一个“有趣”的作业&#xff0c;求得前n个完数&#xff0c;并输出其与其真约数的约数的加和式子&#xff0c;刚开始没怎么在意这个题&#xff0c;想着不就是做过好几遍了的语言基础练习题嘛&#xff0c;但是接下来的几小时没想到都…

探索设计模式的魅力:开启智慧之旅,AI与机器学习驱动的微服务设计模式探索

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索AI与机器学习驱动的微服务设计模式之旅✨ 亲爱的科技爱好者们&#xff0c;有没…

【独家推荐】视频下载神器:一键解决Mac/Win视频下载转换难题!

在信息爆炸的时代&#xff0c;视频已经成为我们获取知识和娱乐的重要途径。然而&#xff0c;很多精彩视频并没有提供直接的下载链接&#xff0c;这就给广大视频爱好者带来了不小的困扰。不过&#xff0c;现在有了这款专为Mac和Windows用户打造的视频下载转换器&#xff0c;一切…

Redux入门:使用@reduxjs/toolkit构建React应用程序状态管理

随着应用程序复杂性的增加,有效管理应用程序状态变得越来越重要。Redux是一种流行的状态管理解决方案,随着应用程序复杂性的增加,有效管理应用程序状态变得越来越重要。Redux是一种流行的状态管理解决方案,但传统的Redux设置和使用过程比较繁琐。幸运的是,Redux官方团队推出了r…

C语言实现贪吃蛇项目(2)

先来看看效果&#xff1a; 20240420_212115 文章目录&#xff1a; 3.项目实现3.0宽字符的打印3.01本地化操作setlocale函数宽字符的打印 3.1贪吃蛇结构的创建和维护3.11贪吃蛇结构的创建3.12贪吃蛇的维护 3.2初始化游戏3.21.打印欢迎界面、隐藏光标和设置窗口大小3.22.绘制地图…

记录好用的python包

记录好用的python包 PipxCentos 安装pipx确保 Pip 被安装更新 Pip安装 Pipx添加 Pipx 到 PATH临时添加到 PATH:永久添加到 PATH: 验证 Pipx 安装 Hatch安装特性 Poetry安装准备工作创建虚拟环境激活虚拟环境安装包追踪 & 更新包常用配置pycharm 远程连接poetry创建的虚拟环…

pycharm创建的项目

pycharm生成django templates删出 settings.py

数据分析_商品维度占比及变化可视化分析(Pandas和Matplotlib)

数据分析_商品维度占比及变化可视化分析(Pandas和Matplotlib) 分析维度包括: 各商品年度销量占比 各商品月度销量变化 构建测试数据 这里你可以了解到: 如何生成时间相关的数据。 如何从列表&#xff08;可迭代对象&#xff09;中生成随机数据。 Pandas 的 DataFrame 自…

IOS 32位调试环境搭建

一、背景 调试IOS程序经常使用gdb&#xff0c;目前gdb只支持32位程序调试&#xff0c;暂不支持IOS 64位程序调试。IOS 32位程序使用GDB调试之前&#xff0c;必须确保手机已越狱&#xff0c;否则无法安装和使用GDB调试软件。下面详细介绍GDB调试IOS 32位程序的环境搭建。 二、I…

数字时代的智慧演奏

数字化时代&#xff0c;工业不再是孤独的机器运转&#xff0c;而是演绎着一场智能与数据的华丽交响。无数智能节点的联动&#xff0c;数据的涌动&#xff0c;成为工业的新活力&#xff0c;同时也是创新的源泉。 工业互联网将每个机器、设备连接在一起&#xff0c;打破了原本独立…

从预训练损失的角度,理解语言模型的涌现能力

原文&#xff1a;Understanding Emergent Abilities of Language Models from the Loss Perspective 摘要 本文从预训练损失的角度重新审视语言模型的涌现能力&#xff0c;挑战了以往以模型大小或训练计算量为标准的观念。通过实验&#xff0c;作者发现预训练损失是预测下游任…

SRIO系列-时钟逻辑与复位逻辑

一、前言 上一篇讲述了SRIO协议的基本概念&#xff0c;传输的HELLO帧格式、事务类型等&#xff0c;本篇说一下SRIO IP核的时钟关系。 基本的IP设置可以参考此篇文章&#xff1a;【高速接口-RapidIO】Xilinx SRIO IP 核详解-CSDN博客 二、时钟关系 PHY可以在两个时钟域上运行…

C#语法知识之运算符

3、运算符 目录 3、运算符1、算数运算符思考 秒转化时间 2、字符串拼接3、条件运算符4、逻辑运算符5、位运算符6、三目运算符思考 闰年 1、算数运算符 1、赋值符号 //把右侧的值赋给左侧的变量2、算数运算符 _ * / float f 1 / 2f; %3、算数运算符的优先级 //乘除余优先级高…

【数据结构3-栈和队列】

数据结构3-栈和队列 1 栈-特殊的线性表-先进后出1.1 栈的三个案例 2 队列-与栈相反-先进先出2.1 队列的案例 3 用C实现栈的代码&#xff1a;4 用C实现队列的代码 1 栈-特殊的线性表-先进后出 1.1 栈的三个案例 2 队列-与栈相反-先进先出 2.1 队列的案例 3 用C实现栈的代码&…

<计算机网络自顶向下> TCP拥塞

目录 TCP拥塞控制机制 TCP拥塞感知 TCP速率控制方法 TCP拥塞控制和流量控制的联合动作 TCP拥塞控制策略 TCP吞吐量 TCP公平性 TCP拥塞控制机制 端到端的拥塞控制机制 路由器不向主机提供有关拥塞的反馈信息 路由器负担较轻 符合网络核心简单的TCP/IP架构原则 端系统根据自…

【机器学习】农田智能监控系统的实践探索

机器学习赋能现代农业&#xff1a;农田智能监控系统的实践探索 一、机器学习在现代农业中的重要作用二、机器学习在农田智能监控系统中的应用三、农田智能监控系统的实践意义 在科技飞速发展的今天&#xff0c;机器学习技术正以其强大的数据处理和模式识别能力&#xff0c;逐步…

Windows下Git的使用

目录 一、克隆远程仓库到本地二、git的三板斧2.1 add-将代码添加到本地仓库2.2 commit-提交代码到本地仓库2.3 push-推送本次添加操作到远程仓库2.4 gitee只有三板斧吗&#xff1f; 三、推送后没有出现绿点四、push到远程时报错五、git图形化界面下载链接 一、克隆远程仓库到本…