Skip to content

Commit 6a83cf8

Browse files
committed
feat(book/graph): add schedule exercise and solution
1 parent 802e9d3 commit 6a83cf8

11 files changed

+587
-8
lines changed

Diff for: book/D-interview-questions-solutions.asc

+191
Original file line numberDiff line numberDiff line change
@@ -655,3 +655,194 @@ We could also have used a Map and keep track of the indexes, but that's not nece
655655

656656
- Time: `O(n)`. We visit each letter once.
657657
- Space: `O(W)`, where `W` is the max length of non-repeating characters. The maximum size of the Set gives the space complexity. In the worst-case scenario, all letters are unique (`W = n`), so our space complexity would be `O(n)`. In the avg. case where there are one or more duplicates, it uses less space than `n`, because `W < n`.
658+
659+
660+
661+
662+
663+
664+
665+
:leveloffset: +1
666+
667+
=== Solutions for Graph Questions
668+
(((Interview Questions Solutions, Graph)))
669+
670+
:leveloffset: -1
671+
672+
673+
[#graph-q-course-schedule]
674+
include::content/part03/graph-search.asc[tag=graph-q-course-schedule]
675+
676+
Basically, we have to detect if the graph has a cycle or not.
677+
There are multiple ways to detect cycles on a graph using BFS and DFS.
678+
679+
One of the most straightforward ways to do it is using DFS one each course (node) and traverse their prerequisites (neighbors). If we start in a node, and then we see that node again, we found a cycle! (maybe)
680+
681+
A critical part of solving this exercise is coming up with good test cases. Let's examine these two:
682+
683+
[graphviz, course-schedule-examples, png]
684+
....
685+
digraph G {
686+
subgraph cluster_1 {
687+
a0 -> a1 -> a2
688+
a0 -> a2 [color=gray]
689+
label = "Example A"
690+
}
691+
692+
subgraph cluster_2 {
693+
b0 -> b1 -> b2 -> b3
694+
b3 -> b1 [color=red]
695+
label = "Example B";
696+
}
697+
}
698+
....
699+
700+
Let's say we are using a regular DFS, where we visit the nodes and keep track of visited nodes. If we test the example A, we can get to the course 2 (a2) in two ways. So, we can't blindly assume that "seen" nodes are because of a cycle. To solve this issue, we can keep track of the parent.
701+
702+
For example B, if we start in course 0 (b0), we can find a cycle. However, the cycle does not involve course 0 (parent). When we visit course 1 (b1) and mark it as the parent, we will see that reach to course 1 (b1) again. Then, we found a cycle!
703+
704+
[source, javascript]
705+
----
706+
include::interview-questions/course-schedule.js[tags=brute1]
707+
----
708+
709+
We built the graph on the fly as an adjacency list (Map + Arrays).
710+
Then we visited each node, checking if there it has cycles. If none has cyles, then we return true.
711+
712+
The cycle check uses DFS. We keep track of seen nodes and also who the parent is. If we get to the parent more than once, we have a cycle like examples A and B.
713+
714+
What's the time complexity?
715+
716+
We visite every node/vertex: `O(|V|)` and then for every node, we visite all it's edges, so we have `O(|V|*|E|)`.
717+
718+
Can we do better?
719+
720+
There's no need to visit nodes more than once. Instead of having a local `seen` variable for each node, we can move it outside the loop. However, it won't be a boolean anymore (seen or not seen). We could see nodes more than once, without being in a cycle (example A). One idea is to have 3 states: `unvisited` (0), `visiting` (1) and `visited` (2). Let's devise the algorithm:
721+
722+
*Algorithm*:
723+
724+
* Build a graph as an adjacency list (map + arrays).
725+
* Fill in every prerequisite as an edge on the graph.
726+
* Visit every node and if there's a cycle, return false.
727+
** When we start visiting a node, we mark it as 1 (visiting)
728+
** Visit all its adjacent nodes
729+
** Mark current node as 2 (visited) when we finish visiting neighbors.
730+
** If we see a node in visiting state more than once, it's a cycle!
731+
** If we see a node in a visited state, skip it.
732+
733+
*Implementation*:
734+
735+
[source, javascript]
736+
----
737+
include::interview-questions/course-schedule.js[tags=description;solution]
738+
----
739+
740+
In the first line, we initialize the map with the course index and an empty array.
741+
This time the `seen` array is outside the recursion.
742+
743+
*Complexity Analysis*:
744+
745+
- Time: `O(|V| + |E|)`. We go through each node and edge only once.
746+
- Space: `O(|V| + |E|)`. The size of the adjacency list.
747+
748+
749+
750+
751+
//
752+
[#graph-q-critical-connections-in-a-network]
753+
include::content/part03/graph-search.asc[tag=graph-q-critical-connections-in-a-network]
754+
755+
On idea to find if a path is critical is to remove it. If we visit the graph and see that some nodes are not reachable, then, oops, It was critical!
756+
757+
We can code precisely that. We can remove one link at a time and check if all other nodes are reachable. It's not very efficient, but it's a start.
758+
759+
[source, javascript]
760+
----
761+
include::interview-questions/critical-connections-in-a-network.js[tags=criticalConnectionsBrute1]
762+
----
763+
764+
We are using a function `areAllNodesReachable`, which implements a BFS for visiting the graph, but DFS would have worked too. The runtime is `O(|E| + |V|)`, where `E` is the number of edges and `V` the number of nodes/servers. In `criticalConnectionsBrute1`, We are looping through all `connections` (`E`) to remove one connection at a time and then checking if all servers are still reachable with `areAllNodesReachable`.
765+
766+
The time complexity is `O(|E|^2 * |V|)`. Can we do it on one pass? Sure we can!
767+
768+
*Tarjan's Strongly Connected Components Algorithms*
769+
770+
A connection is critical only if it's not part of the cycle.
771+
772+
In other words, a critical path is like a bridge that connects islands; if you remove it you won't cross from one island to the other.
773+
774+
Connections that are part of the cycle (blue) have redundancy. If you eliminate one, you can still reach other nodes. Check out the examples below.
775+
776+
[graphviz, critical-connections-sol-examples, png]
777+
....
778+
graph G {
779+
subgraph cluster_0 {
780+
a0 -- a1 [color=blue]
781+
a1 -- a2 [color=blue]
782+
a2 -- a0 [color=blue]
783+
a1 -- a3 [color=blue]
784+
a3 -- a2 [color=blue]
785+
label = "Example A";
786+
}
787+
788+
subgraph cluster_3 {
789+
b0 -- b1 [color=blue]
790+
b1 -- b2 [color=blue]
791+
b2 -- b0 [color=blue]
792+
b1 -- b3 [color=red]
793+
b3 -- b2 [color=transparent] // removed
794+
label = "Example B";
795+
}
796+
797+
subgraph cluster_1 {
798+
c0 -- c1 -- c2 -- c3 [color=red]
799+
label = "Example C";
800+
}
801+
}
802+
....
803+
804+
The red connections are critical; if we remove any, some servers won't be reachable.
805+
806+
We can solve this problem in one pass using DFS. But for that, we keep track of the nodes that are part of a loop (strongly connected components). To do that, we use the time of visit (or depth in the recursion) each node.
807+
808+
For example C, if we start on `c0`, it belongs to group 0, then we move c1, c2, and c3, increasing the depth counter. Each one will be on its own group since there's no loop.
809+
810+
For example B, we can start at `b0`, and then we move to `b1` and `b2`. However, `b2` circles back to `b0`, which is on group 0. We can update the group of `b1` and `b2` to be 0 since they are all connected in a loop.
811+
812+
For an *undirected graph*, If we found a node on our dfs, that we have previously visited, we found a loop! We can mark all of them with the lowest group number. We know we have a critical path when it's a connection that links two different groups. For example A, they all will belong to group 0, since they are all in a loop. For Example B, we will have `b0`, `b1`, and `b2` on the same group while `b3` will be on a different group.
813+
814+
*Algorithm*:
815+
816+
* Build the graph as an adjacency list (map + array)
817+
* Run dfs on any node. E.g. `0`.
818+
** Keep track of the nodes that you have seen using `group` array. But instead of marking them as seen or not. Let's mark it with the `depth`.
819+
** Visit all the adjacent nodes that are NOT the parent.
820+
** If we see a node that we have visited yet, do a dfs on it and increase the depth.
821+
** If the adjacent node has a lower grouping number, update the current node with it.
822+
** If the adjacent node has a higher grouping number, then we found a critical path.
823+
824+
*Implementation*:
825+
826+
[source, javascript]
827+
----
828+
include::interview-questions/critical-connections-in-a-network.js[tags=description;solution]
829+
----
830+
831+
This algorithm only works with DFS.
832+
833+
*Complexity Analysis*:
834+
835+
- Time: `O(|E| + |V|)`. We visit each node and edge only once.
836+
- Space: `O(|E| + |V|)`. The graph has all the edges and nodes. Additionally, we use the `group` variable with a size of `|V|`.
837+
838+
839+
840+
841+
842+
843+
844+
//
845+
846+
847+
848+

Diff for: book/content/part03/graph-search.asc

+135-8
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ endif::[]
77

88
Graph search allows you to visit search elements.
99

10-
WARNING: Graph search is very similar to <<Tree Search & Traversal>>. So, if you read that sections some of the concepts here will be familiar to you.
10+
WARNING: Graph search is very similar to <<Tree Search & Traversal>>. So, if you read that section, some of the concepts here will be familiar to you.
1111

12-
There are two ways to navigate the graph, one is using Depth-First Search (DFS) and the other one is Breadth-First Search (BFS). Let's see the difference using the following graph.
12+
There are two ways to navigate the graph, one is using Depth-First Search (DFS), and the other one is Breadth-First Search (BFS). Let's see the difference using the following graph.
1313

1414
image::directed-graph.png[directed graph]
1515

@@ -44,10 +44,10 @@ image::directed-graph.png[directed graph]
4444

4545
==== Depth-First Search for Graphs
4646

47-
With Depth-First Search (DFS) we go deep before going wide.
47+
With Depth-First Search (DFS), we go deep before going wide.
4848

4949
Let's say that we use DFS on the graph shown above, starting with node `0`.
50-
A DFS, will probably visit 5, then visit `1` and continue going down `3` and `2`. As you can see, we need to keep track of visited nodes, since in graphs we can have cycles like `1-3-2`.
50+
A DFS will probably visit 5, then visit `1` and continue going down `3` and `2`. As you can see, we need to keep track of visited nodes, since in graphs, we can have cycles like `1-3-2`.
5151
Finally, we back up to the remaining node `0` children: node `4`.
5252

5353
So, DFS would visit the graph: `[0, 5, 1, 3, 2, 4]`.
@@ -56,13 +56,13 @@ So, DFS would visit the graph: `[0, 5, 1, 3, 2, 4]`.
5656

5757
==== Breadth-First Search for Graphs
5858

59-
With Breadth-First Search (BFS) we go wide before going deep.
59+
With Breadth-First Search (BFS), we go wide before going deep.
6060

6161
// TODO: BFS traversal
6262
Let's say that we use BFS on the graph shown above, starting with the same node `0`.
63-
A BFS, will visit 5 as well, then visit `1` and will not go down to it's children.
63+
A BFS will visit 5 as well, then visit `1` and not go down to its children.
6464
It will first finish all the children of node `0`, so it will visit node `4`.
65-
After all the children of node `0` are visited it continue with all the children of node `5`, `1` and `4`.
65+
After all the children of node `0` are visited, it will continue with all the children of node `5`, `1`, and `4`.
6666

6767
In summary, BFS would visit the graph: `[0, 5, 1, 4, 3, 2]`
6868

@@ -86,4 +86,131 @@ You might wonder what the difference between search algorithms in a tree and a g
8686

8787
The difference between searching a tree and a graph is that the tree always has a starting point (root node). However, in a graph, you can start searching anywhere. There's no root.
8888

89-
NOTE: Every tree is a graph, but not every graph is a tree.
89+
NOTE: Every tree is a graph, but not every graph is a tree. Only acyclic directed graphs (DAG) are trees.
90+
91+
92+
==== Practice Questions
93+
(((Interview Questions, graph)))
94+
95+
96+
97+
98+
// tag::graph-q-course-schedule[]
99+
===== Course Schedule
100+
101+
*gr-1*) _Check if it's possible to take a number of courses while satisfying their prerequisites._
102+
103+
// end::graph-q-course-schedule[]
104+
105+
// _Seen in interviews at: Amazon, Facebook, Bytedance (TikTok)._
106+
107+
108+
*Starter code*:
109+
110+
[source, javascript]
111+
----
112+
include::../../interview-questions/course-schedule.js[tags=description;placeholder]
113+
----
114+
115+
116+
*Examples*:
117+
118+
[source, javascript]
119+
----
120+
canFinish(2, [[1, 0]]); // true
121+
// 2 courses: 0 and 1. One prerequisite: 0 -> 1
122+
// To take course 1 you need to take course 0.
123+
// Course 0 has no prerequisite, so you can take 0 and then 1.
124+
125+
canFinish(2, [[1, 0], [0, 1]]); // false
126+
// 2 courses: 0 and 1. Two prerequisites: 0 -> 1 and 1 -> 0.
127+
// To take course 1, you need to take course 0.
128+
// To Course 0, you need course 1, so you can't any take them!
129+
130+
canFinish(3, [[2, 0], [1, 0], [2, 1]]); // true
131+
// 3 courses: 0, 1, 2. Three prerequisites: 0 -> 2 and 0 -> 1 -> 2
132+
// To take course 2 you need course 0, course 0 has no prerequisite.
133+
// So you can take course 0 first, then course 1, and finally course 2.
134+
135+
canFinish(4, [[1, 0], [2, 1], [3, 2], [1, 3]]); // false
136+
// 4 courses: 0, 1, 2, 3. Prerequisites: 0 -> 1 -> 2 -> 3 and 3 -> 1.
137+
// You can take course 0 first since it has no prerequisite.
138+
// For taking course 1, you need course 3. However, for taking course 3
139+
// you need 2 and 1. You can't finish then!
140+
----
141+
142+
143+
_Solution: <<graph-q-course-schedule>>_
144+
145+
146+
147+
148+
149+
150+
151+
// tag::graph-q-critical-connections-in-a-network[]
152+
===== Critical Network Paths
153+
154+
*gr-2*) _Given `n` servers and the connections between them, return the critical paths._
155+
156+
// end::graph-q-critical-connections-in-a-network[]
157+
158+
// _Seen in interviews at: Amazon, Google._
159+
160+
Examples:
161+
162+
[graphviz, critical-path-examples, png]
163+
....
164+
graph G {
165+
subgraph cluster_1 {
166+
a0 -- a1 -- a2 [color=firebrick1]
167+
label = "Example A";
168+
}
169+
170+
subgraph cluster_0 {
171+
b0 -- b1 [color=blue]
172+
b1 -- b2 [color=blue]
173+
b2 -- b0 [color=blue]
174+
b1 -- b3 [color=blue]
175+
b3 -- b2 [color=blue]
176+
label = "Example B";
177+
b0, b1, b2, b3 [color=midnightblue]
178+
}
179+
180+
subgraph cluster_3 {
181+
c0 -- c1 [color=blue]
182+
c1 -- c2 [color=blue]
183+
c2 -- c0 [color=blue]
184+
c1 -- c3 [color=firebrick1]
185+
c3 -- c2 [color=transparent] // removed
186+
label = "Example C";
187+
c0, c1, c2 [color=midnightblue]
188+
// c3 [color=red]
189+
}
190+
}
191+
....
192+
193+
[source, javascript]
194+
----
195+
// Example A
196+
criticalConnections(3, [[0, 1], [1, 2]]);// [[0, 1], [1, 2]]
197+
// if you remove any link, there will be stranded servers.
198+
199+
// Example B
200+
criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 2]]);// []
201+
// you can remove any connection and all servers will be reachable.
202+
203+
// Example C
204+
criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3]]); // [[1, 3]]
205+
// if you remove [1, 3], then server 3 won't be reachable.
206+
// If you remove any other link. It will be fine.
207+
----
208+
209+
Starter code:
210+
211+
[source, javascript]
212+
----
213+
include::../../interview-questions/critical-connections-in-a-network.js[tags=description;placeholder]
214+
----
215+
216+
_Solution: <<graph-q-critical-connections-in-a-network>>_

Diff for: book/images/course-schedule-examples.png

22 KB
Loading

Diff for: book/images/critical-connections-sol-examples.png

34.8 KB
Loading

Diff for: book/images/critical-path-examples.png

39.9 KB
Loading

0 commit comments

Comments
 (0)