Every route from source to target
A directed graph has n nodes numbered 0 to n − 1 and a list of
edges, each [u, v] an edge from u to v. Return every path from node
0 to node n − 1 that does not repeat a node, as lists of nodes. Order the paths
lexicographically: compare them element by element, and a path that is a prefix of another comes first.
Examples
| Input | Output | Why |
|---|---|---|
n = 4, edges = [[0,1],[0,2],[1,3],[2,3]] | [[0,1,3],[0,2,3]] | Two routes. |
n = 4, edges = [[0,1],[1,2],[2,0],[2,3]] | [[0,1,2,3]] | The cycle back to 0 is never taken: 0 is already on the path. |
n = 3, edges = [[0,1]] | [] | Node 2 cannot be reached. |
Constraints
- 2 ≤ n ≤ 12
- 0 ≤ edges.length ≤ n(n − 1)
- No self-loops or repeated edges
Hints
Hint 1
Search depth-first from 0, keeping the current path in a list.
Hint 2
When the search reaches n − 1, copy the path into the answer.
Hint 3
A node is unavailable only while it is on the current path: remove it when the search backs out. Sorting each adjacency list makes the answer come out in lexicographic order.
Where people lose marks
- Marking nodes visited for good, as in a plain traversal: paths that share a node are then lost.
- Appending the path list itself rather than a copy, so every recorded path changes as the search continues.
- Not guarding against cycles when the graph is not guaranteed to be acyclic.
Dry run on 5,[[0,1],[0,2],[1,3],[2,3],[1,2],[3,4],[2,4]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 18current path00paths foundnone yet0
Start at 0, the only node on the path.
-
Step 2 / 18current path0011paths foundnone yet0
Extend the path to 1.
-
Step 3 / 18current path001122paths foundnone yet0
Extend the path to 2.
-
Step 4 / 18current path00112233paths foundnone yet0
Extend the path to 3.
-
Step 5 / 18current path0011223344paths found012340
Reached 4: record the path 0 → 1 → 2 → 3 → 4. Then back up to look for others.
-
Step 6 / 18current path001122paths found012340
Every way on from 3 is explored: remove 3 from the path so other paths may use it.
-
Step 7 / 18current path00112243paths found01234001241
Reached 4: record the path 0 → 1 → 2 → 4. Then back up to look for others.
-
Step 8 / 18current path0011paths found01234001241
Every way on from 2 is explored: remove 2 from the path so other paths may use it.
-
Step 9 / 18current path001132paths found01234001241
Extend the path to 3.
-
Step 10 / 18current path00113243paths found0123400124101342
Reached 4: record the path 0 → 1 → 3 → 4. Then back up to look for others.
-
Step 11 / 18current path0011paths found0123400124101342
Every way on from 3 is explored: remove 3 from the path so other paths may use it.
-
Step 12 / 18current path00paths found0123400124101342
Every way on from 1 is explored: remove 1 from the path so other paths may use it.
-
Step 13 / 18current path0021paths found0123400124101342
Extend the path to 2.
-
Step 14 / 18current path002132paths found0123400124101342
Extend the path to 3.
-
Step 15 / 18current path00213243paths found012340012410134202343
Reached 4: record the path 0 → 2 → 3 → 4. Then back up to look for others.
-
Step 16 / 18current path0021paths found012340012410134202343
Every way on from 3 is explored: remove 3 from the path so other paths may use it.
-
Step 17 / 18current path002142paths found0123400124101342023430244
Reached 4: record the path 0 → 2 → 4. Then back up to look for others.
-
Step 18 / 18current path00paths found0123400124101342023430244
Every way on from 2 is explored: remove 2 from the path so other paths may use it. Search finished: 5 paths.
How to think about it
This is the depth-first search pattern. Keep the current path and a set of the nodes on it. From the last node, try each neighbour that is not already on the path; on reaching n − 1, record a copy. When every neighbour of a node has been tried, remove it from the path so that other paths may pass through it.
Sorting each adjacency list in increasing order makes the search produce paths in lexicographic order: two paths first differ where the search chose a smaller neighbour for one of them, and the smaller choice is explored first.
Complexity. Time O(P · n), P = number of paths · Space O(n) besides the output
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def all_paths(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
for nb in adj:
nb.sort() # smallest first: paths come out in order
out, path, on_path = [], [0], [False] * n
on_path[0] = True
def explore(u):
if u == n - 1:
out.append(path[:]) # a copy: path keeps changing
return
for v in adj[u]:
if not on_path[v]:
on_path[v] = True
path.append(v)
explore(v)
path.pop() # backtrack
on_path[v] = False
explore(0)
return out#include <algorithm>
#include <functional>
#include <vector>
std::vector<std::vector<int>> solve(int n, const std::vector<std::vector<int>>& edges) {
std::vector<std::vector<int>> adj(n), out;
for (const auto& e : edges) adj[e[0]].push_back(e[1]);
for (auto& nb : adj) std::sort(nb.begin(), nb.end());
std::vector<int> path{0};
std::vector<bool> onPath(n, false);
onPath[0] = true;
std::function<void(int)> explore = [&](int u) {
if (u == n - 1) { out.push_back(path); return; }
for (int v : adj[u]) {
if (onPath[v]) continue;
onPath[v] = true; path.push_back(v);
explore(v);
path.pop_back(); onPath[v] = false; // backtrack
}
};
explore(0);
return out;
}import java.util.*;
class Solution {
private List<List<Integer>> adj;
private List<int[]> out;
private Deque<Integer> path;
private boolean[] onPath;
private int target;
public int[][] allPaths(int n, int[][] edges) {
adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) adj.get(e[0]).add(e[1]);
for (List<Integer> nb : adj) Collections.sort(nb);
out = new ArrayList<>();
path = new ArrayDeque<>();
onPath = new boolean[n];
target = n - 1;
path.addLast(0);
onPath[0] = true;
explore(0);
return out.toArray(new int[0][]);
}
private void explore(int u) {
if (u == target) {
out.add(path.stream().mapToInt(Integer::intValue).toArray()); // a copy
return;
}
for (int v : adj.get(u)) {
if (onPath[v]) continue;
onPath[v] = true; path.addLast(v);
explore(v);
path.removeLast(); onPath[v] = false; // backtrack
}
}
}function allPaths(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) adj[u].push(v);
for (const nb of adj) nb.sort((a, b) => a - b);
const out = [], path = [0], onPath = new Array(n).fill(false);
onPath[0] = true;
const explore = u => {
if (u === n - 1) { out.push([...path]); return; } // record a copy
for (const v of adj[u]) {
if (onPath[v]) continue;
onPath[v] = true; path.push(v);
explore(v);
path.pop(); onPath[v] = false; // backtrack
}
};
explore(0);
return out;
}fn solve(n: i64, edges: &[[i64; 2]]) -> Vec<Vec<i64>> {
let n = n as usize;
let mut adj = vec![Vec::new(); n];
for e in edges {
adj[e[0] as usize].push(e[1] as usize);
}
for nb in adj.iter_mut() {
nb.sort();
}
fn explore(u: usize, adj: &[Vec<usize>], path: &mut Vec<i64>, on_path: &mut Vec<bool>, out: &mut Vec<Vec<i64>>) {
if u == adj.len() - 1 {
out.push(path.clone());
return;
}
for &v in &adj[u] {
if on_path[v] {
continue;
}
on_path[v] = true;
path.push(v as i64);
explore(v, adj, path, on_path, out);
path.pop(); // backtrack
on_path[v] = false;
}
}
let mut out = Vec::new();
let mut on_path = vec![false; n];
on_path[0] = true;
explore(0, &adj, &mut vec![0], &mut on_path, &mut out);
out
}import Data.List (sort)
import qualified Data.Map.Strict as M
solve :: Int -> [[Int]] -> [[Int]]
solve n edges = explore [0]
where
adj = M.map sort (M.fromListWith (++) [(u, [v]) | [u, v] <- edges])
-- the path is kept reversed: its head is the current node
explore path@(u : _)
| u == n - 1 = [reverse path]
| otherwise = concat [explore (v : path) | v <- M.findWithDefault [] u adj, v `notElem` path]
Variants to try
- Count the paths instead of listing them. On a DAG, dynamic programming in topological order counts them in O(n + m).
- Return only the paths of minimum length (breadth-first search, keeping all predecessors).
- Return paths whose total weight equals a target: the next problem.
Next problems
Routes with an exact total weight
Count the paths from source to destination, never repeating a node, whose edge weights add up to exactly the target.
Open the problem → Hard30 minRoutes that visit every node once
Count the paths from source to destination that visit every node exactly once. Backtracking works for small n; a bitmask over visited nodes makes it O(2ⁿ · n²).
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem →Write a solution and run it against the real test table.
Keep going
Routes with an exact total weight
Count the paths from source to destination, never repeating a node, whose edge weights add up to exactly the target.
Open the problem → Hard30 minRoutes that visit every node once
Count the paths from source to destination that visit every node exactly once. Backtracking works for small n; a bitmask over visited nodes makes it O(2ⁿ · n²).
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.