【算法】这个课程表大不简单——拓扑排序

 

引言

 
>_< 现在需要为学生排好一张课表(课程的学习顺序)


可事情没有这么简单:
 

课程 前驱课程
课程0
课程1 课程0、课程4
课程2
课程3 课程0
课程4
课程5 课程3
课程6 课程3

 

不妨画成一张(Graph)试试看?
【算法】这个课程表大不简单——拓扑排序_第1张图片

我们意识到,这是一个有向图
 
我们需要做的就是找到一个序列:这个序列包含全部的节点,且满足有向图的前驱位置关系
 
—— 这便是【拓扑排序

 
 
 

定义

  对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。(百度 · 百科)

 
 
 

实现

拓扑排序有两个典型的作用:

  1. 检测这个课程表是否合法(判断有向图是否有环)
  2. 排出课程顺序(生成一个拓扑序列)

 
下面,用两个例题展示这两个典型作用的代码实现。
并且,每个例题将用两种思路实现(DFS / BFS),在最后,还将比较DFS/BFS的异同。

 
 
作用1 —— 判断有向图是否有环

例题1:
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]

给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

【DFS实现】
	思路: 类似染色法,在DFS过程中进行染色,如果出现闭环立即判错
	flag[]数组类似draw[]数组:
     		*  0   表示未被访问过
     		*  1   表示在当前的DFS路径中正在被访问(再次指向该处,说明出现闭环)
     		* -1   一次DFS结束,该点"绝对安全",即从该点开始DFS不可能出现闭环


class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> list = new ArrayList<>();
        int[] flag = new int[numCourses];

        // 根据数组生成邻接表(小插曲)
        for(int i = 0; i < numCourses; i++){
            list.add(new ArrayList<>());
        }
        for(int[] arr : prerequisites){
            list.get(arr[1]).add(arr[0]);
        }

		// DFS
        for(int i = 0; i < numCourses; i++){
            if(!DFS(list, flag, i)){
                return false;
            }
        }

        return true;

    }
	
	// DFS核心代码(★☆★)
    private boolean DFS(List<List<Integer>> list, int[] flag, int node){
        if(flag[node] == 1){                    // 这条DFS路径出现了闭环,直接判错
            return false;
        }
        if(flag[node] == -1){                   // 这个节点已经"绝对安全",不需要再次从该点开始DFS
            return true;
        }
        flag[node] = 1;                         // node点在这条正在进行的DFS路径中
        for(int index : list.get(node)){
            if(!DFS(list, flag, index)){
                return false;
            }
        }
        flag[node] = -1;                        // 从node点开始的DFS彻底结束,该点"绝对安全"
        return true;
    }
}
【BFS实现】
     思路: 不使用flag[]数组,而是使用degree[]入度数组
      		1.在生成邻接表的同时生成入度数组
      		2.入度为0的节点先入队
      		3.开始BFS,每次出队一个节点,该节点的所有邻接节点(它指向的节点)入度减一
      		4.如果入度减一后变为0,则该节点入队
      		5.每次进行入队操作时,便计数+1,如果最后入队的次数为总节点数,说明所有节点均遍历一次(被学习过),返回true

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> list = new ArrayList<>();
        int[] degree = new int[numCourses];
        int count = 0;

        // 根据数组生成邻接表
        for(int i = 0; i < numCourses; i++){
            list.add(new ArrayList<>());
        }
        for(int[] arr : prerequisites){
            list.get(arr[1]).add(arr[0]);
            degree[arr[0]]++;
        }

		// BFS核心代码(★☆★)
        Queue<Integer> queue = new LinkedList<>();
        for(int i = 0; i < numCourses; i++){
            if(degree[i] == 0){
                queue.offer(i);
                count++;
            }
        }
        while(!queue.isEmpty()){
            int node = queue.poll();
            for(int index : list.get(node)){
                degree[index]--;
                if(degree[index] == 0){
                    queue.offer(index);
                    count++;
                }
            }
        }
        
        return count == numCourses;

    }
}

 

 
作用2—— 生成一个拓扑序列

例题2:
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]

给定课程总量以及它们的先决条件,请你给出一个合理的课程学习顺序(任意给出一个就行)?

【DFS实现】---- 由上面的DFS代码转化而来
	1.上面的DFS代码中,"是否有环"是直接作为返回值返回的。而这里我们需要的是序列,并不需要这个返回值,"是否有环"直接用一个成员变量记录就行了
	2.每次判定出一个节点"绝对安全"时,就可以入栈。
	

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        List<List<Integer>> list = new ArrayList<>();
        int[] flags = new int[numCourses];

        // (...)生成邻接表过程略

        for(int i = 0; i < numCourses; i++){
            DFS(list, flags, i);
        }

		// 将存储栈转化为最终的拓扑序列
        if(flag){
            int i = 0;
            int res[] = new int[numCourses];
            while (!stack.isEmpty()){
                res[i++] = stack.pop();
            }
            return res;
        }else{
            return new int[0];
        }

    }

    private boolean flag = true;						// 判断是否出现闭环  
    Stack<Integer> stack = new Stack<>();				// 用栈记录拓扑序列
    
    private void DFS(List<List<Integer>> list, int[] flags, int node){
        if(flags[node] == 1){
            flag = false;
            return;
        }
        if(flags[node] == -1){
            return;
        }
        flags[node] = 1;
        for(int index : list.get(node)){
            DFS(list, flags, index);
        }
        flags[node] = -1;
        stack.put(node);
    }
}
【BFS实现】---- 由上面的BFS代码转化而来
	1.这个转化十分简单: 每次有节点入队时,就将它加到记录队列中存储,最后再把存储队列转化为拓扑序列
	2.如果队列的长度等于节点的总个数,则返回序列;否则说明该课表不合法,返回new int[0]


class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        List<List<Integer>> list = new ArrayList<>();
        int[] degree = new int[numCourses];

        int[] res = new int[numCourses];
        int resIndex = 0;

        // 根据数组生成邻接表
        for (int i = 0; i < numCourses; i++) {
            list.add(new ArrayList<>());
        }
        for (int[] arr : prerequisites) {
            list.get(arr[1]).add(arr[0]);
            degree[arr[0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (degree[i] == 0) {
                queue.offer(i);
                res[resIndex++] = i;
            }
        }
        while (!queue.isEmpty()) {
            int node = queue.poll();
            for (int index : list.get(node)) {
                degree[index]--;
                if (degree[index] == 0) {
                    queue.offer(index);
                    res[resIndex++] = index;
                }
            }
        }

        return resIndex == numCourses ? res : new int[0];
    }
}

 

 
 

总结

❶ 我们发现,DFS和BFS是两种不同的思路——【DFS+染色数组VSBFS+入度数组

❷【生成一个拓扑序列】的代码实现直接在【有向图判环】的代码基础上修改就行了

❸ 生成拓扑序列时,我们应当意识到DFS和BFS的一个重要区别:

  1. DFS是一个逆向生成序列的过程。 比如1 → 3 → 5,在DFS时,从1开始递归,但1最后完成递归。我们会依次得到5,3,1,因此这个记录序列的数据结构我们使用Stack
  2. BFS是一个正向生成序列的过程。 比如1 → 3 → 5,在BFS时,入度为0的1先入队,之后3,5依次入度减为0而入队。我们会依次得到1,3,5,因此这个记录序列的数据结构我们使用队列Queue

 
 

 

 

 

 

 

 

 

 

 
☑ 部分题目来源 :

【Leetcode Q207 】课程表Ⅰ

【Leetcode Q210 】课程表Ⅱ

 

End ♬

by a Lolicon ✪

你可能感兴趣的:(算法)