Problem overview
The method validPath receives n, the number of vertices labelled 0 to n - 1, a vector of edges, and two vertices, source and destination. Each entry [u, v] is a two-way connection between vertices u and v. The method returns true when you can walk from source to destination along edges, and false otherwise. There are no self-edges and no duplicate edges.
In the first example, n = 3 and the edges form a triangle, so 0 reaches 2 directly: the answer is true. In the second, the edges split the six vertices into two groups, {0, 1, 2} and {3, 4, 5}, with no edge between them. Vertex 0 can't reach 5, so the answer is false.
Approach: explore everything reachable from the source
The question is whether destination sits in the same connected piece of the graph as source. To answer it, explore from source and see whether the exploration ever reaches destination.
Depth-first search (DFS) explores by going as deep as it can: from a vertex, step to one unvisited neighbour, then continue from there, and only come back to try the next neighbour when that branch is exhausted. A recursive function expresses this in a few lines.
Two preparations are needed first:
- An adjacency list. The input lists edges, but DFS asks "which vertices touch this one?" many times. Storing each vertex's neighbours in a vector answers that quickly.
- A visited set. Graphs can contain cycles, such as the triangle in the first example. Without a record of where you have been, the search would walk around the cycle forever. Marking each vertex when you enter it guarantees every vertex is processed once.
After the search finishes, the visited set holds exactly the vertices reachable from source. The answer is whether destination is in it.
Step by step
- Create the adjacency list
graphas avector<vector<int>>withnentries. The size constructor makesnempty inner vectors, sograph[i]is ready to receive neighbours of vertexi. - Create the visited set as an
unordered_set<int>namedvisited. Membership tests take constant time on average. - Loop over
edgeswithauto& edge, so each edge is read in place rather than copied. - Record one direction: push
edge[1]intograph[edge[0]]. - Record the other direction: push
edge[0]intograph[edge[1]]. The graph is bi-directional, so the search must be able to cross each edge both ways. Skipping this line turns the graph into a directed one and loses paths that run "backwards". - Run the search by calling a
dfshelper, defined below, withsource,graphandvisited. - Answer the question:
visited.count(destination)is 1 if the vertex is in the set and 0 if not, so comparing it with 0 gives the boolean. - Define
dfsas avoidfunction that takes the startingnode, plus the graph and the set by reference. Passing by reference makes every recursive call update the same set. - Mark the vertex first. Inserting
nodebefore looking at its neighbours means a neighbour that leads back here already sees it as visited. - Loop over the neighbours in
graph[node]. - Recurse on unvisited ones only. If
visited.count(adjacentNode)is 0, calldfson it. Without the check, two neighbouring vertices would call each other endlessly.
The solution in C++
class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
vector<vector<int>> graph(n);
unordered_set<int> visited;
for (auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
dfs(source, graph, visited);
return visited.count(destination) > 0;
}
void dfs(int node, vector<vector<int>>& graph, unordered_set<int>& visited) {
visited.insert(node);
for (int adjacentNode : graph[node]) {
if (!visited.count(adjacentNode)) {
dfs(adjacentNode, graph, visited);
}
}
}
};
C++ details to notice:
- Pass containers by reference. With
unordered_set<int>& visitedevery recursive call shares one set. If you wrote the parameter without&, each call would get its own copy, the marks would vanish when the call returned, and the search would revisit vertices. It would also copy a large set at every step. - Member functions can call each other in any order.
validPathcallsdfs, which is defined below it. Inside a class, that is fine. vector<vector<int>> graph(n)calls the size constructor, which createsnempty inner vectors. That is whygraph[edge[0]].push_back(...)works at once, with no setup loop.countversusinsert. For a set,countreturns 0 or 1, so it works as a membership test. Elements are added withinsert, since sets have nopush_back.- Range-based loops over neighbours.
for (int adjacentNode : graph[node])copies eachint, which is as cheap as a reference. The loop never changesgraph[node], so the iteration is safe. - Recursion depth. Each call of
dfsuses stack space. On a graph that is one long chain, the depth can reach the number of vertices. Withnup to 200,000, that is deep enough to risk a stack overflow on some systems. The fix is an explicit stack (avector<int>you push and pop yourself) or a queue-based search.
Complexity
Time: O(n + e), where e is the number of edges. Building the adjacency list touches each edge once. The search enters each vertex at most once, thanks to the visited set, and scans each vertex's neighbour list once, which in total covers every edge twice (once from each end).
Space: O(n + e). The adjacency list stores two entries per edge plus one vector per vertex. The visited set holds up to n vertices, and the recursion can be up to n levels deep.
Edge cases and common mistakes
- Source equals destination. The search inserts
sourcefirst, so the set contains it and the method returnstrue. Withn = 1and no edges, the answer istrue. - No edges at all. With
n = 2, an emptyedgesand different endpoints, the search visits only the source, and the answer isfalse. - Disconnected graphs. The second example is the typical case: the destination lives in a piece the search never enters.
- Forgetting the reverse edge. With only
graph[edge[0]]filled, an edge written as[2, 0]would only be usable from2to0. - Marking too late. If a vertex is marked only after its neighbours are explored, a cycle brings the search back to it before it is marked, and the recursion never ends.
- Isolated vertices. Vertices with no edges just have empty neighbour lists. The loops simply don't run for them.
Testing
n = 3,edges = [[0,1],[1,2],[2,0]], from 0 to 2 givestrue.n = 6,edges = [[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.