Problem overview
The method validPath receives n, the number of vertices labeled 0 to n - 1, a list of edges, and two vertices, source and destination. Every edge [u, v] is bi-directional: you can walk it from u to v and from v to u. The method returns true when some chain of edges leads from source to destination, and false otherwise.
In the first example, n = 3 with edges [[0,1],[1,2],[2,0]] forms a triangle, so vertex 0 reaches vertex 2 directly or through vertex 1: the answer is true. In the second, n = 6 and the edges split the graph into two separate groups, {0, 1, 2} and {3, 4, 5}. Vertex 0 can't reach vertex 5, so the answer is false.
Approach: explore everything reachable from the source
The edges list is awkward to search, because finding the neighbors of a vertex would mean scanning every edge. The first step is to build an adjacency list: an array with one entry per vertex, holding the vertices that vertex is connected to.
Then run a depth-first search (DFS) from source. DFS visits a vertex, then follows its first unvisited neighbor as far as it can go, then backs up and tries the next neighbor. Every vertex that is connected to source gets visited exactly once. A Set of visited vertices serves two purposes: it records the answer, and it stops the search from walking in circles around a cycle.
After the search finishes, destination is reachable exactly when it is in the visited set. On the second example, the search from 0 visits 0, 1 and 2 only, and 5 is not among them.
Step by step
- Create the adjacency list
graphas avar: an array ofnempty[Int]arrays, so indexiholds the neighbors of vertexi. - Create the empty set
visitedof typeSet<Int>. It is avarbecause the search inserts into it. - Loop over
edges. Eachedgeis a pair of endpoints. - Record the edge both ways. Append
edge[1]to the neighbors ofedge[0], andedge[0]to the neighbors ofedge[1]. Without the second append, the search could not walk an edge backwards. - Call
dfsonsource, passinggraphand&visited. The ampersand marks aninoutargument, which lets the helper change the caller's set. - Return whether
visitedcontainsdestination.containsalready returns aBool. - Declare
dfswith the current node, the graph, and the visited set as aninoutparameter. Withoutinout, the helper would work on its own copy of the set. - Insert the node into
visitedfirst. Marking a node before exploring its neighbors means a neighbor that leads back sees it as visited and doesn't return to it. - Loop over
graph[node]with the variableadjacentNode. - Recurse only when the neighbor isn't visited yet. Without this check, two connected vertices would call each other forever.
The solution in Swift
class Solution {
func validPath(_ n: Int, _ edges: [[Int]], _ source: Int, _ destination: Int) -> Bool {
var graph = [[Int]](repeating: [Int](), count: n)
var visited = Set<Int>()
for edge in edges {
graph[edge[0]].append(edge[1])
graph[edge[1]].append(edge[0])
}
dfs(source, graph, &visited)
return visited.contains(destination)
}
func dfs(_ node: Int, _ graph: [[Int]], _ visited: inout Set<Int>) {
visited.insert(node)
for adjacentNode in graph[node] {
if !visited.contains(adjacentNode) {
dfs(adjacentNode, graph, &visited)
}
}
}
}
The Swift details worth a closer look:
- Independent empty arrays.
[[Int]](repeating: [Int](), count: n)repeats one empty arrayntimes. Arrays are value types, so each copy is independent, and appending tograph[0]doesn't affectgraph[1]. In a language where arrays are shared references, this pattern would be a trap. inoutand&. Swift passes sets and arrays by value, copying lazily. A parameter markedinoutis written back to the caller's variable when the function returns. The call site must write&visitedto make that visible. Passing the graph withoutinoutis fine because the helper only reads it, and Swift doesn't copy it unless someone changes it.Set<Int>. Checking whether it contains a value takes constant time on average, andinsertdoes nothing if the value is already there.- Unlabeled parameters. The underscores let callers write
dfs(source, graph, &visited)without labels. - Methods calling each other.
validPathcallsdfsby plain name. Inside a class method, Swift finds other methods of the same class without writingself..
Complexity
Let V be the number of vertices and E the number of edges.
Time: O(V + E). Building the adjacency list touches each edge once. The search visits each vertex once and looks at each vertex's neighbor list once, which adds up to 2E entries.
Space: O(V + E). The adjacency list stores 2E entries plus an array per vertex. The visited set holds up to V vertices, and the recursion can go as deep as V calls.
Edge cases and common mistakes
- Source equals destination.
n = 1with no edges andsource = destination = 0returnstrue, because the source is inserted intovisitedfirst. - No edges. With
n = 2, no edges and different endpoints, the result isfalse. - Edges written in either order.
[[1,0],[2,1]]from 2 to 0 returnstrue, thanks to the two-way recording. - Recording each edge once. Adding only
graph[edge[0]]treats the graph as directed, and paths that use an edge backwards are missed. - Marking a vertex after the recursive calls. On a cycle the search comes back to a vertex that isn't marked yet and never stops.
- Forgetting
inout. Without it the helper changes only a local copy, andvalidPathsees an empty set. - Very long chains. The recursion depth equals the length of the longest path the search follows. A single chain of a few hundred thousand vertices can overflow the call stack and crash the program; the exact limit depends on the stack size. For huge inputs, a loop with an explicit stack avoids the limit.
Testing
n = 3,[[0,1],[1,2],[2,0]], from 0 to 2 givestrue.n = 6,[[0,1],[0,2],[3,5],[5,4],[4,3]], from 0 to 5 givesfalse.n = 1, no edges, from 0 to 0 givestrue.n = 2, no edges, from 0 to 1 givesfalse.n = 3,[[1,0],[2,1]], from 2 to 0 givestrue.
