Quick Overview

Given a graph of numbered nodes and an edge list, decide whether a target node can be reached from a source node: first along directed edges, then with the edges treated as undirected, and finally when the direct edge between the two nodes may not be used. It tests graph traversal, efficient reachability checks and leaving the caller's input unchanged.

Node Reachability: Directed Paths, Undirected Connectivity, and a Removed Direct Edge

Company: Figma

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: hard

Interview Round: Technical Screen

A graph has `n` nodes numbered `0` to `n - 1` and a list of edges. The interview built the problem up in three steps: 1. With the edges read as directed, can `target` be reached from `source`? 2. With the same edges read as undirected, are `source` and `target` connected? 3. If the direct connection between `source` and `target` breaks, is there still another connection between them? This console version answers all three with one function: `directed` chooses how the edges are read, and `exclude_direct` applies the third step on top of either reading. ### Function Signature ```python def has_connection(n: int, edges: list[list[int]], source: int, target: int, directed: bool, exclude_direct: bool) -> bool: ``` ### Rules - Each edge `[u, v]` joins nodes `u` and `v`. When `directed` is `True`, it can be followed only from `u` to `v`; when `directed` is `False`, it can be followed in both directions. - Return `True` if some path leads from `source` to `target` using only usable edges, each followed in an allowed direction, and `False` otherwise. - When `exclude_direct` is `True`, an edge that joins `source` and `target` directly (`[source, target]` or `[target, source]`) is unusable, and every other edge stays usable. If no such edge exists, the flag changes nothing. When `exclude_direct` is `False`, every edge is usable. - Do not modify `edges`: the caller keeps using the same list after the call. ### Constraints - `2 <= n <= 10^5` - `0 <= len(edges) <= 2 * 10^5` - Every edge `[u, v]` has `0 <= u, v < n` and `u != v`. - No edge appears twice, and no pair of nodes is joined in both orders: if `[u, v]` is present, neither another `[u, v]` nor `[v, u]` is. - `0 <= source, target < n` and `source != target`. - All values fit in a 32-bit signed integer. ### Examples **Example 1** ```text Input: n = 4, edges = [[0, 1], [1, 2], [3, 2]], source = 0, target = 3, directed = True, exclude_direct = False Output: False ``` Following the edges' direction, node 0 reaches node 1 and then node 2, but the only edge at node 3 points from 3 to 2, so node 3 cannot be reached. **Example 2** ```text Input: n = 4, edges = [[0, 1], [1, 2], [3, 2]], source = 0, target = 3, directed = False, exclude_direct = False Output: True ``` Read as undirected, the edges give the path 0 - 1 - 2 - 3. **Example 3** ```text Input: n = 5, edges = [[0, 1], [1, 2], [2, 0], [2, 3], [3, 4]], source = 2, target = 3, directed = False, exclude_direct = True Output: False ``` Without the edge `[2, 3]`, nodes 0, 1 and 2 are connected to each other and nodes 3 and 4 to each other, but nothing joins the two groups. With the same graph and flags, `source = 0` and `target = 1` would return `True`: ignoring `[0, 1]` still leaves the path 0 - 2 - 1.

Overview: Given a graph of numbered nodes and an edge list, decide whether a target node can be reached from a source node: first along directed edges, then with the edges treated as undirected, and finally when the direct edge between the two nodes may not be used. It tests graph traversal, efficient reachability checks and leaving the caller's input unchanged.

Read the full Figma Software Engineer interview experience this question came from

