LeetCode 207:课程表(图论,利用拓扑排序判断是否有环)
题目
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同
思路
利用拓扑排序来判断是否成环,如果成环的话,拓扑排序返回的节点列表的数量会少于图的节点树。所以先构建图,然后拓扑排序返回所有不成环的节点列表。
代码
第一版代码
class Solution {
public class Graph{
public HashMap<Integer,Node> nodes;
public HashSet<Edge> edges;
public Graph(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
public class Node{
public int value;
public int in;
public int out;
public ArrayList<Node> nexts;
public ArrayList<Edge> edges;
public Node(int value){
this.value = value;
in=0;
out=0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
public class Edge{
public Node from;
public Node to;
public Edge(Node from, Node to){
this.from = from;
this.to = to;
}
}
public HashSet<Integer> top(Graph graph){
HashMap<Node,Integer> inMap = new HashMap<>();//一个节点对应的剩余的入度
Queue<Node> zeroInQueue = new LinkedList<>();//存放着入度为0的节点
for(Node node:graph.nodes.values()){
inMap.put(node, node.in);
if(node.in==0) zeroInQueue.add(node);
}
HashSet<Integer> result = new HashSet<>();//存放结果
while(!zeroInQueue.isEmpty()){
Node cur = zeroInQueue.poll();
result.add(cur.value);
for(Node next:cur.nexts){
int newin = inMap.get(next)-1;
inMap.put(next,newin);
if(newin==0) zeroInQueue.add(next);
}
}
return result;
}
public Graph createGraph(int[][] prerequisites){
Graph graph = new Graph();
for(int i=0;i<prerequisites.length;i++){
int fromVal = prerequisites[i][1];
int toVal = prerequisites[i][0];
if(!graph.nodes.containsKey(fromVal)) graph.nodes.put(fromVal, new Node(fromVal));
if(!graph.nodes.containsKey(toVal)) graph.nodes.put(toVal, new Node(toVal));
Node fromNode = graph.nodes.get(fromVal);
Node toNode = graph.nodes.get(toVal);
Edge edge = new Edge(fromNode, toNode);
fromNode.out++;
toNode.in++;
graph.edges.add(edge);
fromNode.nexts.add(toNode);
fromNode.edges.add(edge);
}
return graph;
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
if(prerequisites.length==0) return true;
Graph graph = createGraph(prerequisites);
HashSet<Integer> nodes = top(graph);
// myPrint(nodes);
// int nums = nodes.size();
// if(numCourses==nums||numCourses-1==nums) return true;
HashSet<Integer> targets = new HashSet<>();
for(int i=0;i<prerequisites.length;i++){
int preVal = prerequisites[i][0];
int laterVal = prerequisites[i][1];
targets.add(preVal);
targets.add(laterVal);
}
if(targets.equals(nodes)) return true;
return false;
}
public void myPrint(HashSet<Node> nodes){
for(Node node: nodes){
System.out.println(node.value);
}
}
}
执行用时分布,17ms,击败10.60%使用 Java 的用户。
需要优化
第二版代码
感觉第一版代码得出targets列表是多余的,因为只要判断图的节点的数量和拓扑排序的不成环的节点的数量是否一致。所以top排序返回数量即可。此外,删除节点中不必要的属性。
class Solution {
public class Graph{
public HashMap<Integer,Node> nodes;
public HashSet<Edge> edges;
public Graph(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
public class Node{
public int value;
public int in;
public ArrayList<Node> nexts;
public Node(int value){
this.value = value;
in=0;
nexts = new ArrayList<>();
}
}
public class Edge{
public Node from;
public Node to;
public Edge(Node from, Node to){
this.from = from;
this.to = to;
}
}
public int top(Graph graph){
HashMap<Node,Integer> inMap = new HashMap<>();//一个节点对应的剩余的入度
Queue<Node> zeroInQueue = new LinkedList<>();//存放着入度为0的节点
for(Node node:graph.nodes.values()){
inMap.put(node, node.in);
if(node.in==0) zeroInQueue.add(node);
}
int ans=0;
while(!zeroInQueue.isEmpty()){
Node cur = zeroInQueue.poll();
ans++;
for(Node next:cur.nexts){
int newin = inMap.get(next)-1;
inMap.put(next,newin);
if(newin==0) zeroInQueue.add(next);
}
}
return ans;
}
public Graph createGraph(int[][] prerequisites){
Graph graph = new Graph();
for(int i=0;i<prerequisites.length;i++){
int fromVal = prerequisites[i][1];
int toVal = prerequisites[i][0];
if(!graph.nodes.containsKey(fromVal)) graph.nodes.put(fromVal, new Node(fromVal));
if(!graph.nodes.containsKey(toVal)) graph.nodes.put(toVal, new Node(toVal));
Node fromNode = graph.nodes.get(fromVal);
Node toNode = graph.nodes.get(toVal);
Edge edge = new Edge(fromNode, toNode);
toNode.in++;
graph.edges.add(edge);
fromNode.nexts.add(toNode);
}
return graph;
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
if(prerequisites.length==0) return true;
Graph graph = createGraph(prerequisites);
int realnum = top(graph);
int num = graph.nodes.size();
if(realnum==num) return true;
return false;
}
}
13ms,击败15.27%使用 Java 的用户。
可以看到优化的不多,毕竟自己是套的所有图基本都可以用的板子,想要优化的话就得改动这个存储数据结构,不方便自己记忆板子。但读者可以自行选择,我有提供其他数据结构实现的代码,在第三版。
第三版代码
public class Solution {
public static boolean canFinish(int numCourses, int[][] prerequisites) {
// 构建图
ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
int[] inDegree = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int[] prerequisite : prerequisites) {
int from = prerequisite[1];
int to = prerequisite[0];
graph.get(from).add(to);
inDegree[to]++;
}
// 使用拓扑排序判断是否能够完成所有课程
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
int count = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
count++;
for (int neighbor : graph.get(course)) {
inDegree[neighbor]--;
if (inDegree[neighbor] == 0) {
queue.offer(neighbor);
}
}
}
return count == numCourses;
}
}
5ms,击败52.30%使用 Java 的用户。