Start learning free

LESSON 331 OF 344 · GRAPHS

Find if Path Exists in Graph in Java: Depth-First Search

Practice O(V+E) Read the full explanation

There is a bi-directional graph with n vertices, where each vertex is labeled from 0 to n - 1 (inclusive). The edges in the graph are represented as a 2D integer array edges, where each edges[i] = [ui, vi] denotes a bi-directional edge between vertex ui and vertex vi. Every vertex pair is connected by at most one edge, and no vertex has an edge to itself.

You want to determine if there is a valid path that exists from vertex source to vertex destination.

Given edges and the integers n, source, and destination, return true if there is a valid path from source to destination, or false otherwise.

 

Example 1:

Input: n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
Output: true
Explanation: There are two paths from vertex 0 to vertex 2:
- 0 → 1 → 2
- 0 → 2

Example 2:

Input: n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
Output: false
Explanation: There is no path from vertex 0 to vertex 5.

 

Constraints:

  • 1 <= n <= 2 * 105
  • 0 <= edges.length <= 2 * 105
  • edges[i].length == 2
  • 0 <= ui, vi <= n - 1
  • ui != vi
  • 0 <= source, destination <= n - 1
  • There are no duplicate edges.
  • There are no self edges.
STEP 1 OF 11
    Ctrl + Enter to check

    JAVA · GRAPHS

    Find If Path Exist In A Graph in Java, explained

    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 is an int[] pair [u, v] joining two vertices, and it works in both directions. The method returns true when some chain of edges leads from source to destination, and false otherwise.

    In the first example, n = 3 and the edges form a triangle: 0-1, 1-2 and 2-0. From vertex 0 you can reach 2 directly or through 1, so the answer is true. In the second example, vertices 0, 1 and 2 are joined together, and 3, 4 and 5 form a separate group. There is no way from 0 to 5, so the answer is false.

    Approach: explore everything reachable from the source

    A list of edges is hard to search directly, since finding the neighbors of one vertex would mean scanning every edge. So the first move is to rebuild the data as an adjacency list: for each vertex, the list of vertices it connects to. After that, "where can I go from here?" is a single lookup.

    Then run a depth-first search (DFS) from source. DFS visits a vertex, then picks an unvisited neighbor and goes as deep as it can before backing up. Each vertex it reaches goes into a visited set. When the search ends, visited holds exactly the vertices connected to the source, so the answer is whether destination is in that set.

    The visited set also keeps the search finite. Graphs have cycles, such as the triangle above. Without it, the search would go 0, then 1, then 2, then 0 again, forever.

    Step by step

    1. Create the adjacency list as a List<List<Integer>> and add an empty ArrayList for each of the n vertices, so that graph.get(i) holds the neighbors of vertex i.
    2. Create the visited set as a HashSet. Membership checks take constant time on average.
    3. Loop over edges with the variable edge of type int[]. The two endpoints are edge[0] and edge[1].
    4. Record the edge in both directions. Add edge[1] to the list of edge[0], and edge[0] to the list of edge[1]. Skipping the second one would turn the graph into a directed one and miss paths that cross an edge backwards.
    5. Run the search: call dfs(source, graph, visited), a helper written next. It fills visited.
    6. Return visited.contains(destination), which already gives the boolean.
    7. Write dfs as a private void method taking the current node, the graph and the visited set. Every recursive call shares the same set.
    8. Mark the node visited first, before looking at neighbors. Then a neighbor that leads back to this node already sees it as visited.
    9. Loop over graph.get(node) with an int variable adjacentNode, and recurse only into the neighbors that aren't in visited yet.

    The solution in Java

    class Solution {
        public boolean validPath(int n, List<int[]> edges, int source, int destination) {
            List<List<Integer>> graph = new ArrayList<>();
            for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
            Set<Integer> visited = new HashSet<>();
            for (int[] edge : edges) {
                graph.get(edge[0]).add(edge[1]);
                graph.get(edge[1]).add(edge[0]);
            }
            dfs(source, graph, visited);
            return visited.contains(destination);
        }
    
        private void dfs(int node, List<List<Integer>> graph, Set<Integer> visited) {
            visited.add(node);
            for (int adjacentNode : graph.get(node)) {
                if (!visited.contains(adjacentNode)) {
                    dfs(adjacentNode, graph, visited);
                }
            }
        }
    }
    

    Java details worth a closer look:

    • A list of lists is built in two steps. new ArrayList<>() for the outer list creates an empty list. Each vertex's inner list must then be added separately. The diamond <> lets the compiler fill in the element type from the declaration.
    • Edges come as a List<int[]>. Each element is an array, so the endpoints are read with brackets, edge[0], while the list itself is read with get.
    • Autoboxing. A List<Integer> stores Integer objects. graph.get(edge[0]).add(edge[1]) boxes the int on the way in, and for (int adjacentNode : ...) unboxes on the way out.
    • Shared state through references. Java passes the reference to visited by value, so the caller and all recursive calls see the same set. That is why dfs can return void.
    • Recursion depth. DFS goes one frame deeper for each vertex along a path. The constraints allow up to 200,000 vertices, and a graph that is one long chain can make the recursion deep enough to throw StackOverflowError. This lesson's solution is fine for the lesson, but a long chain is the reason many programmers write DFS with an explicit ArrayDeque used as a stack.

    Complexity

    Let m be the number of edges.

    Time: O(n + m). Building the lists takes O(n) for the empty lists and O(m) for the edges. The search visits each vertex once and looks at each edge from both ends, so O(n + m) in total. Set operations take constant time on average.

    Space: O(n + m). The adjacency list holds each edge twice, and the set and the recursion stack are at most n in size.

    Edge cases and common mistakes

    • Source equals destination. The source is added to visited before anything else, so the answer is true even with no edges. For example, n = 1, no edges and source = destination = 0 gives true.
    • No edges and different vertices. Nothing is reachable, and the answer is false.
    • Disconnected graph. The second example shows it: only the group of the source is ever visited.
    • Adding each edge in one direction only. A path like 0, 1, 2 built from edges [1,0] and [2,1] would then be missed.
    • Marking vertices visited too late. If a node is marked only after its neighbors have been explored, cycles cause endless recursion.
    • Forgetting the inner lists. An ArrayList starts empty, so graph.get(i) throws an IndexOutOfBoundsException until the loop has added the vertex lists.

    Testing

    • n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2 gives true.
    • n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5 gives false.
    • n = 1, edges = [], source = 0, destination = 0 gives true.
    Keep going with JavaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free