A graph has `n` nodes numbered `0` to `n - 1` and a list `edges`, where each edge `[u, v]` joins nodes `u` and `v`. You are given two distinct nodes, `source` and `target`, and two boolean flags that decide which edges may be used and in which direction. The interview built the problem up in three steps: (1) with the edges read as directed, can `target` be reached from `source`? (2) with the same edges read as undirected, are `source` and `target` connected? (3) if the direct connection between `source` and `target` breaks, is there still another connection between them? This function answers all three: `directed` chooses how the edges are read, and `exclude_direct` applies the third step on top of either reading. **Rules** - When `directed` is `True`, an edge `[u, v]` can be followed only from `u` to `v`. When `directed` is `False`, it can be followed in both directions. - When `exclude_direct` is `True`, an edge that joins `source` and `target` directly (stored as `[source, target]` or as `[target, source]`) is unusable, and every other edge stays usable. If no such edge exists, the flag changes nothing. When `exclude_direct` is `False`, every edge is usable. - Return `True` if some path leads from `source` to `target` using only usable edges, each followed in an allowed direction, and `False` otherwise. - Do not modify `edges`: the caller keeps using the same list after the call. **Function signature** ```python def has_connection(n: int, edges: list[list[int]], source: int, target: int, directed: bool, exclude_direct: bool) -> bool: ``` **Constraints** - `2 <= n <= 10^5` - `0 <= len(edges) <= 2 * 10^5` - Every edge `[u, v]` has `0 <= u, v < n` and `u != v`. - No edge appears twice, and no pair of nodes is joined in both orders: if `[u, v]` is present, neither another `[u, v]` nor `[v, u]` is. - `0 <= source, target < n` and `source != target`. - All values fit in a 32-bit signed integer; no value can exceed 2^31 - 1, so a 32-bit `int` is enough in every language. **Example 1** ```text Input: n = 4, edges = [[0, 1], [1, 2], [3, 2]], source = 0, target = 3, directed = True, exclude_direct = False Output: False ``` Following the edges' direction, node 0 reaches node 1 and then node 2, but the only edge at node 3 points from 3 to 2, so node 3 cannot be reached. With `directed = False` the same input returns `True`: read as undirected, the edges give the path 0 - 1 - 2 - 3. **Example 2** ```text Input: n = 5, edges = [[0, 1], [1, 2], [2, 0], [2, 3], [3, 4]], source = 2, target = 3, directed = False, exclude_direct = True Output: False ``` Without the edge `[2, 3]`, nodes 0, 1 and 2 are connected to each other and nodes 3 and 4 to each other, but nothing joins the two groups. With the same graph and flags, `source = 0` and `target = 1` would return `True`: ignoring `[0, 1]` still leaves the path 0 - 2 - 1.

Constraints

  • 2 <= n <= 10^5
  • 0 <= len(edges) <= 2 * 10^5
  • Every edge [u, v] has 0 <= u, v < n and u != v.
  • No edge appears twice, and no pair of nodes is joined in both orders: if [u, v] is present, neither another [u, v] nor [v, u] is.
  • 0 <= source, target < n and source != target.
  • All values fit in a 32-bit signed integer (no value can exceed 2^31 - 1).

Examples

Input: (2, [], 0, 1, False, False)

Expected Output: False

Explanation: Minimum graph with no edges: source and target are both isolated.

Input: (2, [[0, 1]], 0, 1, True, False)

Expected Output: True

Explanation: Single edge read as directed: 0 -> 1 reaches the target.

Hints

  1. First pin down which moves each edge [u, v] allows: only from u to v when `directed` is True, both ways when it is False.
  2. `exclude_direct` sets aside at most one edge: the one whose endpoints are exactly `source` and `target`, whichever order it is stored in. Edges that touch only one of them stay usable.
  3. A path may pass through up to 10^5 nodes, so make sure each node is handled at most once and that a very long path cannot exhaust a call stack.

Loading coding console...

Show the approach

Approach

Algorithm: make one pass over edges to build adjacency lists. An edge is skipped only when exclude_direct is true and its two endpoints are exactly source and target, in either stored order; every other edge [u, v] adds the move u -> v, plus v -> u when directed is false. Then run an iterative depth-first search from source with an explicit stack and a visited array, marking a node when it is pushed so each node enters the stack at most once. Return true as soon as target is popped; return false when the stack empties.

Invariant and correctness: every marked node is reachable from source through usable edges followed in allowed directions, because a node is marked only when it is a neighbour of an already-marked node along a usable edge. Conversely, every popped node pushes all of its unmarked neighbours, so by induction on path length every node reachable from source is eventually marked and popped. Therefore the search pops target exactly when a valid path exists.

Edge cases: an empty edge list returns false because source != target; a direct edge stored as [target, source] is excluded as well, which changes the answer only in the undirected reading (in the directed reading it can never be followed out of source toward target anyway); when no direct edge exists the flag removes nothing; edges that touch only one of source or target always stay usable; edges is only read, never modified; the explicit stack avoids recursion-depth limits on a path through up to 10^5 nodes. The JavaScript, Java and C++ references store the adjacency in flat offset arrays (compressed rows) to stay well inside memory limits at 2 * 10^5 edges.

Time complexity:
O(n + m), where m = len(edges)
Space complexity:
O(n + m)