【Leetcode】207. Course Schedule

题目地址:

https://leetcode.com/problems/course-schedule/

给定一个正整数代表需要修的课程数量,再给定修各个课程的先后次序关系(是一个二维数组,每个位置的 ( a , b ) (a,b) (a,b)代表 b b b a a a的前置课程)。问是否存在一种上课顺序能上完所有的课。

用图论建模,可以把每个课程想象成图中的顶点,如果课程 v 1 v_1 v1 v 2 v_2 v2的先修课程,就从 v 1 v_1 v1 v 2 v_2 v2之间加一条边 v 1 → v 2 v_1\to v_2 v1v2。这样问题就转化为这个有向图能否被拓扑排序。

法1:BFS。先以邻接表方式建图,然后计算一下每个点的入度,用一个哈希表进行存储。接下来进行BFS,先将入度为 0 0 0的点都入队,然后从队列中取数访问,每访问一个数,就将其出发的边删掉(实现上就是将其指向的点的入度减去 1 1 1),如果某个点的入度减成了 0 0 0,那就将其入队,并从哈希表中将这个点删掉。如此直到队列为空。如果此时仍然有某个点的入度非 0 0 0,说明存在环,返回false。否则返回true。代码如下:

import java.util.*;

public class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        if (prerequisites == null || prerequisites.length == 0 || prerequisites[0].length == 0) {
            return true;
        }
        
        // 用邻接表建图,key代表点,value代表这个点所指向的顶点;
        // 这里建图的时候如果某个点没有邻边,keySet里不会有这个点
        Map<Integer, List<Integer>> graph = buildGraph(prerequisites);
        // 存储每个点的入度
        int[] indegrees = new int[numCourses];
        for (int[] prerequisite : prerequisites) {
            indegrees[prerequisite[0]]++;
        }
        
        Queue<Integer> queue = new LinkedList<>();
        // 将indegree里没有的点入队;indegree里没有的点入度是0
        for (int i = 0; i < numCourses; i++) {
            if (indegrees[i] == 0)) {
                queue.offer(i);
            }
        }
        
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            // 先要判断一下graph里有没有cur这个点;如果没有的话,说明这个点没有邻居;
            // 但这句话仍然要加,否则会出现NPE
            if (graph.containsKey(cur)) {
            	// 接着开始遍历其邻居
                for (int next : graph.get(cur)) {
                    indegrees[next]--;
                    if (indegrees[next] == 0) {
                        queue.offer(next);
                    }
                }
            }
        }
        
        for (int indegree : indegrees) {
        	// 如果发现还有入度非零的顶点,说明有环,返回false
            if (indegree != 0) {
                return false;
            }
        }
        
        // 所有点入度都变成0了,返回true
        return true;
    }
    
    private Map<Integer, List<Integer>> buildGraph(int[][] prerequisites) {
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int[] prerequisite : prerequisites) {
            map.putIfAbsent(prerequisite[1], new ArrayList<>());
            map.get(prerequisite[1]).add(prerequisite[0]);
        }
        
        return map;
    }
}

时空复杂度 O ( V + E ) O(V+E) O(V+E)

法2:DFS。还是以邻接表方式建图,同时用一个int数组visited记录每个点的访问状态(注意是int数组,不是boolean数组,后面会解释原因并举出反例)。其中,visited[i] = -1表示未访问过,visited[i] = 0表示正在访问,visited[i] = 1表示已经访问。“正在访问”的意思是,某次在调用DFS函数进行深搜,访问到某个节点的时候,本轮DFS之前访问的节点,这类节点是标记为 0 0 0。但本轮DFS结束后访问过的点就全都标记为 1 1 1了。接下来就开始从某个未访问过的顶点(也就是标记为 − 1 -1 1的顶点)开始DFS。一路访问下去的时候将顶点逐个标记为 0 0 0,并且每次只访问标记为 − 1 -1 1的顶点(意思是,标记为 1 1 1的也不需要访问了,因为之前某轮DFS访问过,已经check过无环了,所以不需要再来一遍)。如果某一次发现下一个要访问的顶点已经标记为 0 0 0了,说明有环(譬如说某次访问顺序是 v 0 → v 1 → . . . → v t → v s v_0\to v_1\to ...\to v_t\to v_s v0v1...vtvs,其中 0 ≤ s ≤ t − 1 0\le s\le t-1 0st1,那么环就是 v s → . . . → v t − 1 → v t → v s v_s\to ...\to v_{t-1}\to v_t\to v_s vs...vt1vtvs),否则一直访问到远端,然后回溯,回溯的时候逐个把访问过的点标记为 1 1 1,表示已访问。接下来再从下一个标记为 − 1 -1 1的顶点开始访问,执行同样的操作,直到整个图被访问完毕。如果发现某次访问找到了环,则直接返回false,否则返回true。代码如下:

