diff --git a/problems/3620-network-recovery-pathways/analysis.md b/problems/3620-network-recovery-pathways/analysis.md new file mode 100644 index 0000000..97eb862 --- /dev/null +++ b/problems/3620-network-recovery-pathways/analysis.md @@ -0,0 +1,66 @@ +# 3620. Network Recovery Pathways + +[LeetCode Link](https://leetcode.com/problems/network-recovery-pathways/) + +Difficulty: Hard +Topics: Array, Binary Search, Dynamic Programming, Graph Theory, Topological Sort, Heap (Priority Queue), Shortest Path +Acceptance Rate: 36.6% + +This is a genuinely hard problem, but it becomes very approachable once you spot that it combines two classic ideas you already know. Take your time — the payoff is a clean, fast solution. + +## Hints + +### Hint 1 + +You are asked for the **maximum** of a **minimum** ("the largest minimum-edge cost"). Whenever you see "maximize the minimum" (or "minimize the maximum"), that is a strong signal to think about **binary searching on the answer** rather than trying to compute it directly. Ask yourself: *if I fix a target score `x`, can I check whether some valid path achieves a score of at least `x`?* + +### Hint 2 + +Suppose you fix a candidate score `x`. A path has score at least `x` if and only if **every edge on it costs at least `x`**. So throw away every edge with cost `< x`, and now you only have to answer a simpler yes/no question: using only the remaining edges (and only online intermediate nodes), does there exist a path from `0` to `n − 1` whose total cost is at most `k`? Notice the feasibility is **monotonic**: if a score `x` is achievable, any smaller score is too. That monotonicity is exactly what binary search needs. + +### Hint 3 + +The yes/no check is "is there a `0 → n−1` path with total cost `≤ k`?". The cheapest such path is what matters, so compute the **minimum total cost** to reach `n − 1` using only edges with cost `≥ x`. Because the graph is a **DAG**, you don't need Dijkstra — a single pass in **topological order** gives you the minimum cost with a simple DP relaxation. Only relax *out of* a node that is online (endpoints `0` and `n − 1` are always online), so offline nodes are automatically excluded as intermediates. Feasible when `dist[n−1] ≤ k`. The candidate values of `x` only ever need to be actual edge costs, so binary search over the sorted distinct costs. + +## Approach + +We want the largest value `x` such that there is a valid path from `0` to `n − 1` on which every edge costs at least `x`. Equivalently, the answer is the best achievable "bottleneck" (minimum edge) subject to the total-cost budget `k`. + +**Binary search on the score.** Let `feasible(x)` be true when there exists a path from `0` to `n − 1` such that: + +1. every edge used has cost `≥ x`, +2. all intermediate nodes are online, +3. the total cost of the path is `≤ k`. + +If `feasible(x)` is true then `feasible(x')` is true for every `x' ≤ x` (fewer restrictions on which edges we may use), so `feasible` is monotonically decreasing in `x`. We binary search for the largest `x` for which it holds. The score of any valid path equals the minimum edge cost along it, which is one of the edge costs, so it suffices to search over the **sorted distinct edge costs**. + +**Evaluating `feasible(x)` on a DAG.** Given `x`, keep only edges with cost `≥ x`. We now need the minimum total cost of a path `0 → n−1`. Since the graph is a DAG, process nodes in a fixed **topological order** (computed once with Kahn's algorithm). Maintain `dist[u]` = minimum cost to reach `u` from `0`; initialize `dist[0] = 0` and everything else to infinity. When we visit `u` in topological order: + +- Skip it if `dist[u]` is still infinity (unreachable) or if `u` is offline — we may never route *through* an offline node. (`0` and `n − 1` are always online.) +- Otherwise, for each kept edge `u → v` with cost `c ≥ x`, relax `dist[v] = min(dist[v], dist[u] + c)`. + +Because we visit nodes in topological order, `dist[u]` is final by the time we process `u`, so one pass suffices. After the pass, `feasible(x)` is `dist[n−1] ≤ k`. + +**Putting it together.** Sort the distinct costs, binary search for the largest feasible one. If no cost is feasible (or there are no edges), return `-1`. + +*Walkthrough (Example 1):* `edges = [[0,1,5],[1,3,10],[0,2,3],[2,3,4]]`, all online, `k = 10`. Distinct costs `[3,4,5,10]`. For `x = 3` we keep all edges; the cheapest path `0→2→3` costs `7 ≤ 10`, so feasible. For `x = 4` we drop the cost-3 edge, node `2` becomes unreachable, and the only path `0→1→3` costs `15 > 10`, infeasible. The largest feasible candidate is `3`, the answer. + +**Watch the magnitudes.** With up to `5·10⁴` edges each costing up to `10⁹`, a path total can reach `5·10¹³`, and `k` itself can be `5·10¹³`. These exceed 32-bit range, so use 64-bit integers throughout (`int64` in Go) for `k`, the distances, and the return value. + +## Complexity Analysis + +Let `n` be the number of nodes and `m` the number of edges. + +Time Complexity: O((n + m) · log m). The topological sort is O(n + m) once. Each `feasible` check is a single O(n + m) DAG pass, and binary search performs O(log m) of them (there are at most `m` distinct costs). + +Space Complexity: O(n + m) for the adjacency list, the topological order, the distance array, and the sorted distinct costs. + +## Edge Cases + +- **No edges at all (`m = 0`).** There is no path from `0` to `n − 1`, so the answer is `-1`. The binary search naturally returns `-1` when the candidate list is empty. +- **No path within budget.** Even the cheapest `0 → n−1` path may exceed `k`, or `0` and `n − 1` may be disconnected. Every candidate is infeasible and the answer is `-1`. +- **Offline intermediate nodes.** A node being offline must forbid routing through it while still allowing it to be reached-but-unused; guarding relaxation on `online[u]` handles this. Endpoints `0` and `n − 1` are guaranteed online. +- **Zero-cost edges and `k = 0`.** Costs can be `0`, so a valid path (and score `0`) can exist even when `k = 0`. Use `≤` comparisons so a total cost exactly equal to `k` counts as valid. +- **Large totals / overflow.** Path totals and `k` can be up to `5·10¹³`; use 64-bit integers to avoid overflow. +- **Direct edge `0 → n−1`.** A single edge is a valid path; its score is its own cost when the cost is within `k`. +``` diff --git a/problems/3620-network-recovery-pathways/problem.md b/problems/3620-network-recovery-pathways/problem.md new file mode 100644 index 0000000..0ecfd85 --- /dev/null +++ b/problems/3620-network-recovery-pathways/problem.md @@ -0,0 +1,117 @@ +--- +number: "3620" +frontend_id: "3620" +title: "Network Recovery Pathways" +slug: "network-recovery-pathways" +difficulty: "Hard" +topics: + - "Array" + - "Binary Search" + - "Dynamic Programming" + - "Graph Theory" + - "Topological Sort" + - "Heap (Priority Queue)" + - "Shortest Path" +acceptance_rate: 3656.9 +is_premium: false +created_at: "2026-07-03T04:23:55.041547+00:00" +fetched_at: "2026-07-03T04:23:55.041547+00:00" +link: "https://leetcode.com/problems/network-recovery-pathways/" +date: "2026-07-03" +--- + +# 3620. Network Recovery Pathways + +You are given a directed acyclic graph of `n` nodes numbered from 0 to `n − 1`. This is represented by a 2D array `edges` of length `m`, where `edges[i] = [ui, vi, costi]` indicates a one‑way communication from node `ui` to node `vi` with a recovery cost of `costi`. + +Some nodes may be offline. You are given a boolean array `online` where `online[i] = true` means node `i` is online. Nodes 0 and `n − 1` are always online. + +A path from 0 to `n − 1` is **valid** if: + + * All intermediate nodes on the path are online. + * The total recovery cost of all edges on the path does not exceed `k`. + + + +For each valid path, define its **score** as the minimum edge‑cost along that path. + +Return the **maximum** path score (i.e., the largest **minimum** -edge cost) among all valid paths. If no valid path exists, return -1. + + + +**Example 1:** + +**Input:** edges = [[0,1,5],[1,3,10],[0,2,3],[2,3,4]], online = [true,true,true,true], k = 10 + +**Output:** 3 + +**Explanation:** + +![](https://assets.leetcode.com/uploads/2025/06/06/graph-10.png) + + * The graph has two possible routes from node 0 to node 3: + + 1. Path `0 -> 1 -> 3` + + * Total cost = `5 + 10 = 15`, which exceeds k (`15 > 10`), so this path is invalid. + + 2. Path `0 -> 2 -> 3` + + * Total cost = `3 + 4 = 7 <= k`, so this path is valid. + + * The minimum edge‐cost along this path is `min(3, 4) = 3`. + + * There are no other valid paths. Hence, the maximum among all valid path‐scores is 3. + + + + +**Example 2:** + +**Input:** edges = [[0,1,7],[1,4,5],[0,2,6],[2,3,6],[3,4,2],[2,4,6]], online = [true,true,true,false,true], k = 12 + +**Output:** 6 + +**Explanation:** + +![](https://assets.leetcode.com/uploads/2025/06/06/graph-11.png) + + * Node 3 is offline, so any path passing through 3 is invalid. + + * Consider the remaining routes from 0 to 4: + + 1. Path `0 -> 1 -> 4` + + * Total cost = `7 + 5 = 12 <= k`, so this path is valid. + + * The minimum edge‐cost along this path is `min(7, 5) = 5`. + + 2. Path `0 -> 2 -> 3 -> 4` + + * Node 3 is offline, so this path is invalid regardless of cost. + + 3. Path `0 -> 2 -> 4` + + * Total cost = `6 + 6 = 12 <= k`, so this path is valid. + + * The minimum edge‐cost along this path is `min(6, 6) = 6`. + + * Among the two valid paths, their scores are 5 and 6. Therefore, the answer is 6. + + + + + + +**Constraints:** + + * `n == online.length` + * `2 <= n <= 5 * 104` + * `0 <= m == edges.length <= ``min(105, n * (n - 1) / 2)` + * `edges[i] = [ui, vi, costi]` + * `0 <= ui, vi < n` + * `ui != vi` + * `0 <= costi <= 109` + * `0 <= k <= 5 * 1013` + * `online[i]` is either `true` or `false`, and both `online[0]` and `online[n − 1]` are `true`. + * The given graph is a directed acyclic graph. diff --git a/problems/3620-network-recovery-pathways/solution.go b/problems/3620-network-recovery-pathways/solution.go new file mode 100644 index 0000000..155868d --- /dev/null +++ b/problems/3620-network-recovery-pathways/solution.go @@ -0,0 +1,112 @@ +package main + +// 3620. Network Recovery Pathways +// +// We want the largest "bottleneck" score x such that there is a path from +// node 0 to node n-1 where every edge costs at least x, all intermediate +// nodes are online, and the total edge cost is at most k. +// +// Approach: binary search on the answer x (over the sorted distinct edge +// costs). For a fixed x, keep only edges with cost >= x and ask whether the +// cheapest 0 -> n-1 path costs at most k. Because the graph is a DAG, a single +// pass in topological order computes those minimum costs. Feasibility is +// monotonic in x, so binary search finds the largest feasible x, or -1. +// +// Time: O((n + m) log m) Space: O(n + m) + +import "sort" + +type edge struct { + to int + cost int64 +} + +func findMaxPathScore(edges [][]int, online []bool, k int64) int64 { + n := len(online) + + // Build adjacency list and in-degrees for topological sorting. + adj := make([][]edge, n) + indeg := make([]int, n) + costSet := make(map[int64]struct{}) + for _, e := range edges { + u, v, c := e[0], e[1], int64(e[2]) + adj[u] = append(adj[u], edge{to: v, cost: c}) + indeg[v]++ + costSet[c] = struct{}{} + } + + if len(costSet) == 0 { + return -1 // no edges => no path + } + + // Kahn's algorithm: fixed topological order of the DAG. + topo := make([]int, 0, n) + queue := make([]int, 0, n) + for i := 0; i < n; i++ { + if indeg[i] == 0 { + queue = append(queue, i) + } + } + deg := make([]int, n) + copy(deg, indeg) + for len(queue) > 0 { + u := queue[0] + queue = queue[1:] + topo = append(topo, u) + for _, e := range adj[u] { + deg[e.to]-- + if deg[e.to] == 0 { + queue = append(queue, e.to) + } + } + } + + // Distinct edge costs, ascending: the only candidate scores. + costs := make([]int64, 0, len(costSet)) + for c := range costSet { + costs = append(costs, c) + } + sort.Slice(costs, func(i, j int) bool { return costs[i] < costs[j] }) + + const inf = int64(1) << 62 + dist := make([]int64, n) + + // feasible reports whether some 0 -> n-1 path using only edges with + // cost >= x has total cost <= k. + feasible := func(x int64) bool { + for i := range dist { + dist[i] = inf + } + dist[0] = 0 + for _, u := range topo { + if dist[u] == inf || !online[u] { + // Unreachable, or an offline node we may not route through. + continue + } + du := dist[u] + for _, e := range adj[u] { + if e.cost < x { + continue + } + if nd := du + e.cost; nd < dist[e.to] { + dist[e.to] = nd + } + } + } + return dist[n-1] != inf && dist[n-1] <= k + } + + // Largest candidate cost that stays feasible. + ans := int64(-1) + lo, hi := 0, len(costs)-1 + for lo <= hi { + mid := (lo + hi) / 2 + if feasible(costs[mid]) { + ans = costs[mid] + lo = mid + 1 + } else { + hi = mid - 1 + } + } + return ans +} diff --git a/problems/3620-network-recovery-pathways/solution_test.go b/problems/3620-network-recovery-pathways/solution_test.go new file mode 100644 index 0000000..5de86d8 --- /dev/null +++ b/problems/3620-network-recovery-pathways/solution_test.go @@ -0,0 +1,93 @@ +package main + +import "testing" + +func TestSolution(t *testing.T) { + tests := []struct { + name string + edges [][]int + online []bool + k int64 + expected int64 + }{ + { + name: "example 1: two routes, cheaper one wins the bottleneck", + edges: [][]int{{0, 1, 5}, {1, 3, 10}, {0, 2, 3}, {2, 3, 4}}, + online: []bool{true, true, true, true}, + k: 10, + expected: 3, + }, + { + name: "example 2: offline node forces a higher-bottleneck path", + edges: [][]int{{0, 1, 7}, {1, 4, 5}, {0, 2, 6}, {2, 3, 6}, {3, 4, 2}, {2, 4, 6}}, + online: []bool{true, true, true, false, true}, + k: 12, + expected: 6, + }, + { + name: "edge case: no edges at all -> -1", + edges: [][]int{}, + online: []bool{true, true}, + k: 100, + expected: -1, + }, + { + name: "edge case: budget too small for any path -> -1", + edges: [][]int{{0, 1, 5}, {1, 2, 5}}, + online: []bool{true, true, true}, + k: 4, + expected: -1, + }, + { + name: "edge case: direct edge within budget", + edges: [][]int{{0, 1, 10}}, + online: []bool{true, true}, + k: 10, + expected: 10, + }, + { + name: "edge case: direct edge just over budget -> -1", + edges: [][]int{{0, 1, 10}}, + online: []bool{true, true}, + k: 9, + expected: -1, + }, + { + name: "edge case: zero-cost edge with k = 0", + edges: [][]int{{0, 1, 0}}, + online: []bool{true, true}, + k: 0, + expected: 0, + }, + { + name: "edge case: only online detour is available", + edges: [][]int{{0, 1, 8}, {1, 3, 8}, {0, 2, 2}, {2, 3, 2}}, + online: []bool{true, false, true, true}, + k: 20, + expected: 2, + }, + { + name: "edge case: source and sink disconnected -> -1", + edges: [][]int{{0, 1, 5}}, + online: []bool{true, true, true}, + k: 1000, + expected: -1, + }, + { + name: "edge case: large costs need 64-bit arithmetic", + edges: [][]int{{0, 1, 1000000000}, {1, 2, 1000000000}}, + online: []bool{true, true, true}, + k: 2000000000, + expected: 1000000000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := findMaxPathScore(tt.edges, tt.online, tt.k) + if result != tt.expected { + t.Errorf("findMaxPathScore() = %v, want %v", result, tt.expected) + } + }) + } +}