think of a directed graph 0->1->2->3->4, isn't your solution's time complexity O(E * N**2)...you will start the same loop for 1, 2, 3, 4 after doing it for 0
The .remove(crs) was so confusing but I finally understood it. Simplest explanation: if we exit the for-loop inside dfs, we know that crs is a good node without cycles. However, if it remained in the visited set, we could trip the first if-clause in the dfs function if we revisit it and return False. That's what we don't want to do, because we just calculated that crs has no cycles. So we remove it from visited so that other paths can successfully revisit it. Basically we can visit the node twice without it being a cycle due to it terminating multiple paths.
Actually you can see this trick in many graph, binary tree or other problems using back tracking. Because the visit set is only used to contain the current visit path. So whenever you exit the sub-function you create on this level, you have to pop the info you passed in before.
Pretty smart! He removes all pre-courses at once after iterate through one adjacency list. In the drawing solution, he removes the pre-courses one by one. To align with the codes, the list can only be "cleaned out" if all pre-courses returns true. Actually, I was a little bit confused when I first saw the codes.
And necessary for time complexity to be O(num_nodes). If we didn't do it, the last for loop would have us visiting nodes as many times as it required by other courses, making the overall complexity O(n^2).
Your channel is soo helpful! If I get stuck on a LC question I always search for your channel! Helped me pass OAs for several companies. Thank you so much.
If you move the line 13 `if preMap[crs] == []` before the line 11 `if` check, then you don't need the `visitedSet.remove(crs)` in line 19, because you will never traverse the visited path that way. Thanks for great explanation.
Here is a simpler solution. Instead of removing and adding from a map, we simply use a status 0 -> unvisited, 1-> visiting, and 2->visited for tracking the current node status in a DFS path. If the node is revisited before completing all neighbors, it would have status 1 which implies there is a cycle in the graph. Same logic as Neetcode's though. class Solution: def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: graph = [[] for _ in range(numCourses)] visit = [0]*numCourses for u,v in prerequisites: graph[u].append(v) def dfs(node): if visit[node] == 1: return False if visit[node] == 2: return True visit[node] = 1 for neighbor in graph[node]: if not dfs(neighbor): return False visit[node] = 2 return True for i in range(numCourses): if visit[i] == 0: if not dfs(i): return False return True
this is a clever solution. not something i would ever come up with haha, i had a similar idea, but it kind of just broke down during the dfs step. i had a lot of trouble trying to figure out how to detect cycles in a directed graph...in the end when i was looking in the discussion i saw that you could just do a topological sort so i felt silly after that haha. gotta work on graph problems more :-)
To simplify this problem This is based on finding if the directed graph has a cycle. If yes then return false(cannot complete all courses) else return true.
You draw edge incorrectly. If it's [0, 1] meaning you first have to take course 1 before 0, edge is gonna be 1->0, not 0->1, because first we need to take 1 and only then we will have access to the 0.
I have a hard time envisioning visited.remove(crs). I cannot connect this to the "Drawing Solution" earlier. I can see when preMap[crs] is set to 0 @7:44, but I cannot see any part where visisted.remove(crs) corresponds to. I understand to detect a cycle, we need to visisted.add(crs), But I cannot see where visited.remove(crs) fits. Can someone help?
Lets say course 3 is dependent on 2 and 2 is on 1, while traversing for 3 you make dfs(2) which in turn is dfs(1) but dfs(1) does not have pre-req so u mark it as [] (initially) and similarly you need to mark dfs(2) to [] which is done using set.remove() and map.append([] ) for key =2 .
Same. What helped me was thinking about it as setting the course node to a "leaf node". If you notice the leaf nodes (courses with no prerequisites) are never added to the visited set. So setting it to [] and removing it from the set does that.
I think it makes more sense if you replace visitSet.remove() with visitSet.clear(). visitSet.clear() also works for our purpose, which is basically to give us a new visitSet for each course so we don't get false positives from earlier runs.
When the graph is not fully connected. 1->2->3, 4->3. If you should not remove 3, 4->3 would be false because 3 is already in the set. However, you can also choose to change the order of the two ifs in the start of the bfs to avoid removing.
Thanks for the very clear explanation. I have a suggestion for direction of arrows that may be less confusing. For example, prerequisites = [[1,0]] means that if we have to take course 0 before course 1. So my graph would be pictured like: 0------->1 instead of 1------>0.
I was really confused about the direction of the edges. Intuitively, I would think precourse -> course, but you have the arrows going backwards from course -> precourse. By switching the arrows around to: precourse -> course, and having your adjacency list as: { precourse: [ course ] } instead of: { course: [ precourse ] }, your DFS solution still works. The benefit to doing it this way is that you can use the same adjacency list pattern for a BFS topological sort approach, which needs access to the neighbors of nodes with zero in-degrees.
I implemented DFS via a stack. I also tried a similar approach with BFS using a deque queue but it was a lot slower. class Solution: def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: graph = defaultdict(list) for prereq in prerequisites: graph[prereq[0]].append(prereq[1]) for i in range(numCourses): if i in graph: stack = graph[i] seen = set() while stack: node = stack.pop() if node == i: return False if node not in seen: for j in graph[node]: stack.append(j) seen.add(node) return True
I have taken Neetcode's Course Schedule 2 idea and implemented this on those lines: Personally I found that idea more intuitive and easier to follow. class Solution { HashMap prereq = new HashMap(); // hashset to mark the visited elements in a path HashSet completed = new HashSet(); // we use a hashset for current path as it enables quicker lookup // we could use the output to see if the node is already a part of output, // but the lookup on a list is O(n) HashSet currPath = new HashSet(); public boolean canFinish(int numCourses, int[][] prerequisites) { // base case if (numCourses
Great explanation, I was doing the adj list pre->course and confused myself in the coding step. Thanks for the video, smart ideas. I definitely was not thinking of the fully connected graph case
Thank you so much for all of these videos. Very well explained and also well put together and displayed. Really fantastic material, it's been absolutely invaluable in helping me to learn and improve my skills.
Intuitively I was thinking of this solution and gave myself 30 mins to solve it, I got to the part of DFS but then confused myself because I was like "I feel like I need DFS here but where should I start it, how should I call it" and then the time was up xD, I'm happy I almost came up with it alone though. Thanks for the video, it clarified what I was having trouble with.
The way you set up the edges is very unintuitive. I think of it as if 0 is a prerequisite for 1 then 0 -> 1. So you'd traverse the graph in the order you would take the classes. Otherwise good video!
Java version of this solution: class Solution { Map preMap = new HashMap(); Set visitSet = new HashSet(); public boolean canFinish(int numCourses, int[][] prerequisites) { for (int i = 0; i < numCourses; i++) { preMap.put(i, new ArrayList()); } for (int[] p : prerequisites) { List neighbors = preMap.get(p[0]); neighbors.add(p[1]); preMap.put(p[0], neighbors); } for (int i = 0; i < numCourses; i++) { if (!dfs(i, preMap, visitSet)) return false; } return true; } public boolean dfs(int course, Map preMap, Set visitSet) { if (visitSet.contains(course)) return false; if (preMap.get(course).size() == 0) return true; visitSet.add(course); for (int pre : preMap.get(course)) { if (!dfs(pre, preMap, visitSet)) return false; } visitSet.remove(course); preMap.put(course, new ArrayList()); return true; } }
Thanks for your explanation! it is clean and easy to percept. I really appreciate your coding style. Just a heads up that the if perMap[crs] == [] return True can be omitted since if we have an empty array for prerequisites for crs, the for loop afterwards will just end and return True at the end!
Given N = number of courses, P = prerequisites; TC: O(N + P), because we are visiting each "node" once, and each "edge" once as well. SC: O(N+P), as our hashmap is of size N + P, and the recursive call stack + visited set are of size N.
When I did this exercise for the first time, I actually created a whole complete graph data structure from scratch. Then created 2 visited maps to resolve the circular issue. So much memory required
Hey man, thanks a lot for your description. You probably have the best explanation on this this problem compared to other RU-vidrs. There’s a girl that’s pretty good too her names jenny
in the example case [1,0], why is it out reach arrow 1-> 0 ? I thought in order to get 1, you need to get 0 first, so, it's 0->1 ? am i right? it's a little anti-intuitive ?
I feel using Kahn's algorithm for detecting cycles in the directed graph is the simplest solution for this, even though logically not 100% right as we use indegree in Kahn's algorithm, as soon as I see cycle and undirected graph I solved it with this method. Then I got to know we are supposed to deal with outdegree to deal with independent nodes in this case! Though Kahn's algo works !
@NeetCode Thank you so much for your work! I'am going through collection of your solutions and litterally feeling smarter) I don't really understand one thing in this problem - why do they provide numCourses at all? I mean, when given number is less then total number of courses provided in prerequisites - like (1, [[0, 1]]) the algorithm fails. And of course you dont realy need this number to create preMap (you could use defaultdict, or check if key exists on string 14 before comparing to empty list). Iteration through length of preMap also will work when running dfs.
This is very clear explanation, but I met Time Limit Exceeded problem, so I made the follow changes to met the requirements: class Solution(object): def canFinish(self, numCourses, prerequisites): """ :type numCourses: int :type prerequisites: List[List[int]] :rtype: bool """ adjacency_list = [[] for _ in range(numCourses)] for crs, prec in prerequisites: adjacency_list[crs].append(prec) visited = [0] * numCourses def dfs(crs): if visited[crs] == 1: return False if visited[crs] == 2: return True visited[crs] = 1 for prec in adjacency_list[crs]: if not dfs(prec): return False visited[crs] = 2 return True for crs in range(numCourses): if not dfs(crs): return False return True
Explanation on WHY/HOW cycle was detected(The crux of the problem) -As we perform dfs, we add node to 'visited' set if it does not exist. -Once we have exhausted all its neighbors/prerequisites AND return back to it from call stack, we pop it from call stack and we remove from 'visited' set. -A cycle is detected when the node we are popping off of call stack still exists in 'visited' set. BUT WHY?? In the last example he provides @ 10:50, while we have/are visiting the last neighbor/prepreq of node 0, unfortunately we have not returned back to it due to its order in call stack. Hence a cycle was detected before we we're able to remove node 0 from call stack.
Simpler version (imo): class Solution: def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: pre = collections.defaultdict(list) for e in prerequisites: pre[e[0]].append(e[1]) visited = set() completed = set() def dfs(node): if node in visited and node not in completed: return False if node in visited: return True visited.add(node) res = all(dfs(child) for child in pre[node]) completed.add(node) return res return all(dfs(n) for n in range(numCourses))
has anyone tried to do this with the tortoise and hare algorithm? seems like just converting the input to a linked list and looking for infinite loops should work...
I have to say if you didn't realize this was a graph problem or to just think about it literally initially about courses and prerequisites you can get pretty stuck with where to go. :(
we dont need to travese 4 from 1 if we are keeping track of the visited nodes. cross edge iterations can be eliminated to increase the speed of the algorithm, right?
Is it just me or does it seem like Leetcode has added a testcase for this problem that causes it to exceed time limit, even with this great implementation? Cannot get a pass for this problem...
Could another solution be just detecting if the graph contains a cycle or not? You would use 2 pointers and make one move by one position and the other by 2. If the nodes encounter each other then you will have a cycle therefore you can’t complete the courses. Could someone tell me if there is a flaw in this solution? Thanks :)
Why can't your map be an integer as the key and an integer as the value? Why does the value have to be a list? I thought it would be okay to declare the map as int, int since you have one prereq for each course
Am I the only one who is confused why numCourses is even part of this problem? All the test cases in the leetcode are either cyclic (output: false) or non cyclic (output: true), so this problem can be solved with Kahn's algorithm.
how come you don't use a set instead of a list for the visitedSet? One would need to use the nonlocal keyword but the lookup times are much quicker. Couldn't there be an edge case where your last node in the dfs loops back to the second to last and then you are searching the whole array. This could potentially happen a couple times no? Thanks for any clarity you can provide!
Not able to do the BFS solution of this problem, got stuck in thinking how it will be approached, tried with one problem getting TLE in 29th test case. Can anyone help!
Quick question: I was trying to solve this using the Ailen Dictionary method you also made a video of. I can't get it to pass all test cases. May I ask if the method for the alien dictionary can be applied to this one?
I understand the preMap[crs] == [] purpose. But, does that mean when we find out that a course's prerequisite is [], we directly conclude that this course can be completed and return true for that course, without checking if the other prerequisites of that same course can all be completed as well? In other words, do we consider that a course can be completed if at least one of its prerequisites can be so?
you explain so good but theres a problem with me . This is the first time I tried to figure out what's exactly happening in graph algorithms specially DFS but seems like I should start from scratch cuz I couldn't get it at all . here comes the problem That I don't even know how to start and where to learn
Just Use the Topological ordering to solve the problem, i think he is misunderstood the problem. I used kahn's algorithm to use this problem from collections import defaultdict """Try to solve the problem using the Topological Sorting""" class Kahn: def run(self, edges, V): indegree_arr = [0 for _ in range(V)] q = [] topo_arr = [] adj = defaultdict(list) for i, edge in enumerate(edges): u, v = edge indegree_arr[v] += 1 adj[u].append(v) for i, val in enumerate(indegree_arr): if val == 0: q.append(i) while q: node = q.pop(0) topo_arr.append(node) nb = adj[node] for i, n in enumerate(nb): indegree_arr[n] -= 1 if indegree_arr[n] == 0: q.append(n) return topo_arr class Solution: def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: k = Kahn() topo_arr = k.run(prerequisites, numCourses) if not topo_arr or len(topo_arr) != numCourses: return False return True
I don’t really get what If not dfs(pre): Return false Is doing. I understand it’s checking if the statement is false but what is it doing specifically?
BFS , More intuitive solution class Solution(object): def canFinish(self, numCourses, prerequisites): """ :type numCourses: int :type prerequisites: List[List[int]] :rtype: bool """ graph = defaultdict(list) in_degree = [0] * numCourses for course, prereq in prerequisites: graph[prereq].append(course) in_degree[course] += 1 # Step 2: Initialize the queue with courses having in-degree of 0 queue = deque([i for i in range(numCourses) if in_degree[i] == 0]) # Step 3: Process the courses using BFS processed_courses = 0 while queue: current_course = queue.popleft() processed_courses += 1 # Reduce the in-degree of neighboring courses for neighbor in graph[current_course]: in_degree[neighbor] -= 1 # If in-degree becomes 0, add it to the queue if in_degree[neighbor] == 0: queue.append(neighbor) # Step 4: Check if all courses are processed return processed_courses == numCourses
I am not understanding why we set preMap[crs] = [], in the drawing solution we just removed one by one after checking. Why are we just removing all the prereqs after one is checked?
[[0,1],[1,2],[2,3],[3,1],[1,4]] for this test case answer is true or false, I donot get it. If we start from 4. It is possible to cover all nodes, So answer should be true, but it cames false. Can anyone have explanation for this.
its been almost three months i am doing leetcode but still cannot come up with my own solution, can anybody tell me exactly what is thats wrong i am doing? :(