原题(Medium):
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
拓扑排序也可以通过 BFS 完成。
思路:拓扑排序
只看题可能不太容易理解题意,结合给出的说明和提示,我们应该明白这题考的就是给出一个有向图的节点数和边集,查找该有向图是否有环。建立拓扑排序首先肯定就是从入度为0的节点开始,然后把其指向的节点的入度减一,如果减一后有节点的入度归零,那么就把入度归零的节点放进拓扑排序里。举一个有向图经拓扑排序后的图:
我不想在此过多讨论拓扑排序的概念。我只想讨论如何实现拓扑排序。题目给予的资源是节点数和边集,为了实现拓扑排序,我们需要一个邻接链表和入度数组,根据边集,我们可以构建图的邻接链表和入度数组。具体实现可以是:定义一个map容器,里面存放每个节点出度点的集合,集合可用set容器表示。在遍历边集中,每一条边u→v,把v插入至key为u的set集合里,再把v的入度+1。在构建完邻接链表和入度数组后,可以从入度为零的节点出发,使用BFS来遍历整个图,BFS一般使用队列实现,所以先把入度为零的节点放进队列,然后在队列中按FIFO顺序把这些入度为零的点弹出,在邻接链表里找到它的出度点集合,把这些节点的入度数减一,减一之后查看这些节点的入度数是否归零,归零就放入队列,直到队列为空,就完成对图的拓扑排序。我们可以使用一个整型变量来记录从队列中弹出的节点的个数,每弹出一个节点就+1,如果图是有环的情况,那么所记录到的节点数就会与提供的节点数不一致。
1 bool canFinish(int numCourses, vector<vector<int>>& prerequisites) { 2 //邻接链表,set集合为的是帮其所指的节点按先后自动排序 3 map<int,set<int>> adjacent; 4 //入度数组 5 vector<int>indegree(numCourses); 6 7 //遍历边集,对于边集中的每条边来说,u→v 8 for(auto& edge : prerequisites) 9 { 10 int u = edge[0]; 11 int v = edge[1]; 12 13 adjacent[u].insert(v); //把v插入至key为u的set集合里 14 indegree[v]++; //再把v的入度+1 15 } 16 17 int count = 0; 18 queue<int> indeQ; 19 //拓扑排序的起点就是入度为0的节点,所以先把入度为0的节点放进队列中 20 for(int i = 0; i<numCourses; i++) 21 if(!indegree[i]) indeQ.push(i); 22 23 //在队列中按FIFO顺序弹出入度为0的结点,根据邻接链表把该节点所指的所有节点的入度数-1,如果有节点在-1之后入度数为0,放进队列中 24 while(!indeQ.empty()) 25 { 26 int node = indeQ.front(); 27 indeQ.pop(); 28 //每在队列中弹出一个节点,说明需要学习的课程+1 29 count++; 30 //根据邻接链表把该节点所指的所有节点(是个set集合) 31 auto& adjs = adjacent[node]; 32 //入度数都-1 33 for(auto adj : adjs) 34 { 35 indegree[adj]--; 36 //如果有节点在-1之后入度数为0,放进队列中 37 if(!indegree[adj]) indeQ.push(adj); 38 } 39 } 40 //如果需要学习的课程数与课程总量不想等,说明存在先决条件成环 41 return count == numCourses; 42 }