leetcode--207.Course Schedule

问题

There are a total of n courses you have to take, labeled from 0 to n-1.

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?

Example 1:

Input: 2, [[1,0]] 
Output: true
Explanation: There are a total of 2 courses to take. 
             To take course 1 you should have finished course 0. So it is possible.

Example 2:

Input: 2, [[1,0],[0,1]]
Output: false
Explanation: 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:

  1. The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
  2. You may assume that there are no duplicate edges in the input prerequisites.

思路与解法

这道题目的要求是让我们判断是否可以找到一个课程的顺序,满足所有先修课程都在后续课程之前学习。

方法一:

首先,最容易想到的方法便是找到一个所有课程的拓扑序列,如果找不到,则说明输入数据并不满足题目要求,return false即可。但是该题目并不需要找到完成的拓扑序列,我们只需要判断输入的“图”是否存在环即可。此时,我们可以利用DFS递归实现:

代码实现

此算法我用go语言实现:

  • 对于无向图,判断是否有环,我们可以从一个节点出发,并以后继节点不断递归,对访问过的点进行标记,如果找到与一个已经标记过的点,则表明该图中存在环。如果不是连通图,则可以在最外层嵌套一层循环,以每个未访问过的节点为起点进行递归搜索。
  • 对于有向图,不管是否连通,我们都应该在最外层嵌套一层循环,以每个未访问的节点为起点进行递归搜索:因为有向边限制了从一个节点出发并不一定能够遍历整个图。另一方面,在有向图的遍历中,每个节点有三个状态(无向图只需要两个):未访问的节点,标记为0;在一次递归过程中已访问的节点,标记为-1;完全访问过的节点标记为1
    之所以存在状态-1,也是因为有向边限制从一个节点出发并不一定能够遍历整个图。所以,在之后再次遍历该有向图的时候,可以不受之前是否访问过的干扰;状态1则表明了某个节点已经访问过,减少最外层循环的次数。

具体实现如下:

// 递归判断是否存在环
func findCycle(s int, visited []int, graph map[int][]int) bool {
    visited[s] = -1									// 将再一次递归过程中访问过的点标记为-1
    for _, edge := range graph[s] {					// 遍历其后继节点
        if visited[edge] == -1 {					// 存在环
            return true
        } else if visited[edge] == 0 {				// 从为访问过的点出发继续递归
            if findCycle(edge, visited, graph) {
                return true
            }
        }
    }
    visited[s] = 1									// 在回溯过程中,将此次递归中的所有访问过的节点标记为1,因为这些节点并不构成环,所以外层循环再次遍历这些节点并没有意义
    return false
}

func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make(map[int][]int)
    visited := make([]int, numCourses)
	// 构造图,pair[0] -> pair[1]存在一条边,表示pair[1]必须在pair[0]之前修习
    for _, pair := range prerequisites {
        graph[pair[1]] = append(graph[pair[1]], pair[0])
    }
    for i:=0; i<numCourses; i++ {
        if visited[i] == 0 {
            if findCycle(i, visited, graph) {
                return false
            }
        }
    }
    return true
}

遇到的问题

最初findCycle写法如下:

func findCycle(s int, visited []int, graph map[int][]int) bool {
    visited[s] = -1									// 将再一次递归过程中访问过的点标记为-1
    for _, edge := range graph[s] {					// 遍历其后继节点
        if visited[edge] == -1 {					// 存在环
            return true
        } else if visited[edge] == 0 {				// 从为访问过的点出发继续递归
            return findCycle(edge, visited, graph)
        }
    }
    visited[s] = 1									// 在回溯过程中,将此次递归中的所有访问过的节点标记为1,因为这些节点并不构成环,所以外层循环再次遍历这些节点并没有意义
    return false
}

由于对后续节点的判断中,return findCycle(edge, visited, graph),导致回溯过程中visited[s]无法置为1,所以结果一直不对。

方法二:

我们可以采用剥洋葱的思想,将先修课程修完,然后从后续的课程中,寻找“先修课程”,直到所有的课程都修完一遍时,return true;否则,如果有某些课程始终学习不到,return fasle

代码实现

func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make(map[int][]int)
    in_degree := make([]int, numCourses)
    queue := make([]int, 0)
    // count用来计数修习过的课程
    count := 0
    // 构造图,pair[0] -> pair[1]存在一条边,表示pair[1]必须在pair[0]之前修习
    for _, pair := range prerequisites {
        graph[pair[1]] = append(graph[pair[1]], pair[0])
        in_degree[pair[0]]++								//统计pair[0]的入度
    }
	// 将所有入度为0的点加入到队列中
    for i:=0;i<numCourses;i++ {
        if in_degree[i] == 0 {
            queue = append(queue, i)
            count++
        }
    }
	// 当queue为0时结束循环
    for len(queue)!=0 {
        head := queue[0]
        queue = queue[1:]
        // 遍历head的后继节点,并将其入度减少1;若入度为0,则加入到队列,count加1
        for _, edge := range graph[head] {
            in_degree[edge]--
            if in_degree[edge] == 0 {
                queue = append(queue, edge)
                count++
            }
        }
    }
    // 若count==numCourses,则表明所有课程已经修习完毕
    return count==numCourses
}

如果想要获得拓扑排列,则可以将上述代码中count变量更改为schedule []int,并在count++的位置将节点添加到此切片中。
两种方法在时间复杂度上并没有什么不同,但是我更推荐第二种写法, 更易实现。

猜你喜欢

转载自blog.csdn.net/liuyh73/article/details/83049165