文章目录
前言
在数据结构中我们学过 拓扑排序以及图的相关知识,在这里我们进行简单的复习↓
有向无环图
我们下文要解的算法题,都可以用这种关系图来表示。
什么是拓扑排序?
在数据结构中我们学过:
- 拓扑排序是一种对 有向无环图(DAG) 进行排序的算法。
- 在对于我们下面的解题,可以理解为拓扑排序是确定任务执行顺序的
- 如果图中存在环路,那么该图就没有拓扑排序,可以由此利用拓扑排序判断图是否有环
拓扑排序 实现思路
由于拓扑排序是用于找到一系列事件执行的先后顺序,实际上结果并不一定唯一。我们只需要从某个起点开始进行搜索,每次删掉搜索位置,直至最后看图是否有环。
如何进行排序?
- 找出图中入度为 0 的点,然后输出
- 删除与该点连接的边
- 重复 1、2 操作,直到图中没有点 / 没有入度为 0 的点为止
拓扑排序 代码思路
简单来说,我们提供的方法:借助队列,进行一次BFS
- 初始化: 把所有入度为 0 的点加入到队列中
- 当队列不为空时
- 取队头元素,加入到最终结果中
- 删除与该元素相连的边
- 最后判断: 与删除边相连的点,入度是否变为 0
- 如果入度为 0,加入到队列中
示例题
我们通过下面一道题加深对拓扑排序的理解,以及代码的编写。
207.课程表
示例:
思路
- 题意分析:正如示例图所提到的,我们所需做的就是将题目给出的选修课顺序作图,并利用拓扑排序判断该图是否可以按顺序读完(无环)
怎么利用代码作图?
我们知道,对于任意的有向图,都可以用相应的邻接表 / 邻接矩阵表示,当我们要用代码作图,只需要利用邻接矩阵 + 容器即可。
- 邻接矩阵:
- 容器:
我们可以选用如上图 ① 二维数组 ② 哈希表,进行邻接表的表示。
根据前面提到的实现思路 + 代码思路 ,以及上面的作图方法,就可以着手写代码了
代码
cpp
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 0. 初始化
unordered_map<int, vector<int>> AList; // 哈希表作邻接表 表示图
vector<int> inDegree(numCourses); // 记录每个点的入度
// 1. 建图
for(auto e : prerequisites)
{
int a = e[0], b = e[1]; // 提取一条边: b->a
AList[b].push_back(a);
inDegree[a]++; // 更新入度
}
// 拓扑排序
// (1) 将所有入度为0的点加入到队列
queue<int> q;
for(int i = 0; i < numCourses; ++i)
{
if(inDegree[i] == 0)
q.push(i);
}
// (2) 进行bfs
while(q.size())
{
int t = q.front(); q.pop();
// 对邻接表的行t进行:
for(int x : AList[t])
{
inDegree[x]--;
if(inDegree[x] == 0) q.push(x);
}
}
// 3. 判断此时表是否有环
for(int i = 0; i < inDegree.size(); ++i)
if(inDegree[i]) return false; // in[i] 如果!=0 则入度不为0,依然有元素(成环)
return true;
}
210.课程表II
思路
-
题意分析:这道题的思路与上一道题一模一样!唯一的区别就是本题要求返回任意一种存在的顺序即可。
-
解法 :拓扑排序 + bfs
- 这里我们选择用
vector<vector<int>>
进行邻接表的创建 - 我们只需要在bfs每次提取队头元素时,将当前元素加入到结果数组中即可,其余部分代码没有区别。
- 这里我们选择用
代码
cpp
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> AList(numCourses); // 邻接表
vector<int> inD(numCourses), ret; // inD用于记录入度
// 1. 建图
for(auto e : prerequisites)
{
int a = e[0], b = e[1];
inD[a]++;
AList[b].push_back(a);
}
// 2. 将入度为0的点入队
queue<int> q;
for(int i = 0; i < inD.size(); ++i)
if(inD[i] == 0) q.push(i);
// 3. 拓扑排序
// (1) bfs
while(q.size())
{
int t = q.front(); q.pop();
ret.push_back(t);
for(int x : AList[t])
{
inD[x]--;
if(inD[x] == 0)
q.push(x);
}
}
// 存在环,返回空数组
if(ret.size() != numCourses) return {};
return ret;
}
LCR114.火星词典
思路
- 题目分析 :题目要求根据给出的单词序列 求出相应的字典序,我们对示例1 进行分析:
- 由上图思路,我们便可以知道这道题可以用拓扑排序解决。
- 解法 :拓扑排序 + bfs
- 细节问题 :
- 建图:在之前的题中,由于我们建图得到的元素都是由题干直接给出,且为数字,不会重复。我们有哈希嵌套数组 与数组嵌套数组 两种方式。
对于本题由于单词序列中不同单词的比较可能会有相同的结果,为避免数据重复、我们可以使用哈希嵌套哈希 的方式建图。hash<type, hash<type>>
- 入度统计:本题中的元素是小写字母且并不一定都存在, 如果用数组统计入度,对于不存在的字母入度一样为0,虽然可以设定初始值,但我们这里用哈希表直接解决。
hash<char, int>
- 比较单词的方法:利用一个指针,分别比较两个单词的字母即可。
- 特殊情况:当存在"abc" "ab" 的比较时,此时序列是不合法的,需要作特殊情况写出。
- 建图:在之前的题中,由于我们建图得到的元素都是由题干直接给出,且为数字,不会重复。我们有哈希嵌套数组 与数组嵌套数组 两种方式。
代码
cpp
class Solution {
public:
// 定义全局变量,在写功能函数时不用多余传参
unordered_map<char, unordered_set<char>> AList; // 邻接表建图
unordered_map<char, int> inD; // 统计入度
bool Illegal; // 用于标记比较是否合法
string alienOrder(vector<string>& words) {
// 初始化入度哈希表 + 建图
for(string &s : words)
for(char ch : s)
inD[ch] = 0; // 初始化入度哈希表
for(int i = 0; i < words.size(); ++i)
for(int j = i + 1; j < words.size(); ++j)
{
compare(words[i], words[j]);
if(Illegal) return "";
}
// 拓扑排序
queue<char> q;
for(auto [a, b] : inD) // 将入度为0的字母加入到队列
if(b == 0) q.push(a);
string ret = "";
while(q.size())
{
char t = q.front(); q.pop();
ret += t;
for(char ch : AList[t])
{
if(--inD[ch] == 0) q.push(ch);
}
}
// 判断结果
for(auto [a, b] : inD)
if(b != 0) return "";
return ret;
}
void compare(string &s1, string &s2)
{
int minLen = min(s1.size(), s2.size());
int i = 0; // 循环外也需要i,定义到循环外
for(; i < minLen; ++i)
{
if(s1[i] != s2[i])
{
char a = s1[i], b = s2[i];
// 如果邻接哈希表没有a / 邻接表没有a->b
if(!AList.count(a) || !AList[a].count(b)){
AList[a].insert(b); // 加上关系 a->b
inD[b]++;
}
break;
}
}
// 特殊情况:abc ab
if(i == minLen && s1.size() > s2.size()) Illegal = true;
}
};