import java.util.*;

public class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        if (prerequisites == null || prerequisites.length == 0 || prerequisites[0].length == 0) {
            return true;
        }
    
        Map<Integer, List<Integer>> graph = buildGraph(prerequisites);
        int[] visited = new int[numCourses];
        // 先将所有顶点标记为未访问
        Arrays.fill(visited, -1);
        for (int i = 0; i < numCourses; i++) {
        	// 每次只从未访问过的顶点开始DFS
            if (graph.containsKey(i) && visited[i] == -1) {
                if (dfs(i, graph, visited)) {
                    return false;
                }
            }
        }
        
        return true;
    }
    
    // 函数功能是从cur开始做DFS,返回值是本次DFS有没有发现环;有环则返回true,否则返回false
    private boolean dfs(int cur, Map<Integer, List<Integer>> graph, int[] visited) {
    	// 先标记为本轮DFS正在访问
        visited[cur] = 0;
        if (graph.containsKey(cur)) {
            for (int next : graph.get(cur)) {
            	// 如果下一个要访问的点中有被标记为0的,说明发现了环,直接返回true;
            	// 否则访问下一个未访问过的顶点,如果发现有环也直接返回true
                if (visited[next] == 0) {
                    return true;
                } else if (visited[next] == -1) {
                    if (dfs(next, graph, visited)) {
                        return true;
                    }
                }
            }
        }
        // 回溯之前把每个访问过的点都标记为1,下次DFS需要避开这些已经考察过的顶点;
        visited[cur] = 1;
        // 同时上面一直没返回true,说明本轮DFS未发现环,所以返回false
        return false;
    }
    
    private Map<Integer, List<Integer>> buildGraph(int[][] prerequisites) {
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int[] prerequisite : prerequisites) {
            map.putIfAbsent(prerequisite[1], new ArrayList<>());
            map.get(prerequisite[1]).add(prerequisite[0]);
        }
        
        return map;
    }
}

时空复杂度一样。

注解:
之所以要用int数组而不是boolean数组,原因是我们需要对每个点表达三种状态,即 − 1 -1 1表示从未访问, 0 0 0表示本轮DFS已经访问过(也就是上面说的“正在访问”的意思), 1 1 1表示之前几轮DFS已经访问过。如果只用两个状态表示的话,就会导致在访问某个点的时候,如果它的邻居已经访问过,我们并不能断定有环,因为有可能这个邻居是上几轮DFS访问过标记的,和本轮访问并没有关系。举个例子,如果有这样一个图,一共只有三个点两条边,边为 0 → 2 0\to 2 02 1 → 2 1\to 2 12。第一轮DFS将 0 0 0 2 2 2标记为已访问过,并且未发现环;接下来从 1 1 1开始DFS,发现 2 2 2被访问过了,但此时不能说明有环,因为 2 2 2是上一轮DFS访问的,并不是本轮。而用三个状态就能避免这个问题。

C++:

class Solution {
 public:
  bool canFinish(int n, vector<vector<int>>& pre) {
    int ind[n];
    fill(ind, ind + n, 0);
    unordered_map<int, vector<int>> g;
    for (auto &e : pre) {
      int a = e[0], b = e[1];
      g[b].push_back(a);
      ind[a]++;
    }
        
    queue<int> q;
    for (int i = 0; i < n; i++)
      if (!ind[i]) q.push(i);

    int cnt = 0;
    while (q.size()) {
      int t = q.front(); q.pop();
      cnt++;
      for (auto &x : g[t]) {
        ind[x]--;
        if (!ind[x]) q.push(x);
      }
    }

    return cnt == n;
  }
};

时空复杂度一样。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值