【拓扑系列】拓扑排序
前言
认识有向无环图
图中不会形成环状的有向图就是有向无环图。
入度和出度
- 出度:一个顶点指向另一个节点的数量。
- 入度:一个顶点有多少是指向它的。
认识AOV网:顶点活动图
- AOV网(Activity On Vertex Network)是一个用顶点表示活动,有向边表示活动之间优先关系的有向图。在这个图中,顶点代表工程中的各个活动,而有向边则反映了这些活动之间的先后依赖关系。
AOV网的特点包括:
- 顶点表示活动:在AOV网中,每一个顶点都代表一个具体的活动,这些活动构成了工程的各个组成部分。
有向边表示优先关系:有向边用来表示活动之间的优先关系,即一个活动必须在另一个活动完成之后才能开始。这种关系通过有向边在图中进行表示,从而形成一个有向图。 - 无回路:AOV网中不允许存在回路,即不存在一个活动直接或间接地依赖于它自身的完成,这保证了活动的逻辑顺序和工程的可行性。
- AOV网的概念在现代化管理中非常有用,特别是在描述和分析一项工程的计划和实施过程中。通过将工程分解为多个小的子工程(即活动),并用有向图来表示这些活动之间的依赖关系,可以帮助理解和优化工程的执行流程。此外,拓扑排序是AOV网的一个重要应用,用于判断网中是否存在环,确保所有活动都能按照正确的顺序执行,避免出现无法完成的循环依赖。
拓扑排序
-
拓扑排序是对一个有向无环图(DAG)进行排序,将图中的所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。12
-
拓扑排序常用于确定一个依赖关系集中事物发生的顺序。例如,在项目管理中,可以确定项目各阶段之间的依赖关系,从而计算出项目的执行顺序。
-
拓扑排序的实现方法通常使用深度优先搜索(DFS)或广度优先搜索(BFS)。在树结构中,拓扑排序可以简单地通过层次遍历实现。
我们用更直接的话来讲就是:拓扑排序的作用就是找到做事情的先后顺序,并且拓扑排序的结果不唯一。
拓扑排序的具体操作:
- 找出图中入度为0的节点,然后输出
- 删除这个节点的连接的边
- 重复步骤1,2操作,直到图中没有点或者没有入度为0的节点为止(可能存在环)。所以可以根据这个特性判断图中是否有环。
实现拓扑排序:借助队列,来一次bfs即可
4. 初始化:将所有入度为0的节点添加的队列中
5. 当队列不为空的时候,拿出队头元素添加到结果中,删除该元素相连的边,判断删完后与删除边相连接的点入度是否为0,如果为0添加到队列,然后一直循环。
1. 课程表
1.1 题目来源
1.2 题目描述
你这个学期必须选修 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] 中的所有课程对 互不相同
1.3 题目解析
class Solution {
public:
vector<vector<int>> edges;
vector<int> in;
queue<int> q;
vector<int> ret;
bool bfs(int n)
{
// 将入度数为0的放入队列
for (int i = 0; i < n; i++)
{
if (in[i] == 0)
{
q.push(i);
}
}
while (!q.empty())
{
int t = q.front();
q.pop();
ret.push_back(t);
for (auto v : edges[t])
{
if(--in[v] == 0)
{
q.push(v);
}
}
}
return ret.size() == n; // 判断是否有环
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
in.resize(numCourses);
edges.resize(numCourses);
// 建图
for (auto point : prerequisites)
{
edges[point[1]].push_back(point[0]);
//计入计入入度数
in[point[0]]++;
}
return bfs(numCourses);
}
};
2. 课程表 II
2.1 题目来源
2.2 题目描述
现在你总共有 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] 互不相同
2.3 题目解析
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
vector<vector<int>> edges(numCourses);
vector<int> in(numCourses);
vector<int> ret;
queue<int> q;
// 建图
for (auto v : prerequisites)
{
edges[v[1]].push_back(v[0]);
in[v[0]]++;
}
//将入度数为0的放入队列
for (int i = 0; i < numCourses; i++)
{
if (in[i] == 0) q.push(i);
}
//深度遍历
while (!q.empty())
{
int t = q.front();
q.pop();
ret.push_back(t);
for (auto v : edges[t])
{
if (--in[v] == 0)
q.push(v);
}
}
if (ret.size() == numCourses)
return ret;
else return vector<int>();
}
};
3. LCR 114. 火星词典
3.1 题目来源
3.2 题目描述
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 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”]
输出:“”
解释:不存在合法字母顺序,因此返回 “” 。- 示例 4:
输入:words = [“abc”,“ab”]
输出:“”
解释:不存在合法字母顺序,因此返回 “” 。
提示:
1 <= words.length <= 100
1 <= words[i].length <= 100
words[i] 仅由小写英文字母组成
3.3 题目解析
这个题目的关键就在于要理解题目意思,题目意思是,两个字符进行对比,一旦遇到了不相同的字符,字符在前面的是小于字符在后面的,并且如果s1的长度小于s2的话并且s1的长度大于s2的长,如果s2等于s1的部分字符串,就类似于示例4,那么也是不符合的。
所以我们该怎么处理words字典呢,这里我们就可以直接使用两层for循环,依次进行比较,并且一旦找到了的话其实可以用图的方式进行来连接例如(a->b 就标识a小于b)于是有了这个思路我们就可以使用拓扑排序了。
上面的几题都是对int整形进行比较的,也是使用的vector来存放图,但是这里就不一样了,类型是char,所以这里我们一应该使用hash表来进行存储,即unordered_map<char, unordered_set< char >>,因为字典中会后重复的,所以我们的邻接表也应该使用一个hash来进行存储。
而至于我们存放入度数的话也不能直接使用vector或者数组来存储了,也应该使用hash来进行存储。
所以我们的具体步骤就是:
- 创建unordered_map<char, unordered_set< char>> edges来建图,创建unordered_map<char, int> in来存放入度数
- 遍历整个字典将初始化入度in
- 使用双指针遍历字典,建立图
- 深度遍历
class Solution {
public:
unordered_map<char, unordered_set<char>> edges;
unordered_map<char, int> in;
queue<char> q;
string ret;
string alienOrder(vector<string>& words)
{
for (auto &s : words)
{
for (auto c : s)
{
in[c] = 0;
}
}
// 建图
for (int i = 0; i < words.size(); i++)
{
for (int j = i + 1; j < words.size(); j++)
{
if(add(words[i], words[j]))
return "";
}
}
// 将度为0的放入队列
for (auto &[a, b] : in)
{
if (b == 0)
q.push(a);
}
// 进行深度遍历
while (!q.empty())
{
char t = q.front();
q.pop();
ret += t;
for (auto v : edges[t])
{
if (--in[v] == 0)
q.push(v);
}
}
std::cout << ret;
if (ret.size() == in.size()) return ret;
else return "";
}
bool add (string& s1, string& s2)
{
int n = min(s1.size(), s2.size());
int i = 0;
for (; i < n; 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;
}
}
if (i == s2.size() && i < s1.size()) return true; // 处理特殊情况,示例4
else return false;
}
};