写在前面:
今天我们继续来看一道经典的图论问题,而这个问题可以说是跟我们一众学生的生活息息相关啊!我们每年都有很多需要完成的必修指标,每一个必修指标可能会有一个或多个先修要求,而我们需要决定是否能将这些课全都上一遍,这不就是咱们苦逼大学生每学期选课前的日常嘛!那既然如此,我们就来看看这道与我们生活息息相关的这道算法题吧~~
题目介绍:
题目信息:
- 题目链接:https://leetcode.com/problems/course-schedule-ii/description/
- 题目类型:DFS,Graph,Adjacent List,Topology
- 题目难度:Medium,但其实我觉得作为一道 hard 也不是不可以
- 题目来源:Google 高频面试真题
题目介绍:
- 给定一个整数,代表所有需要修的课的总数,所有需要修的课为 0, ...., numCourses - 1
- 给定一个数组,每一个元素是一个长度为 2 的小数组,每一个小数组的第一个为目标课程,第二个为这个目标课程所需要的先修 (prerequisites)
- 找出一个可以把全部课上完的组合,返回这个组合
- 如果不可能都上完,则返回一个空数组
题目想法:
图论转化:
这道题目的关键是在于找到对应关系,而这个对应关系就来源于 prerequisite,也就是先修, 即:a ---> b 一定得先上过 a 才能上 b
这样的话,这道题其实就是一个巨大的单向图的问题,每一个课都是一个节点,而我们可以从任何一个节点开始,只需要找到一个可以不重复的访问所有节点的策略就可以了。同时,这道题目可以允许多个起点,因为对于一些没有任何 prerequisite 的课,在图中表示为游离点,我们也是可以直接上的,所以我们只需要找有连接的点中,有没有内置的循环即可。当且仅当在图中的一个部分存在循环的时候,我们才无法上完所有的课
ADJ List
这道题的 adjacent List 也相对比较好想直接,我们只需要遍历所有的 prerequiste,将每一个prerequisite[0] 作为 dest,prerequisite[1] 作为 src 就可以了,我们将会形成一个:
adjacentList<src, vector<dest>>
Traverse 图的方法:
方法1: DFS
这是一种相对比较标准的有序图的遍历解法。核心思想就是选定一个出发点,一路 traverse 直到到底(在我们的场景下就是最后一个最高阶的课程),然后再退回寻找其他路径,直到所有图都被覆盖。
在使用 DFS 进行遍历的时候,我们可以用一些小 tricks 来减少重复的遍历:
- 利用 全局bool,如果一次遍历出现循环,则全局可能性为 false,不需要再遍历了
- 利用“染色”的方法,没有被处理的点是白色,当前一轮 dfs 正在处理的点为灰色,而 dfs 结束以后的点处理为灰色
- 我们进行起点选择的时候,只选择还是白色的点作为起点
- 我们在遍历途中,如果发现我们将要去的点为灰色,这说明我们这次遍历遇到了循环,因为灰色意味着他和我们是同一组遍历被发现的,这个时候可以全局停止遍历了
- 当一个点完成遍历以后,我们把他标记为黑色,并且放入已遍历的数组中
- 因为DFS,一定是最高级的,也就是图中最后一个被遍历到的点先被放进数组,所以我们在输出结果的时候,将已遍历的结果倒转一下输出即可
- Runtime O(V+E) 我们遍历了所有的 node 和 edge 各一次
- Space O(V+E) 我们存储的也是所有的 node 和 他们的 edge
方法2: Indegree map 和 queue
这种方法是一种我们比较好想的方法,来源于我们日常生活中的思维模式:
在考虑一节课能不能上的时候,我们通常会考虑他的 prerequisite 有没有上完,而缺了几节 prerequisite,就意味着我们和这节课还有多少节课的差距
如果这个课没有任何依赖的话,我们就可以直接上这门课。
这个特性刚好可以图论中的 degree 特性连接起来。在一个有序图中,一个点的 degree 可以被表示为有多少个点以他为目标。在我们写 adj List 的时候,我们就可以同时记录这个点的 indegree,每当我们记录到一个 pair 的时候,这个 pair 的 dest 对应的点的 indegree 就要 + 1
而在我们进行遍历的时候,我们先将所有 indegree 为 0 的点放入 queue,假装我们是要上这些课,然后在不断的 pop 的过程中,就好似我们上完了一节节课以后,对应的其他课的 prerequisite 就减少了,也离我们更近了,所以对应这节课的所有 adj 的邻居 indegree 都 -1。而如果有新的课 indegree 变成 0 以后,意味着我们又可以上这门课了,我们就把他放入到 queue 中。一直反复直到我们的 queue 为空,无课可上了。
- Runtime:O(V+E)
- Space: O(V+E)
Note: 两种方法在 speed 和 space 上是相同量级的,但是因为 DFS 要使用 recursion 来进行实现,无论是内存的占用还是效率都是比不上仅使用循环的第二种方法的。实测第二种方法也是跑的相对更快一些,也更被我们的思维所接受。
题目代码:
方法1(DFS):
class Solution {
public:
int WHITE = 1;
int GRAY = 2;
int BLACK = 3;
void DFS(unordered_map<int, vector<int>> adjacentList, vector<int>& color, bool& isPossible, vector<int>& topologicalOrder, int i) {
//break the research if it is impossible
if(!isPossible)
return;
//the current process indicator, if we meet another gray when we want to traverse --> loop
color[i] = GRAY;
for(int node: adjacentList[i]){
if(color[node] == WHITE){
//a new node we can visit and try
DFS(adjacentList, color, isPossible, topologicalOrder, node);
}else if(color[node] == GRAY){
//we encounter a loop, making the whole process not possible
isPossible = false;
}
}
//finish traverse this node, make it black, marked as visited and settled
color[i] = BLACK;
topologicalOrder.push_back(i);
}
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
//create adjacent list and the topological color scheme
unordered_map<int, vector<int>> adjacentList;
vector<int> color(numCourses, WHITE);
bool isPossible = true;
vector<int> topologicalOrder;
//fill out the adjacent List
for(int i = 0; i < prerequisites.size(); i++){
adjacentList[prerequisites[i][1]].push_back(prerequisites[i][0]);
}
//iterate every possible, non visited node using DFS
for(int i = 0; i < numCourses; i++){
if(!isPossible)
break;
if(color[i] == WHITE)
DFS(adjacentList, color, isPossible, topologicalOrder, i);
}
//reverse the topological order if needed:
vector<int> order;
if(isPossible){
order.resize(numCourses);
for(int i = 0; i < numCourses; i++){
order[i] = topologicalOrder[numCourses-i-1];
}
}
return order;
}
};
方法2(Queue):
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
bool isPossible = true;
vector<int> inDegree(numCourses, 0);
map<int, vector<int>> adjList;
vector<int> res;
// Create the adjacency list representation of the graph.
for (vector<int> relation : prerequisites) {
int dest = relation[0];
int src = relation[1];
adjList[src].push_back(dest); // connect the neighbors
inDegree[dest] += 1; //since there is one more node to reach him
}
//first, we push every node with 0 indegree into the queue, since they are free to start:
//we keep remove the nodes out of the graph, decresing their neighbours degrees as if
//we are finish one course and move to another.
queue<int> zeroDegree;
for(int i = 0; i < numCourses; i++){
if(inDegree[i] == 0){
zeroDegree.push(i);
}
}
while(!zeroDegree.empty()){
int current = zeroDegree.front();
zeroDegree.pop();
res.emplace_back(current);
for(int course: adjList[current]){
inDegree[course] -= 1;
//if there is a free elective rn, we push to the queue
if(inDegree[course] == 0){
zeroDegree.push(course);
}
}
}
if(res.size() == numCourses){
return res;
}
return vector<int>();
}
};