Skip to content

Feat/graph questions #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions book/D-interview-questions-solutions.asc
Original file line number Diff line number Diff line change
Expand Up @@ -655,3 +655,194 @@ We could also have used a Map and keep track of the indexes, but that's not nece

- Time: `O(n)`. We visit each letter once.
- 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`.







:leveloffset: +1

=== Solutions for Graph Questions
(((Interview Questions Solutions, Graph)))

:leveloffset: -1


[#graph-q-course-schedule]
include::content/part03/graph-search.asc[tag=graph-q-course-schedule]

Basically, we have to detect if the graph has a cycle or not.
There are multiple ways to detect cycles on a graph using BFS and DFS.

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)

A critical part of solving this exercise is coming up with good test cases. Let's examine these two:

[graphviz, course-schedule-examples, png]
....
digraph G {
subgraph cluster_1 {
a0 -> a1 -> a2
a0 -> a2 [color=gray]
label = "Example A"
}
subgraph cluster_2 {
b0 -> b1 -> b2 -> b3
b3 -> b1 [color=red]
label = "Example B";
}
}
....

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.

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!

[source, javascript]
----
include::interview-questions/course-schedule.js[tags=brute1]
----

We built the graph on the fly as an adjacency list (Map + Arrays).
Then we visited each node, checking if there it has cycles. If none has cyles, then we return true.

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.

What's the time complexity?

We visite every node/vertex: `O(|V|)` and then for every node, we visite all it's edges, so we have `O(|V|*|E|)`.

Can we do better?

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:

*Algorithm*:

* Build a graph as an adjacency list (map + arrays).
* Fill in every prerequisite as an edge on the graph.
* Visit every node and if there's a cycle, return false.
** When we start visiting a node, we mark it as 1 (visiting)
** Visit all its adjacent nodes
** Mark current node as 2 (visited) when we finish visiting neighbors.
** If we see a node in visiting state more than once, it's a cycle!
** If we see a node in a visited state, skip it.

*Implementation*:

[source, javascript]
----
include::interview-questions/course-schedule.js[tags=description;solution]
----

In the first line, we initialize the map with the course index and an empty array.
This time the `seen` array is outside the recursion.

*Complexity Analysis*:

- Time: `O(|V| + |E|)`. We go through each node and edge only once.
- Space: `O(|V| + |E|)`. The size of the adjacency list.




//
[#graph-q-critical-connections-in-a-network]
include::content/part03/graph-search.asc[tag=graph-q-critical-connections-in-a-network]

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!

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.

[source, javascript]
----
include::interview-questions/critical-connections-in-a-network.js[tags=criticalConnectionsBrute1]
----

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`.

The time complexity is `O(|E|^2 * |V|)`. Can we do it on one pass? Sure we can!

*Tarjan's Strongly Connected Components Algorithms*

A connection is critical only if it's not part of the cycle.

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.

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.

[graphviz, critical-connections-sol-examples, png]
....
graph G {
subgraph cluster_0 {
a0 -- a1 [color=blue]
a1 -- a2 [color=blue]
a2 -- a0 [color=blue]
a1 -- a3 [color=blue]
a3 -- a2 [color=blue]
label = "Example A";
}
subgraph cluster_3 {
b0 -- b1 [color=blue]
b1 -- b2 [color=blue]
b2 -- b0 [color=blue]
b1 -- b3 [color=red]
b3 -- b2 [color=transparent] // removed
label = "Example B";
}
subgraph cluster_1 {
c0 -- c1 -- c2 -- c3 [color=red]
label = "Example C";
}
}
....

The red connections are critical; if we remove any, some servers won't be reachable.

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.

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.

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.

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.

*Algorithm*:

* Build the graph as an adjacency list (map + array)
* Run dfs on any node. E.g. `0`.
** 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`.
** Visit all the adjacent nodes that are NOT the parent.
** If we see a node that we have visited yet, do a dfs on it and increase the depth.
** If the adjacent node has a lower grouping number, update the current node with it.
** If the adjacent node has a higher grouping number, then we found a critical path.

*Implementation*:

[source, javascript]
----
include::interview-questions/critical-connections-in-a-network.js[tags=description;solution]
----

This algorithm only works with DFS.

*Complexity Analysis*:

- Time: `O(|E| + |V|)`. We visit each node and edge only once.
- Space: `O(|E| + |V|)`. The graph has all the edges and nodes. Additionally, we use the `group` variable with a size of `|V|`.







//




143 changes: 135 additions & 8 deletions book/content/part03/graph-search.asc
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ endif::[]

Graph search allows you to visit search elements.

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.
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.

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.
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.

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

Expand Down Expand Up @@ -44,10 +44,10 @@ image::directed-graph.png[directed graph]

==== Depth-First Search for Graphs

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

Let's say that we use DFS on the graph shown above, starting with node `0`.
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`.
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`.
Finally, we back up to the remaining node `0` children: node `4`.

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

==== Breadth-First Search for Graphs

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

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

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

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

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.

NOTE: Every tree is a graph, but not every graph is a tree.
NOTE: Every tree is a graph, but not every graph is a tree. Only acyclic directed graphs (DAG) are trees.


==== Practice Questions
(((Interview Questions, graph)))




// tag::graph-q-course-schedule[]
===== Course Schedule

*gr-1*) _Check if it's possible to take a number of courses while satisfying their prerequisites._

// end::graph-q-course-schedule[]

// _Seen in interviews at: Amazon, Facebook, Bytedance (TikTok)._


*Starter code*:

[source, javascript]
----
include::../../interview-questions/course-schedule.js[tags=description;placeholder]
----


*Examples*:

[source, javascript]
----
canFinish(2, [[1, 0]]); // true
// 2 courses: 0 and 1. One prerequisite: 0 -> 1
// To take course 1 you need to take course 0.
// Course 0 has no prerequisite, so you can take 0 and then 1.
canFinish(2, [[1, 0], [0, 1]]); // false
// 2 courses: 0 and 1. Two prerequisites: 0 -> 1 and 1 -> 0.
// To take course 1, you need to take course 0.
// To Course 0, you need course 1, so you can't any take them!
canFinish(3, [[2, 0], [1, 0], [2, 1]]); // true
// 3 courses: 0, 1, 2. Three prerequisites: 0 -> 2 and 0 -> 1 -> 2
// To take course 2 you need course 0, course 0 has no prerequisite.
// So you can take course 0 first, then course 1, and finally course 2.
canFinish(4, [[1, 0], [2, 1], [3, 2], [1, 3]]); // false
// 4 courses: 0, 1, 2, 3. Prerequisites: 0 -> 1 -> 2 -> 3 and 3 -> 1.
// You can take course 0 first since it has no prerequisite.
// For taking course 1, you need course 3. However, for taking course 3
// you need 2 and 1. You can't finish then!
----


_Solution: <<graph-q-course-schedule>>_







// tag::graph-q-critical-connections-in-a-network[]
===== Critical Network Paths

*gr-2*) _Given `n` servers and the connections between them, return the critical paths._

// end::graph-q-critical-connections-in-a-network[]

// _Seen in interviews at: Amazon, Google._

Examples:

[graphviz, critical-path-examples, png]
....
graph G {
subgraph cluster_1 {
a0 -- a1 -- a2 [color=firebrick1]
label = "Example A";
}
subgraph cluster_0 {
b0 -- b1 [color=blue]
b1 -- b2 [color=blue]
b2 -- b0 [color=blue]
b1 -- b3 [color=blue]
b3 -- b2 [color=blue]
label = "Example B";
b0, b1, b2, b3 [color=midnightblue]
}
subgraph cluster_3 {
c0 -- c1 [color=blue]
c1 -- c2 [color=blue]
c2 -- c0 [color=blue]
c1 -- c3 [color=firebrick1]
c3 -- c2 [color=transparent] // removed
label = "Example C";
c0, c1, c2 [color=midnightblue]
// c3 [color=red]
}
}
....

[source, javascript]
----
// Example A
criticalConnections(3, [[0, 1], [1, 2]]);// [[0, 1], [1, 2]]
// if you remove any link, there will be stranded servers.
// Example B
criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 2]]);// []
// you can remove any connection and all servers will be reachable.
// Example C
criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3]]); // [[1, 3]]
// if you remove [1, 3], then server 3 won't be reachable.
// If you remove any other link. It will be fine.
----

Starter code:

[source, javascript]
----
include::../../interview-questions/critical-connections-in-a-network.js[tags=description;placeholder]
----

_Solution: <<graph-q-critical-connections-in-a-network>>_
Binary file added book/images/course-schedule-examples.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/images/critical-connections-sol-examples.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/images/critical-path-examples.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading