Facebook Pixel

3807. Minimum Cost to Repair Edges to Traverse a Graph ๐Ÿ”’

MediumBreadth-First SearchGraphBinary Search
LeetCode โ†—

Problem Description

You are given an undirected graph with n nodes labeled from 0 to n - 1. The graph consists of m edges represented by a 2D integer array edges, where edges[i] = [uแตข, vแตข, wแตข] indicates that there is an edge between nodes uแตข and vแตข with a repair cost of wแตข.

You are also given an integer k. Initially, all edges are damaged, meaning none of them can be used until they are repaired.

You may choose a non-negative integer money and repair all edges whose repair cost is less than or equal to money. Every edge whose repair cost exceeds money remains damaged and cannot be used.

Your goal is to travel from node 0 to node n - 1 using at most k edges along the path.

Return an integer denoting the minimum amount of money required to make this possible. If there is no choice of money that allows you to reach node n - 1 from node 0 within k edges, return -1.

In short, you need to find the smallest budget such that, after repairing all edges costing no more than that budget, a path from node 0 to node n - 1 exists that uses no more than k edges.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why BFS?

This problem maps to BFS through a short path in the full flowchart.

Graph?yesShortestpath?yesBFS

The shortest sequence of transitions maps to BFS in the state-space graph.

Open in Flowchart
Show step-by-step reasoning

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves n nodes and a set of edges connecting them, which is a clear graph structure.

Is it a tree?

  • No: The graph is a general undirected graph that may contain cycles and multiple edges, not a tree.

Is the problem related to directed acyclic graphs?

  • No: The graph is undirected, so it is not a directed acyclic graph.

Is the problem related to shortest paths?

  • No: We are not minimizing path length or path cost directly; instead, we want the minimum budget so that a path within k edges exists. The traversal itself only needs to check reachability under an edge-count limit.

Does the problem involve connectivity?

  • No: While reachability matters, the core question is not simply whether two nodes are connected via a union-find style grouping; it depends on the number of edges used in the path.

Does the problem have small constraints?

  • No: The constraints are not tiny enough to justify exhaustive DFS/backtracking over all paths.

BFS:

  • Yes: For a fixed budget, BFS naturally explores nodes layer by layer, where each layer corresponds to one additional edge used. This lets us check whether node n - 1 is reachable from node 0 within k edges. Combined with binary search over the sorted repair costs, BFS verifies feasibility for each candidate budget.

Conclusion: The flowchart guides us toward using the Breadth-First Search (BFS) pattern (paired with Binary Search over edge costs) to determine the minimum money required to reach node n - 1 from node 0 using at most k edges.

Intuition

The key observation is about the relationship between the chosen budget money and the edges we can use. As money increases, more edges get repaired and become available. More available edges can only help us reach node n - 1 โ€” they never make it harder. This means the problem has a monotonic property:

  • If a budget money allows us to travel from node 0 to node n - 1 using at most k edges, then any larger budget also works (since the larger budget keeps all those repaired edges and possibly adds more).
  • If a budget is too small to succeed, then any smaller budget also fails.

Whenever we see this "works for everything above a threshold, fails for everything below it" structure, it's a strong signal to use binary search on the answer. We're searching for the smallest budget that flips the result from "impossible" to "possible".

Next, we notice that the optimal money doesn't need to be an arbitrary number. Repairing an edge only matters at the exact cost of some edge โ€” increasing money between two consecutive edge costs changes nothing. So the minimum valid budget must be one of the values in edges. This lets us sort the edges by repair cost and binary search over their indices rather than over a numeric range. If we pick index mid, we repair all edges with cost less than or equal to edges[mid][2].

Finally, for each candidate budget during the binary search, we must verify the actual condition: can we go from node 0 to node n - 1 using at most k edges? Since "at most k edges" is about the number of edges in the path (every edge counts the same regardless of its repair cost), this is an unweighted reachability-within-k-steps question. BFS is the perfect fit here: it explores the graph layer by layer, where each new layer represents using one more edge. If we reach node n - 1 at a layer (distance) that is <= k, the budget is feasible.

Putting it together: sort the edges by cost, binary search on the index to find the smallest budget, and at each step run a BFS over the currently repaired edges to check whether node n - 1 is reachable within k edges. If even repairing all edges fails, the answer is -1.

Pattern Learn more about Breadth-First Search, Graph and Binary Search patterns.

Solution Approach

We use Binary Search + BFS to solve this problem efficiently.

Step 1: Sort the edges by repair cost.

Since the minimum valid budget must equal one of the edge costs, we first sort edges by their cost w:

edges.sort(key=lambda x: x[2])

After sorting, choosing index mid means our budget is edges[mid][2], and all edges from index 0 to mid (which all have cost <= edges[mid][2]) are repaired and usable.

Step 2: Binary search over the edge index.

We binary search on the index of the answer, with left boundary l = 0 and right boundary r = m - 1, where m = len(edges). For each middle index mid = (l + r) >> 1, we check whether repairing edges edges[0..mid] lets us reach node n - 1 from node 0 within k edges:

  • If check(mid) returns True, the budget is enough, so we shrink the search to the left half by setting r = mid.
  • Otherwise, we need a bigger budget, so we set l = mid + 1.
l, r = 0, m - 1
while l < r:
    mid = (l + r) >> 1
    if check(mid):
        r = mid
    else:
        l = mid + 1

This loop converges on the smallest index whose budget might work.

Step 3: The check function using BFS.

For a given index idx, we build an adjacency list g using only the edges edges[0..idx] (the repaired ones):

g = [[] for _ in range(n)]
for u, v, _ in edges[: idx + 1]:
    g[u].append(v)
    g[v].append(u)

Then we run a layer-by-layer BFS starting from node 0. We track the current layer with dist, where dist represents the number of edges used so far. A vis array prevents revisiting nodes. At each layer:

  • If we encounter node n - 1, we return whether dist <= k (we reached the target within the allowed edge count).
  • Otherwise, we expand all unvisited neighbors into the next layer nq, and increment dist.
q = [0]
dist = 0
vis = [False] * n
vis[0] = True
while q:
    nq = []
    for u in q:
        if u == n - 1:
            return dist <= k
        for v in g[u]:
            if not vis[v]:
                vis[v] = True
                nq.append(v)
    q = nq
    dist += 1
return False

Because BFS visits nodes in increasing order of distance, the first time we reach node n - 1 gives the minimum number of edges needed, which is exactly what we compare against k.

Step 4: Final verification and answer.

After the binary search ends, l points to the candidate index. We run check(l) one more time to confirm it actually satisfies the requirement (this handles the case where even repairing all edges is insufficient). If it succeeds, we return edges[l][2]; otherwise, we return -1:

return edges[l][2] if check(l) else -1

Complexity Analysis.

  • Time complexity: O((n + m) ร— log m). The binary search runs O(log m) iterations, and each check call builds the graph and runs a BFS, costing O(n + m). The initial sort costs O(m ร— log m).
  • Space complexity: O(n + m) for the adjacency list, the visited array, and the BFS queue.

Example Walkthrough

Let's trace through a concrete example to see how Binary Search + BFS works.

Input:

  • n = 4 (nodes labeled 0, 1, 2, 3)
  • edges = [[0, 1, 4], [0, 2, 1], [1, 3, 5], [2, 3, 2]]
  • k = 2 (path may use at most 2 edges)

We want the smallest budget money so that, after repairing all edges with cost <= money, we can travel from node 0 to node 3 using at most 2 edges.


Step 1: Sort the edges by repair cost.

Before:  [[0,1,4], [0,2,1], [1,3,5], [2,3,2]]
After:   [[0,2,1], [2,3,2], [0,1,4], [1,3,5]]
          idx 0     idx 1    idx 2    idx 3
          cost=1    cost=2   cost=4   cost=5

Now m = 4, so we binary search over indices [0, 3]. Picking index mid means budget = edges[mid][2] and edges 0..mid are repaired.


Step 2 & 3: Binary search with BFS checks.

Initialize l = 0, r = 3.

Iteration 1: mid = (0 + 3) >> 1 = 1. Budget = edges[1][2] = 2. Repaired edges: [0,2,1], [2,3,2]. Adjacency list:

0 -> [2]
2 -> [0, 3]
3 -> [2]
1 -> []

Run BFS from node 0:

  • Layer dist = 0: queue [0]. Node 0 โ‰  3. Neighbors โ†’ visit 2. Next queue [2].
  • Layer dist = 1: queue [2]. Node 2 โ‰  3. Neighbors โ†’ visit 3 (skip 0). Next queue [3].
  • Layer dist = 2: queue [3]. Node 3 found! Return dist <= k โ†’ 2 <= 2 โ†’ True.

check(1) = True, so the budget is enough. Shrink right: r = 1.

Iteration 2: l = 0, r = 1. mid = (0 + 1) >> 1 = 0. Budget = edges[0][2] = 1. Repaired edges: [0,2,1] only. Adjacency list:

0 -> [2]
2 -> [0]
1 -> []
3 -> []

Run BFS from node 0:

  • Layer dist = 0: queue [0]. Node 0 โ‰  3. Visit 2. Next queue [2].
  • Layer dist = 1: queue [2]. Node 2 โ‰  3. Only neighbor 0 already visited. Next queue [].
  • Queue empty โ†’ return False (node 3 unreachable).

check(0) = False, so we need a bigger budget. Move left: l = mid + 1 = 1.

Now l == r == 1, the loop ends.


Step 4: Final verification and answer.

l = 1. Run check(1) once more โ†’ True (as computed above). Return edges[1][2] = 2.


Why the answer is correct:

  • Budget = 1 repairs only edge 0โ€”2. We can reach node 2 but there's no way onward to node 3. Fails.
  • Budget = 2 repairs 0โ€”2 and 2โ€”3, giving the path 0 โ†’ 2 โ†’ 3 using exactly 2 edges, which satisfies k = 2. Works.

Notice the monotonic property in action: budget 1 fails, and every budget >= 2 succeeds. The binary search efficiently pinpointed the threshold without testing budgets 4 or 5.

Final Output: 2

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def minCost(self, n: int, edges: List[List[int]], k: int) -> int:
6        def check(idx: int) -> bool:
7            # Build an undirected graph using only the first (idx + 1) edges
8            # (i.e., the edges with the smallest weights after sorting).
9            graph = [[] for _ in range(n)]
10            for u, v, _ in edges[: idx + 1]:
11                graph[u].append(v)
12                graph[v].append(u)
13
14            # BFS from node 0 to find the shortest hop-distance to node n - 1.
15            queue = [0]
16            distance = 0
17            visited = [False] * n
18            visited[0] = True
19            while queue:
20                next_queue = []
21                for u in queue:
22                    # Reached the target: valid only if hop count is within k.
23                    if u == n - 1:
24                        return distance <= k
25                    for v in graph[u]:
26                        if not visited[v]:
27                            visited[v] = True
28                            next_queue.append(v)
29                queue = next_queue
30                distance += 1
31            # Target unreachable with this set of edges.
32            return False
33
34        m = len(edges)
35        # Sort edges by weight so the index maps to a weight threshold.
36        edges.sort(key=lambda edge: edge[2])
37
38        # Binary search for the smallest edge index that makes check(idx) True.
39        left, right = 0, m - 1
40        while left < right:
41            mid = (left + right) >> 1
42            if check(mid):
43                right = mid
44            else:
45                left = mid + 1
46
47        # Return the corresponding weight if a valid threshold exists, else -1.
48        return edges[left][2] if check(left) else -1
49
1class Solution {
2    private int nodeCount;
3    private int[][] sortedEdges;
4    private int maxHops;
5
6    /**
7     * Binary searches over the sorted edge weights to find the smallest
8     * weight threshold such that node (n-1) is reachable from node 0
9     * within at most k BFS layers (hops), using only edges whose weight
10     * is no greater than that threshold.
11     */
12    public int minCost(int n, int[][] edges, int k) {
13        this.nodeCount = n;
14        this.sortedEdges = edges;
15        this.maxHops = k;
16
17        // Sort edges by ascending weight so that index in the array
18        // corresponds to a weight threshold.
19        Arrays.sort(edges, (a, b) -> a[2] - b[2]);
20
21        // Binary search on the edge index (i.e., on the weight threshold).
22        int left = 0;
23        int right = edges.length - 1;
24        while (left < right) {
25            int mid = (left + right) >> 1;
26            if (check(mid)) {
27                // Reachable using edges[0..mid]; try a smaller threshold.
28                right = mid;
29            } else {
30                // Not reachable; need a larger threshold.
31                left = mid + 1;
32            }
33        }
34
35        // Verify the final candidate; return its weight if it works.
36        return check(left) ? edges[left][2] : -1;
37    }
38
39    /**
40     * Builds a graph using only edges with index in [0, idx]
41     * (i.e., the idx+1 smallest-weight edges) and performs a
42     * layered BFS from node 0. Returns true if node (n-1) is
43     * reachable within at most maxHops layers.
44     */
45    private boolean check(int idx) {
46        // Adjacency list for the current subgraph.
47        List<Integer>[] graph = new List[nodeCount];
48        Arrays.setAll(graph, i -> new ArrayList<>());
49
50        // Add the idx+1 cheapest edges as undirected connections.
51        for (int i = 0; i <= idx; ++i) {
52            int u = sortedEdges[i][0];
53            int v = sortedEdges[i][1];
54            graph[u].add(v);
55            graph[v].add(u);
56        }
57
58        // Layered BFS: each layer increment counts one extra hop.
59        List<Integer> currentLayer = new ArrayList<>();
60        currentLayer.add(0);
61
62        int distance = 0;
63        boolean[] visited = new boolean[nodeCount];
64        visited[0] = true;
65
66        while (!currentLayer.isEmpty()) {
67            List<Integer> nextLayer = new ArrayList<>();
68            for (int u : currentLayer) {
69                // Target reached: succeed only if within the hop budget.
70                if (u == nodeCount - 1) {
71                    return distance <= maxHops;
72                }
73                // Expand to unvisited neighbors for the next layer.
74                for (int v : graph[u]) {
75                    if (!visited[v]) {
76                        visited[v] = true;
77                        nextLayer.add(v);
78                    }
79                }
80            }
81            currentLayer = nextLayer;
82            ++distance;
83        }
84
85        // Target node never reached within the subgraph.
86        return false;
87    }
88}
89
1class Solution {
2public:
3    int minCost(int n, vector<vector<int>>& edges, int k) {
4        // Sort edges in ascending order by their weight (edge[2]).
5        // This lets us binary search on the edge index: a larger index
6        // means more edges (with greater weights) are available.
7        sort(edges.begin(), edges.end(),
8             [](const vector<int>& a, const vector<int>& b) {
9                 return a[2] < b[2];
10             });
11
12        // check(idx): determine whether, using only the first (idx + 1)
13        // edges (i.e. edges with weight <= edges[idx][2]), there exists a
14        // path from node 0 to node (n - 1) that uses at most k edges (hops).
15        auto check = [&](int idx) -> bool {
16            // Build the adjacency list with the currently allowed edges.
17            vector<vector<int>> graph(n);
18            for (int i = 0; i <= idx; ++i) {
19                int u = edges[i][0];
20                int v = edges[i][1];
21                graph[u].push_back(v);
22                graph[v].push_back(u);
23            }
24
25            // BFS layer by layer from node 0; "dist" tracks the number of
26            // hops from the source to the current frontier.
27            vector<int> currentLevel;
28            currentLevel.push_back(0);
29
30            vector<char> visited(n, 0);
31            visited[0] = 1;
32
33            int dist = 0;
34            while (!currentLevel.empty()) {
35                vector<int> nextLevel;
36                for (int u : currentLevel) {
37                    // Reached the destination: valid only if within k hops.
38                    if (u == n - 1) {
39                        return dist <= k;
40                    }
41                    // Expand to all unvisited neighbors.
42                    for (int v : graph[u]) {
43                        if (!visited[v]) {
44                            visited[v] = 1;
45                            nextLevel.push_back(v);
46                        }
47                    }
48                }
49                // Move to the next BFS layer and increment the hop count.
50                currentLevel.swap(nextLevel);
51                ++dist;
52            }
53            // Destination is unreachable with the allowed edges.
54            return false;
55        };
56
57        int m = edges.size();
58        // Binary search for the smallest edge index that makes check() true.
59        int left = 0, right = m - 1;
60        while (left < right) {
61            int mid = (left + right) >> 1;
62            if (check(mid)) {
63                right = mid;       // mid works, try to find a smaller index.
64            } else {
65                left = mid + 1;    // mid fails, search the larger half.
66            }
67        }
68        // If the final candidate index is valid, return its edge weight as
69        // the minimum cost; otherwise no valid path exists, return -1.
70        return check(left) ? edges[left][2] : -1;
71    }
72};
73
1/**
2 * Finds the minimum possible maximum edge weight along a path from node 0 to node n-1,
3 * such that node n-1 is reachable within at most k hops (BFS levels)
4 * using only edges whose weight is <= that maximum.
5 *
6 * Strategy: sort edges by weight, then binary search on the edge index.
7 * For each candidate index, build a graph using only edges [0..index]
8 * and verify reachability within k levels via BFS.
9 *
10 * @param n     Number of nodes (labeled 0 .. n-1).
11 * @param edges Array of [u, v, weight] triples.
12 * @param k     Maximum allowed number of hops on the path.
13 * @returns     The minimum valid maximum edge weight, or -1 if unreachable.
14 */
15function minCost(n: number, edges: number[][], k: number): number {
16    // Sort edges in ascending order of weight so that index thresholds
17    // correspond to monotonically increasing maximum allowed weight.
18    edges.sort((a, b) => a[2] - b[2]);
19
20    /**
21     * Checks whether node n-1 is reachable from node 0 within k hops,
22     * using only the first (index + 1) lightest edges.
23     *
24     * @param index The highest edge index (inclusive) allowed to be used.
25     * @returns     True if n-1 is reachable in <= k BFS levels, otherwise false.
26     */
27    const check = (index: number): boolean => {
28        // Build an undirected adjacency list from edges[0..index].
29        const graph: number[][] = Array.from({ length: n }, () => []);
30        for (let i = 0; i <= index; i++) {
31            const [u, v] = edges[i];
32            graph[u].push(v);
33            graph[v].push(u);
34        }
35
36        // Standard level-by-level BFS starting from node 0.
37        let queue: number[] = [0];
38        const visited: boolean[] = Array(n).fill(false);
39        visited[0] = true;
40
41        // distance tracks the current BFS level (number of hops from node 0).
42        let distance = 0;
43        while (queue.length > 0) {
44            const nextQueue: number[] = [];
45            for (const u of queue) {
46                // If the target node is reached, succeed only if within k hops.
47                if (u === n - 1) {
48                    return distance <= k;
49                }
50                // Expand to all unvisited neighbors for the next level.
51                for (const v of graph[u]) {
52                    if (!visited[v]) {
53                        visited[v] = true;
54                        nextQueue.push(v);
55                    }
56                }
57            }
58            queue = nextQueue;
59            distance++;
60        }
61
62        // Target node was never reached.
63        return false;
64    };
65
66    // Binary search for the smallest edge index that makes check(index) true.
67    let left = 0;
68    let right = edges.length - 1;
69    while (left < right) {
70        const mid = (left + right) >> 1;
71        if (check(mid)) {
72            // mid works, so the answer is at mid or to its left.
73            right = mid;
74        } else {
75            // mid fails, so a larger threshold is required.
76            left = mid + 1;
77        }
78    }
79
80    // Validate the final candidate; return its weight or -1 if even the
81    // full set of edges cannot satisfy the reachability constraint.
82    return check(left) ? edges[left][2] : -1;
83}
84

Time and Space Complexity

  • Time Complexity: O((m + n) ร— log m). The algorithm first sorts the edges, which takes O(m ร— log m). Then it performs a binary search over the edge index range [0, m - 1], requiring O(log m) iterations. In each iteration, the check function is invoked: it rebuilds the adjacency graph by traversing up to idx + 1 edges (at most m), which costs O(m), and then runs a BFS over the partial graph that visits at most n nodes and m edges, costing O(n + m). Thus each check call is O(m + n), and the binary search contributes O((m + n) ร— log m). Combining the sorting and binary search phases yields an overall time complexity of O((m + n) ร— log m), where n and m are the number of nodes and edges, respectively.

  • Space Complexity: O(n). Although the adjacency list g stores the edges, the dominant auxiliary structures within each check call are the vis array of size n, and the BFS queues q and nq, which together hold at most n nodes. Ignoring the graph storage that is rebuilt per call (and treating it as part of the working memory bounded by edges), the analysis follows the reference and reports the space complexity as O(n), where n is the number of nodes.

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not handling the case where n == 1 (start equals target)

When n == 1, node 0 is node n - 1. No edges are needed, so the answer should be 0. However, the code unconditionally accesses edges[left][2]:

return edges[left][2] if check(left) else -1

If edges is empty (which is common when n == 1), then m = 0, and:

  • left, right = 0, -1, so the while left < right loop is skipped.
  • check(0) builds edges[:1] on an empty list โ€” BFS starts at node 0, immediately sees u == n - 1 (since 0 == 0), and returns 0 <= k โ†’ True.
  • Then edges[left][2] becomes edges[0][2] โ†’ IndexError on the empty list.

Solution: Add an early return for the trivial case before touching edges:

m = len(edges)
# Start node is already the target; no edges (and no money) needed.
if n == 1:
    return 0
if m == 0:
    return -1

Pitfall 2: Treating budget as continuous instead of "one of the edge costs"

A naive instinct is to binary search over the value range of money (e.g., 0 to max(w)). This wastes work and can be subtly wrong if costs are sparse. The key insight is that the optimal money is always exactly equal to some edge's cost โ€” any value between two consecutive costs unlocks the same set of edges as the lower one.

Solution: Binary search over the sorted edge indices (as done here), so each candidate budget edges[mid][2] corresponds to a meaningful state change in the set of usable edges. This guarantees correctness and bounds iterations to O(log m).


Pitfall 3: Using a standard "visited-on-pop" BFS that misreports the hop distance

A frequent mistake is checking u == n - 1 only when a node is dequeued one at a time, while incrementing distance per node rather than per layer. This conflates the number of nodes processed with the number of edges traversed and yields a wrong hop count.

Solution: Use layer-by-layer BFS (as in the code), where distance is incremented once per level. Because BFS expands nodes in nondecreasing hop order, the first time node n - 1 appears, distance equals the minimum number of edges on any path โ€” exactly what must be compared against k.


Pitfall 4: Mutating the caller's input via in-place sort

edges.sort(...) reorders the caller's list in place. If the grader or surrounding code relies on the original edge ordering afterward, this introduces a hard-to-trace side effect.

Solution: Sort a copy instead, to keep the function pure:

edges = sorted(edges, key=lambda edge: edge[2])

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More