Problem overview
The function valid_path receives the number of vertices n (labeled 0 to n - 1), a vector of edges, and two vertices, source and destination. Each edge [u, v] connects two vertices in both directions. The function returns true when you can walk from source to destination along edges, and false otherwise.
In the first example, n = 3 with edges [[0, 1], [1, 2], [2, 0]] forms a triangle, so vertex 2 is reachable from vertex 0 and the answer is true. In the second, n = 6 with edges [[0, 1], [0, 2], [3, 5], [5, 4], [4, 3]] has two separate groups, {0, 1, 2} and {3, 4, 5}. Vertex 5 can't be reached from vertex 0, so the answer is false.
Approach: explore everything reachable from the source
Checking every possible path would be wasteful, and the number of paths can be huge. The question is only whether the destination belongs to the same connected group as the source. So explore the whole group once, remembering each vertex you have seen, and look at the end whether the destination is among them.
Depth-first search does the exploring. From a vertex, step to an unvisited neighbor and keep going deeper from there; when a vertex has no new neighbors, return to the previous one and try its next neighbor. The set of visited vertices does two jobs. It makes sure no vertex is processed twice, and it stops the search from running around a cycle forever, like the triangle in the first example.
Before searching, the edge list is turned into an adjacency list: a vector whose position i holds the neighbors of vertex i. Finding a vertex's neighbors then takes no scan over all the edges.
On the first example the adjacency list is [[1, 2], [0, 2], [1, 0]]. The search visits 0 and goes to its neighbor 1. From 1 it skips 0, which is visited, and goes to 2. Now visited is {0, 1, 2}, which contains the destination 2.
Step by step
- Create the adjacency list
graphas a mutableVec<Vec<i32>>holdingnempty vectors. The length must be ausize, hencen as usize. - Create an empty
HashSet<i32>calledvisited, to remember which vertices the search has reached. - Loop over
&edges. Borrowing keeps the vector from being moved, and eachedgeis a reference to aVec<i32>. - Record each edge in both directions. Push
edge[1]onto the list ofedge[0]andedge[0]onto the list ofedge[1], indexing withas usize. Without the second push, the search could not walk an edge backward. - Start the search with
Self::dfs(source, &graph, &mut visited). The graph is borrowed immutably and the set mutably, so the helper fills the caller's set. - Return whether
visitedcontains the destination, as the final expression.containstakes a reference, so pass&destination. - Write the helper
dfs, with parametersnode: i32,graph: &Vec<Vec<i32>>andvisited: &mut HashSet<i32>. - Insert the current node into
visitedfirst. Marking before exploring is what stops two neighbors from calling each other forever. - Loop over the node's neighbors,
&graph[node as usize], with the pattern&adjacent_nodeto copy each number out of its reference. - Recurse on every neighbor that is not yet visited, passing
graphandvisitedon as they are: they are already references.
The solution in Rust
impl Solution {
fn valid_path(n: i32, edges: Vec<Vec<i32>>, source: i32, destination: i32) -> bool {
let mut graph: Vec<Vec<i32>> = vec![Vec::new(); n as usize];
let mut visited: HashSet<i32> = HashSet::new();
for edge in &edges {
graph[edge[0] as usize].push(edge[1]);
graph[edge[1] as usize].push(edge[0]);
}
Self::dfs(source, &graph, &mut visited);
visited.contains(&destination)
}
fn dfs(node: i32, graph: &Vec<Vec<i32>>, visited: &mut HashSet<i32>) {
visited.insert(node);
for &adjacent_node in &graph[node as usize] {
if !visited.contains(&adjacent_node) {
Self::dfs(adjacent_node, graph, visited);
}
}
}
}
The Rust details worth a closer look:
HashSetcomes fromstd::collections. A standalone program needsuse std::collections::HashSet;at the top.vec![Vec::new(); n]clones the empty vectorntimes, so every vertex gets its own independent list.- Borrowing.
&graphhands the helper a shared read-only view, and&mut visitedhands it exclusive write access. Insidedfsthe parameters are already references, so recursion passesgraphandvisitedwithout another&or&mut. Rust does not let two&mutborrows of the same set overlap, but passing the existing one down is fine. - Casting indexes. Vertices are stored as
i32, but indexing a vector needs ausize, so every index is writtenx as usize. for &adjacent_node in &graph[...]. Iterating a borrowed vector yields&i32items; the&in the pattern copies each one into a plaini32.- No return value in
dfs. It works by side effect, fillingvisited. Its last statement is aforloop, so it returns().
Complexity
Time: O(n + m), where m is the number of edges. Building the adjacency list touches each edge once. The search visits each vertex once, and over the whole search every adjacency list is read once, which adds up to 2m neighbor checks.
Space: O(n + m). The adjacency list holds 2m entries plus n vectors, and the set holds up to n vertices. The recursion also uses stack space proportional to the longest chain of vertices followed, up to n in the worst case.
Edge cases and common mistakes
- Source equals destination. With
n = 1, no edges andsource = destination = 0, the search marks 0 as visited and the answer istrue. - No edges at all. Every vertex is alone, so the answer is
trueonly when the source is the destination. - Edges given "backwards". For
n = 2andedges = [[1, 0]]the answer from 0 to 1 istrue, because both directions are recorded. Adding only one direction would make itfalse. - Cycles. Skipping the
containscheck, or marking a vertex only after its neighbors are explored, sends the triangle example into endless recursion. - Very long chains. Each recursive call uses a stack frame. A single line of 200,000 vertices, which the input limits allow, can make this recursive version overflow the stack and abort the program. An explicit stack replaces the recursion: push the source, then repeatedly pop a vertex and push each neighbor for which
visited.insert(neighbor)returnstrue. That version handled the same 200,000-vertex chain, andinsertreturnsfalsewhen the value was already in the set.
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,edges = [],source = 0,destination = 0givestrue.n = 2,edges = [[1,0]],source = 0,destination = 1givestrue.
