问题:java
There are a total of n courses you have to take, labeled from 0
to n - 1
.node
Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]
算法
Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?express
For example:数组
2, [[1,0]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.数据结构
2, [[1,0],[0,1]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.机器学习
Note:ide
Hints:学习
解决:ui
【解析】求Course Schedule,等同问题是有向图检测环,vertex是course, edge是prerequisite。我以为通常会使用Topological Sorting拓扑排序来检测。一个有向图假若有环则不存在Topological Order。一个DAG的Topological Order能够有大于1种。
① BFS的解法,咱们定义二维数组graph来表示这个有向图,一位数组in来表示每一个顶点的入度。咱们开始先根据输入来创建这个有向图,并将入度数组也初始化好。而后咱们定义一个queue变量,将全部入度为0的点放入队列中,而后开始遍历队列,从graph里遍历其链接的点,每到达一个新节点,将其入度减一,若是此时该点入度为0,则放入队列末尾。直到遍历完队列中全部的值,若此时还有节点的入度不为0,则说明环存在,返回false,反之则返回true。
class Solution {//30ms
public boolean canFinish(int numCourses, int[][] prerequisites) {
int len = prerequisites.length;
if (numCourses == 0 || len == 0) {
return true;
}
int[] in = new int[numCourses];//记录节点的入度
for (int i = 0;i < len;i ++){//遍历有向边,初始化入度
in[prerequisites[i][0]] ++;
}
Queue<Integer> queue = new LinkedList<>();//保存入度为0的节点
for (int i = 0;i < numCourses;i ++){
if (in[i] == 0){
queue.offer(i);
}
}
int count = queue.size();//记录入度为0的节点数
while(! queue.isEmpty()){
int top = queue.poll();
for (int[] tmp : prerequisites ) {//顶节点指向的节点的入度-1
if (tmp[1] == top) {
in[tmp[0]] --;
if (in[tmp[0]] == 0) {
count ++;
queue.offer(tmp[0]);
}
}
}
}
return count == numCourses;
}
}
② DFS方法,递归判断是否存在环,若存在,则返回false,不然,返回true。
class Solution{ // 13 ms
public boolean canFinish(int numCourses,int[][] pre){
if (numCourses == 0 || pre.length == 0) {
return true;
}
boolean[] isvisted = new boolean[numCourses];//记录节点的状态
boolean[] isonstack = new boolean[numCourses];
Map<Integer,List<Integer>> adjacent = new HashMap<>();//保存依赖于key课程的课程的链表
for (int[] tmp : pre) {
if (adjacent.containsKey(tmp[1])) {
adjacent.get(tmp[1]).add(tmp[0]);
}else{
List<Integer> list = new ArrayList<>();
list.add(tmp[0]);
adjacent.put(tmp[1],list);
}
}
for (int i = 0;i < numCourses ;i ++ ) {
if (dfs(adjacent,isvisted,isonstack,i)) {//存在环
return false;
}
}
return true;
}
public boolean dfs(Map<Integer,List<Integer>> adjacent,boolean[] isvisited,boolean[] isonstack,int i){
isvisited[i] = true;
isonstack[i] = true;
if (adjacent.containsKey(i)) {
for (int j : adjacent.get(i)) {
if (! isvisited[j] && dfs(adjacent,isvisited,isonstack,j)) {
return true;
}
if (isonstack[j]) {
return true;
}
}
}
isonstack[i] = false;
return false;
}
}
③ 在discuss中看到的。
class Solution {//1ms
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] root = new int[numCourses];//使用数组保存节点对应的前缀
Arrays.fill(root,-1);
for (int[] tmp : prerequisites) {
int cur = tmp[1];
while(cur >= 0){
cur = root[cur];
if (cur == tmp[0]) {//存在环
return false;
}
}
root[tmp[0]] = tmp[1];
}
return true;
}
}
【注---拓扑排序】http://blog.csdn.net/dm_vincent/article/details/7714519
一、定义:将有向图中的顶点以线性方式进行排序。即对于任何链接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u老是在顶点v的前面。
若是这个概念还略显抽象的话,那么不妨考虑一个很是很是经典的例子——选课。我想任何看过数据结构相关书籍的同窗都知道它吧。假设我很是想学习一门机器学习的课程,可是在修这么课程以前,咱们必需要学习一些基础课程,好比计算机科学概论,C语言程序设计,数据结构,算法等等。那么这个制定选修课程顺序的过程,实际上就是一个拓扑排序的过程,每门课程至关于有向图中的一个顶点,而链接顶点之间的有向边就是课程学习的前后关系。只不过这个过程不是那么复杂,从而很天然的在咱们的大脑中完成了。将这个过程以算法的形式描述出来的结果,就是拓扑排序。
那么是否是全部的有向图都可以被拓扑排序呢?显然不是。继续考虑上面的例子,若是告诉你在选修计算机科学概论这门课以前须要你先学习机器学习,你是否是会被弄糊涂?在这种状况下,就没法进行拓扑排序,由于它中间存在互相依赖的关系,从而没法肯定谁先谁后。在有向图中,这种状况被描述为存在环路。所以,一个有向图能被拓扑排序的充要条件就是它是一个有向无环图(DAG:Directed Acyclic Graph)。
二、偏序/全序关系:
偏序和全序其实是离散数学中的概念。
仍是以上面选课的例子来描述这两个概念。假设咱们在学习完了算法这门课后,能够选修机器学习或者计算机图形学。这个或者表示,学习机器学习和计算机图形学这两门课之间没有特定的前后顺序。所以,在咱们全部能够选择的课程中,任意两门课程之间的关系要么是肯定的(即拥有前后关系),要么是不肯定的(即没有前后关系),绝对不存在互相矛盾的关系(即环路)。以上就是偏序的意义,抽象而言,有向图中两个顶点之间不存在环路,至于连通与否,是无所谓的。因此,有向无环图必然是知足偏序关系的。
理解了偏序的概念,那么全序就好办了。所谓全序,就是在偏序的基础之上,有向无环图中的任意一对顶点还须要有明确的关系(反映在图中,就是单向连通的关系,注意不能双向连通,那就成环了)。可见,全序就是偏序的一种特殊状况。回到咱们的选课例子中,若是机器学习须要在学习了计算机图形学以后才能学习(可能学的是图形学领域相关的机器学习算法……),那么它们之间也就存在了肯定的前后顺序,本来的偏序关系就变成了全序关系。
实际上,不少地方都存在偏序和全序的概念。
好比对若干互不相等的整数进行排序,最后老是可以获得惟一的排序结果(从小到大,下同)。这个结论应该不会有人表示疑问吧:)可是若是咱们以偏序/全序的角度来考虑一下这个再天然不过的问题,可能就会有别的体会了。
那么如何用偏序/全序来解释排序结果的惟一性呢?
咱们知道不一样整数之间的大小关系是肯定的,即1老是小于4的,不会有人说1大于或者等于4吧。这就是说,这个序列是知足全序关系的。而对于拥有全序关系的结构(如拥有不一样整数的数组),在其线性化(排序)以后的结果必然是惟一的。对于排序的算法,咱们评价指标之一是看该排序算法是否稳定,即值相同的元素的排序结果是否和出现的顺序一致。好比,咱们说快速排序是不稳定的,这是由于最后的快排结果中相同元素的出现顺序和排序前不一致了。若是用偏序的概念能够这样解释这一现象:相同值的元素之间的关系是没法肯定的。所以它们在最终的结果中的出现顺序能够是任意的。而对于诸如插入排序这种稳定性排序,它们对于值相同的元素,还有一个潜在的比较方式,即比较它们的出现顺序,出现靠前的元素大于出现后出现的元素。所以经过这一潜在的比较,将偏序关系转换为了全序关系,从而保证告终果的惟一性。
拓展到拓扑排序中,结果具备惟一性的条件也是其全部顶点之间都具备全序关系。若是没有这一层全序关系,那么拓扑排序的结果也就不是惟一的了。在后面会谈到,若是拓扑排序的结果惟一,那么该拓扑排序的结果同时也表明了一条哈密顿路径。
三、典型实现算法
(1)Kahn算法:
摘一段维基百科上关于Kahn算法的伪码描述:
L← Empty list that will contain the sorted elements S ← Set of all nodes with no incoming edges while S is non-empty do remove a node n from S insert n into L foreach node m with an edge e from nto m do remove edge e from thegraph ifm has no other incoming edges then insert m into S if graph has edges then return error (graph has at least onecycle) else return L (a topologically sortedorder)
不难看出该算法的实现十分直观,关键在于须要维护一个入度为0的顶点的集合:
每次从该集合中取出(没有特殊的取出规则,随机取出也行,使用队列/栈也行,下同)一个顶点,将该顶点放入保存结果的List中。
紧接着循环遍历由该顶点引出的全部边,从图中移除这条边,同时获取该边的另一个顶点,若是该顶点的入度在减去本条边以后为0,那么也将这个顶点放到入度为0的集合中。而后继续从集合中取出一个顶点…………
当集合为空以后,检查图中是否还存在任何边,若是存在的话,说明图中至少存在一条环路。不存在的话则返回结果List,此List中的顺序就是对图进行拓扑排序的结果。
对上图进行拓扑排序的结果:
2->8->0->3->7->1->5->6->9->4->11->10->12
复杂度分析:
初始化入度为0的集合须要遍历整张图,检查每一个节点和每条边,所以复杂度为O(E+V);
而后对该集合进行操做,又须要遍历整张图中的,每条边,复杂度也为O(E+V);
所以Kahn算法的复杂度即为O(E+V)。
(2)基于DFS的拓扑排序:
除了使用上面直观的Kahn算法以外,还可以借助深度优先遍从来实现拓扑排序。这个时候须要使用到栈结构来记录拓扑排序的结果。
一样摘录一段维基百科上的伪码:
L ← Empty list that will contain the sorted nodes S ← Set of all nodes with no outgoing edges for each node n in S do visit(n) function visit(node n) if n has not been visited yet then mark n as visited for each node m with an edgefrom m to ndo visit(m) add n to L
DFS的实现更加简单直观,使用递归实现。利用DFS实现拓扑排序,实际上只须要添加一行代码,即上面伪码中的最后一行:add n to L。
须要注意的是,将顶点添加到结果List中的时机是在visit方法即将退出之时。
这个算法的实现很是简单,可是要理解的话就相对复杂一点。
关键在于为何在visit方法的最后将该顶点添加到一个集合中,就能保证这个集合就是拓扑排序的结果呢?
由于添加顶点到集合中的时机是在dfs方法即将退出之时,而dfs方法自己是个递归方法,只要当前顶点还存在边指向其它任何顶点,它就会递归调用dfs方法,而不会退出。所以,退出dfs方法,意味着当前顶点没有指向其它顶点的边了,即当前顶点是一条路径上的最后一个顶点。
下面简单证实一下它的正确性:
考虑任意的边v->w,当调用dfs(v)的时候,有以下三种状况:
须要注意的是,以上第三种状况在拓扑排序的场景下是不可能发生的,由于若是状况3是合法的话,就表示存在一条由w到v的路径。而如今咱们的前提条件是由v到w有一条边,这就致使咱们的图中存在环路,从而该图就不是一个有向无环图(DAG),而咱们已经知道,非有向无环图是不能被拓扑排序的。
那么考虑前两种状况,不管是状况1仍是状况2,w都会先于v被添加到结果列表中。因此边v->w老是由结果集中后出现的顶点指向先出现的顶点。为了让结果更天然一些,可使用栈来做为存储最终结果的数据结构,从而可以保证边v->w老是由结果集中先出现的顶点指向后出现的顶点。
复杂度分析:
复杂度同DFS一致,即O(E+V)。具体而言,首先须要保证图是有向无环图,判断图是DAG可使用基于DFS的算法,复杂度为O(E+V),然后面的拓扑排序也是依赖于DFS,复杂度为O(E+V)
仍是对上文中的那张有向图进行拓扑排序,只不过此次使用的是基于DFS的算法,结果是:
8->7->2->3->0->6->9->10->11->12->1->5->4
(3)两种实现算法的总结:
这两种算法分别使用链表和栈来表示结果集。
对于基于DFS的算法,加入结果集的条件是:顶点的出度为0。这个条件和Kahn算法中入度为0的顶点集合彷佛有着殊途同归之妙,这两种算法的思想犹如一枚硬币的两面,看似矛盾,实则否则。一个是从入度的角度来构造结果集,另外一个则是从出度的角度来构造。
实现上的一些不一样之处:
Kahn算法不须要检测图为DAG,若是图为DAG,那么在出度为0的集合为空以后,图中还存在没有被移除的边,这就说明了图中存在环路。而基于DFS的算法须要首先肯定图为DAG,固然也可以作出适当调整,让环路的检测和拓扑排序同时进行,毕竟环路检测也可以在DFS的基础上进行。
两者的复杂度均为O(V+E)。
四、拓扑排序解的惟一性:
哈密顿路径:
哈密顿路径是指一条可以对图中全部顶点正好访问一次的路径。本文中只会解释一些哈密顿路径和拓扑排序的关系,至于哈密顿路径的具体定义以及应用,能够参见本文开篇给出的连接。
前面说过,当一个DAG中的任何两个顶点之间都存在能够肯定的前后关系时,对该DAG进行拓扑排序的解是惟一的。这是由于它们造成了全序的关系,而对存在全序关系的结构进行线性化以后的结果必然是惟一的(好比对一批整数使用稳定的排序算法进行排序的结果必然就是惟一的)。
须要注意的是,非DAG也是可以含有哈密顿路径的,为了利用拓扑排序来实现判断,因此这里讨论的主要是判断DAG中是否含有哈密顿路径的算法,所以下文中的图指代的都是DAG。
那么知道了哈密顿路径和拓扑排序的关系,咱们如何快速检测一张图是否存在哈密顿路径呢?
根据前面的讨论,是否存在哈密顿路径的关键,就是肯定图中的顶点是否存在全序的关系,而全序的关键,就是任意一对顶点之间都是可以肯定前后关系的。所以,咱们可以设计一个算法,用来遍历顶点集中的每一对顶点,而后检查它们之间是否存在前后关系,若是全部的顶点对有前后关系,那么该图的顶点集就存在全序关系,即图中存在哈密顿路径。
可是很显然,这样的算法十分低效。对于大规模的顶点集,是没法应用这种解决方案的。一般一个低效的解决办法,十有八九是由于没有抓住现有问题的一些特征而致使的。所以咱们回过头来再看看这个问题,有什么特征使咱们没有利用的。仍是举对整数进行排序的例子:
好比如今有3, 2, 1三个整数,咱们要对它们进行排序,按照以前的思想,咱们分别对(1,2),(2,3),(1,3)进行比较,这样须要三次比较,可是咱们很清楚,1和3的那次比较其实是多余的。咱们为何知道此次比较是多余的呢?我认为,是咱们下意识的利用了整数比较知足传递性的这一规则。可是计算机是没法下意识的使用传递性的,所以只能经过其它的方式来告诉计算机,有一些比较是没必要要的。因此,也就有了相对插入排序,选择排序更加高效的排序算法,好比归并排序,快速排序等,将n2的算法加速到了nlogn。或者是利用了问题的特色,采起了更加独特的解决方案,好比基数排序等。
扯远了一点,回到正题。如今咱们没有利用到的就是全序关系中传递性这一规则。如何利用它呢,最简单的想法每每就是最实用的,咱们仍是选择排序,排序后对每对相邻元素进行检测不就间接利用了传递性这一规则嘛?因此,咱们先使用拓扑排序对图中的顶点进行排序。排序后,对每对相邻顶点进行检测,看看是否存在前后关系,若是每对相邻顶点都存在着一致的前后关系(在有向图中,这种前后关系以有向边的形式体现,即查看相邻顶点对之间是否存在有向边)。那么就能够肯定该图中存在哈密顿路径了,反之则不存在。