Problem overview
The method validPath receives the number of vertices n, a list of edges, a source vertex and a destination vertex. Vertices are labeled 0 to n - 1. Each edge [u, v] connects two vertices in both directions. The method returns True if you can walk from source to destination along edges, and False otherwise.
In the first example, n = 3 and the edges form a triangle: [[0, 1], [1, 2], [2, 0]]. From 0 you can reach 2 directly, or through 1, so the answer is True. In the second example, n = 6 with edges [[0, 1], [0, 2], [3, 5], [5, 4], [4, 3]]. Vertices 0, 1 and 2 form one group, and 3, 4 and 5 form another. No edge joins the groups, so there is no path from 0 to 5 and the answer is False.
Approach: explore everything reachable from the source
Checking every possible path would be wasteful. A simpler question is enough: which vertices can be reached from source? If destination is one of them, a path exists.
Depth-first search (DFS) answers that question. Stand on a vertex, mark it as visited, then go to one unvisited neighbor and repeat from there. When a vertex has no unvisited neighbors left, the search backs up to the previous vertex and tries its next neighbor. In the end, the visited set holds every vertex connected to the source.
Two things make this work. First, the graph needs an adjacency list, a map from each vertex to its neighbors, so the neighbors of a vertex are found without scanning all the edges. Second, the search needs the visited set. Without it, a cycle such as the triangle in the first example would send the search around forever: 0 to 1 to 2 and back to 0.
On the first example, DFS from 0 visits 0, then its first neighbor 1, then 1's unvisited neighbor 2. The vertex 2 sees only visited neighbors, so the search backs out. The visited set is {0, 1, 2}, which contains the destination 2.
Step by step
- Create the adjacency list.
graphis adefaultdict(list), so reading a vertex that has no neighbors yet gives an empty list, not aKeyError. - Create the visited set,
visited, to remember reached vertices. Membership tests on a set take constant time on average. - Loop over
edges, with each pair in the variableedge. - Record one direction: append
edge[1]to the neighbors ofedge[0]. - Record the other direction: append
edge[0]to the neighbors ofedge[1]. The graph is bi-directional, so a walk must be able to cross each edge both ways. - Define the inner function
dfs(node). It is nested insidevalidPath, so it readsgraphandvisiteddirectly, and only the node needs to be passed in. - Mark the node visited first. Marking before exploring means any neighbor that leads back finds this vertex already visited.
- Loop over the node's neighbors, with each one in
adjacent_node. - Recurse only on unvisited neighbors:
if adjacent_node not in visited, calldfson it. - Start the search by calling
dfs(source)in the body ofvalidPath. Defining a function doesn't run it. - Return whether
destinationis invisited. The expressiondestination in visitedis already a boolean.
The solution in Python
from collections import defaultdict
class Solution:
def validPath(self, n, edges, source, destination) -> bool:
graph = defaultdict(list)
visited = set()
# Build the graph
for edge in edges:
graph[edge[0]].append(edge[1])
graph[edge[1]].append(edge[0])
def dfs(node):
visited.add(node)
for adjacent_node in graph[node]:
if adjacent_node not in visited:
dfs(adjacent_node)
dfs(source)
return destination in visited
The Python details worth a closer look:
defaultdict(list). When you read or write a missing key, it creates an empty list for that key first. That is whygraph[edge[0]].append(...)works on a key seen for the first time. A side effect is that readinggraph[node]for an isolated vertex also inserts an empty entry. That is harmless here.- A set, not
{}. Empty braces{}create an empty dict. An empty set is writtenset(). - Closures.
dfsusesvisitedandgraphfrom the enclosing method. Since it only callsvisited.addand never assigns a new value to the name, it needs nononlocaldeclaration. - Source equals destination. The search starts by adding
sourcetovisited, sovalidPath(1, [], 0, 0)returnsTrue. - Recursion depth. Python allows about 1000 nested calls by default and then raises a
RecursionError. A graph that is one long chain of a few thousand vertices makesdfsgo that deep. For graphs like that, the same search is written with an explicit stack: a list used withappendandpop, holding vertices still to explore.
Complexity
Time: O(n + e), where e is the number of edges. Building the graph touches each edge once. The search visits each vertex once, because a visited vertex is never expanded again, and it looks at each edge from both of its ends, so it examines every neighbor list entry once.
Space: O(n + e). The adjacency list stores two entries per edge. The visited set holds at most n vertices, and the recursion can be up to n calls deep.
Edge cases and common mistakes
- No edges. With
edges = [], the answer isTruewhensource == destinationandFalseotherwise.validPath(2, [], 0, 1)returnsFalse. - Disconnected graph. The second example shows this: the search never leaves the source's group.
- Adding only one direction. Without the second
append, the graph becomes directed. A path that needs to walk an edge backwards is then missed. - Marking visited too late. If a vertex is added to
visitedonly after the loop over its neighbors, two neighbors can call each other forever, until the recursion limit is hit. - Forgetting the visited check. A cycle makes the search endless.
- Returning inside the search. Returning early works too, but this version is simpler: finish the search, then test membership once.
- Unused
n. The method never needsn, because the adjacency list is built from the edges. A vertex with no edges has no entry ingraph, and the dictionary supplies an empty list when it is looked up.
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 = [],source = 0,destination = 1givesFalse.