Problem overview
You get an undirected graph with n vertices numbered 0 to n - 1. The edges arrive as edges, a list of two-element IntArrays, where [u, v] means vertices u and v are connected in both directions. The function validPath returns true when some chain of edges leads from source to destination, and false when none does. No edge repeats and no vertex has an edge to itself.
In the first example, n = 3 and the edges form a triangle: [[0, 1], [1, 2], [2, 0]]. From 0 to 2 you can go directly, or through 1, so the answer is true. In the second example, n = 6, vertices 0, 1 and 2 form one group and vertices 3, 4 and 5 another, with no edge between them, so there is no path from 0 to 5 and the answer is false.
Approach: explore everything reachable from the source
Raw edge pairs are awkward for this question, because finding the neighbours of a vertex means scanning all edges. So first convert them into an adjacency list: for every vertex, the list of vertices it touches. Then run depth-first search (DFS) from source. DFS visits a vertex, then goes as deep as possible along its neighbours before backing up to try the others. When it finishes, the set of visited vertices is exactly the set reachable from source, and the answer is whether destination is in it.
The one thing DFS must never do is revisit a vertex. In an undirected graph every edge can be crossed both ways, so without a record of visited vertices, two neighbours would call each other forever. The solution marks a vertex as visited as soon as it enters it, which also breaks longer cycles such as the triangle above.
On the triangle, DFS starts at 0 and marks it. The neighbours of 0 are 1 and 2. It enters 1, marks it, and sees neighbours 0 (visited) and 2. It enters 2, marks it, and finds nothing new. Back at the start, 2 is already marked. The visited set is {0, 1, 2}, which contains the destination.
Step by step
- Create the adjacency list.
graphis an array ofnempty mutable lists, sograph[i]will hold the neighbours of vertexi. TheArray(n) { ... }constructor runs its lambda once per index, so every vertex gets its own list. - Create the visited set. A hash-based set answers "have we been here?" quickly.
- Loop over
edges. Eachedgeis a pair, with its endpoints inedge[0]andedge[1]. - Record each edge in both directions. The graph is undirected, so each endpoint goes into the other's list. If one direction is missing, searches that must travel "backwards" fail.
- Start the search. Call a helper named
dfsonsourcewithgraphandvisited. The helper is written next. - Answer from the set. Return whether
visitedcontainsdestination.containsalready returns aBoolean, so noifis needed. - Declare
dfs. It takes the currentnode, thegraphand thevisitedset. The set is passed by reference, so the helper's changes are visible tovalidPath. - Mark the node first. Adding it to
visitedbefore looking at its neighbours is what stops cycles from causing infinite recursion. - Loop over
graph[node]withadjacentNodeas the loop variable. - Recurse only on unvisited neighbours. Skipping visited ones guarantees that each vertex is entered once.
The solution in Kotlin
class Solution {
fun validPath(n: Int, edges: List<IntArray>, source: Int, destination: Int): Boolean {
val graph = Array(n) { mutableListOf<Int>() }
val visited = mutableSetOf<Int>()
for (edge in edges) {
graph[edge[0]].add(edge[1])
graph[edge[1]].add(edge[0])
}
dfs(source, graph, visited)
return visited.contains(destination)
}
fun dfs(node: Int, graph: Array<MutableList<Int>>, visited: MutableSet<Int>) {
visited.add(node)
for (adjacentNode in graph[node]) {
if (!visited.contains(adjacentNode)) {
dfs(adjacentNode, graph, visited)
}
}
}
}
The Kotlin details worth a closer look:
Array(n) { mutableListOf<Int>() }builds an array whose element at each index comes from the lambda. Because the lambda runs once per index, the lists are separate objects. Filling an array with one shared list would make every vertex see every edge. Its type isArray<MutableList<Int>>, which is the typedfsdeclares for its parameter.mutableSetOf<Int>()creates aMutableSet<Int>. A plainSetis a read-only view with noaddmethod.- Passing collections gives the callee a reference, not a copy. That is how
dfsfills the caller's set without returning anything. - Calling a sibling method by name.
dfsis a member of the same class, sovalidPathcalls it directly, even though it is declared below. !visited.contains(adjacentNode)is the negated membership test. Kotlin also allowsadjacentNode !in visited, which means the same.- Deep recursion. Each recursive call uses a stack frame. On a long chain of vertices, say a path with thousands of vertices, the default JVM stack can overflow and throw
StackOverflowError. The search with an explicit stack of its own avoids that limit.
Complexity
Time: O(n + e), where e is the number of edges. Building the lists touches each edge once. The search enters each vertex at most once and looks at each neighbour list once, so it examines each edge a constant number of times.
Space: O(n + e). The adjacency lists hold two entries per edge, the visited set holds up to n vertices, and the recursion can be up to n calls deep.
Edge cases and common mistakes
source == destination.n = 1with no edges and both ends 0 givestrue, since the search marks the source before anything else.- No edges. With
n = 2, an empty list of edges and different endpoints, the answer isfalse. The visited set holds only the source. - Edges listed in the "wrong" direction. With
n = 4, the single edge[2, 3],source = 3anddestination = 2returnstrue, because each edge is stored both ways. Adding onlygraph[edge[0]].add(edge[1])would answerfalse. - Marking too late. If a node were added to
visitedonly after its neighbours had been processed, a cycle would recurse forever. - Disconnected graphs. The second example shows it: the other group of vertices is never reached, so
destinationisn't invisited. - Shared lists. Building the array with one list object repeated
ntimes merges every vertex's neighbours into one pile.
Testing
n = 3, edges[[0, 1], [1, 2], [2, 0]],source = 0,destination = 2givestrue.n = 6, edges[[0, 1], [0, 2], [3, 5], [5, 4], [4, 3]],source = 0,destination = 5givesfalse.n = 1, no edges,source = 0,destination = 0givestrue.n = 2, no edges,source = 0,destination = 1givesfalse.n = 4, edges[[2, 3]],source = 3,destination = 2givestrue.