diff --git a/problems/3534-path-existence-queries-in-a-graph-ii/analysis.md b/problems/3534-path-existence-queries-in-a-graph-ii/analysis.md new file mode 100644 index 0000000..10292e6 --- /dev/null +++ b/problems/3534-path-existence-queries-in-a-graph-ii/analysis.md @@ -0,0 +1,110 @@ +# 3534. Path Existence Queries in a Graph II + +[LeetCode Link](https://leetcode.com/problems/path-existence-queries-in-a-graph-ii/) + +Difficulty: Hard +Topics: Array, Two Pointers, Binary Search, Dynamic Programming, Greedy, Bit Manipulation, Graph Theory, Sorting +Acceptance Rate: 44.4% + +This is a genuinely tough problem — it combines a clever graph observation with an +efficient way to answer many shortest-path queries. Don't be discouraged if the +optimal solution doesn't appear immediately; the payoff is a very reusable pattern +(sorting + greedy reach + binary lifting). Take it hint by hint. + +## Hints + +### Hint 1 + +The edge rule only depends on the *values* `nums[i]`, not on the node labels. An edge +connects two nodes whenever their values differ by at most `maxDiff`. What happens if +you stop thinking about the nodes in their original order and instead **sort them by +value**? Two nodes that are far apart in the label order but close in value are +neighbors — sorting makes that structure visible. + +### Hint 2 + +Once the values are sorted, notice that a node connects to a *contiguous window* of +the sorted array: everything whose value lies within `maxDiff` of it. That immediately +tells you two things. First, connectivity: two sorted-adjacent nodes are directly +connected exactly when their gap is `<= maxDiff`, so the components are maximal runs of +sorted values with no gap larger than `maxDiff`. Second, movement: from any node the +*best* single move toward a target on its right is to jump to the **farthest** node +still within `maxDiff`. Think about how to precompute that farthest reach for every +position (hint: two pointers, because the reach only moves rightward as you advance). + +### Hint 3 + +The shortest path between two nodes in the same component reduces to: "starting at the +left one, how few greedy 'jump as far right as you can' moves reach the right one?" +That is a classic *minimum-jumps* problem. Doing it naively per query is too slow for +`10^5` queries, but the greedy jump is a fixed function `far[i]` — so you can build a +**binary-lifting** table: `up[k][i]` = where you land after `2^k` greedy jumps. Then any +query counts the jumps in `O(log n)` by descending powers of two: take a `2^k` jump +whenever it still leaves you strictly left of the target, then add one final jump. + +## Approach + +**Step 1 — Sort by value.** Build `order`, the node indices sorted by `nums`. Record +`sortedVals[r]` = the value at sorted rank `r`, and `pos[node]` = the sorted rank of an +original node so we can translate queries into sorted-space positions. + +**Step 2 — Components.** Walk the sorted array left to right. Start component `0`; each +time `sortedVals[i] - sortedVals[i-1] > maxDiff`, increment the component id. Two nodes +can reach each other **iff** they share a component. This works because within a run +where every adjacent gap is `<= maxDiff`, each node connects to its sorted neighbor, so +the whole run is connected; and a gap larger than `maxDiff` is an unbridgeable wall (no +node on one side is within `maxDiff` of any node on the other side, since values are +sorted). + +**Step 3 — Greedy farthest reach.** For each sorted position `i`, `far[i]` is the +largest index `j` with `sortedVals[j] - sortedVals[i] <= maxDiff`. Because `far` is +non-decreasing in `i`, a two-pointer sweep computes all of them in `O(n)`. From node +`i`, one hop can land anywhere in `[left..far[i]]`; to travel rightward as fast as +possible, always aim for `far[i]`. + +**Step 4 — Binary lifting.** Set `up[0] = far`. Then `up[k][i] = up[k-1][up[k-1][i]]` +means "apply `2^(k-1)` greedy jumps twice." Build `~log2(n)` levels. + +**Step 5 — Answer queries.** For query `(u, v)`: +- Let `pu = pos[u]`, `pv = pos[v]`. If `pu == pv`, the answer is `0`. +- If `comp[pu] != comp[pv]`, they're disconnected → `-1`. +- Otherwise let `l = min(pu, pv)`, `r = max(pu, pv)`. Starting at `cur = l` with + `steps = 0`, iterate `k` from high to low: if `up[k][cur] < r`, take that jump + (`cur = up[k][cur]`, `steps += 2^k`). After the loop, one more greedy jump from `cur` + reaches `r` (because it landed as far as possible while still strictly left of `r`), + so the answer is `steps + 1`. + +**Worked micro-example** (Example 2): `nums = [5,3,1,9,10]`, `maxDiff = 2`. Sorted +values are `[1,3,5,9,10]`. Gaps `2,2,4,1`, so the `4` splits things into components +`{1,3,5}` and `{9,10}`. `far` in sorted space is `[1,2,2,4,4]`. Query `[0,2]` maps to +sorted positions `2` and `0` → `l=0, r=2`: from `0` jump to `far[0]=1` (`steps=1`), +then one final jump reaches `2` → answer `2`. Query `[2,3]` crosses the component wall +→ `-1`. + +## Complexity Analysis + +Let `n = len(nums)`, `q = len(queries)`. + +Time Complexity: `O(n log n + q log n)` — sorting is `O(n log n)`; the two-pointer reach +and component scan are `O(n)`; the binary-lifting table is `O(n log n)` to build; each +query costs `O(log n)`. + +Space Complexity: `O(n log n)` for the binary-lifting table (plus `O(n)` for the sorted +arrays, components, and reach). + +## Edge Cases + +- **Self query (`u == v`)**: distance is `0`. Handled before any component/jump logic; + also covers Example 3's `[0,0]`. +- **Disconnected pair**: different components → `-1` (Example 2's `[2,3]`, + Example 3's `[0,1]` and `[1,2]`). +- **`maxDiff == 0`**: only nodes with *identical* values connect. The component scan + naturally handles this since any positive gap exceeds `0`. +- **All nodes isolated**: every adjacent gap exceeds `maxDiff`, so each node is its own + component; every cross-node query returns `-1`. +- **Single node (`n == 1`)**: the only valid query is `[0,0]` → `0`; the binary-lifting + table must still be sized safely (at least one level) — guard the `LOG` computation. +- **Duplicate values**: they form zero-gap edges and belong to the same component; + the sort keeps them adjacent so nothing special is needed. +- **Long chains**: e.g. values `1,2,3,4,5` with `maxDiff = 1` require `n-1` hops end to + end — this is exactly what binary lifting keeps fast. diff --git a/problems/3534-path-existence-queries-in-a-graph-ii/problem.md b/problems/3534-path-existence-queries-in-a-graph-ii/problem.md new file mode 100644 index 0000000..5a1b6bb --- /dev/null +++ b/problems/3534-path-existence-queries-in-a-graph-ii/problem.md @@ -0,0 +1,107 @@ +--- +number: "3534" +frontend_id: "3534" +title: "Path Existence Queries in a Graph II" +slug: "path-existence-queries-in-a-graph-ii" +difficulty: "Hard" +topics: + - "Array" + - "Two Pointers" + - "Binary Search" + - "Dynamic Programming" + - "Greedy" + - "Bit Manipulation" + - "Graph Theory" + - "Sorting" +acceptance_rate: 4443.3 +is_premium: false +created_at: "2026-07-10T04:31:13.391651+00:00" +fetched_at: "2026-07-10T04:31:13.391651+00:00" +link: "https://leetcode.com/problems/path-existence-queries-in-a-graph-ii/" +date: "2026-07-10" +--- + +# 3534. Path Existence Queries in a Graph II + +You are given an integer `n` representing the number of nodes in a graph, labeled from 0 to `n - 1`. + +You are also given an integer array `nums` of length `n` and an integer `maxDiff`. + +An **undirected** edge exists between nodes `i` and `j` if the **absolute** difference between `nums[i]` and `nums[j]` is **at most** `maxDiff` (i.e., `|nums[i] - nums[j]| <= maxDiff`). + +You are also given a 2D integer array `queries`. For each `queries[i] = [ui, vi]`, find the **minimum** distance between nodes `ui` and `vi`. If no path exists between the two nodes, return -1 for that query. + +Return an array `answer`, where `answer[i]` is the result of the `ith` query. + +**Note:** The edges between the nodes are unweighted. + + + +**Example 1:** + +**Input:** n = 5, nums = [1,8,3,4,2], maxDiff = 3, queries = [[0,3],[2,4]] + +**Output:** [1,1] + +**Explanation:** + +The resulting graph is: + +![](https://assets.leetcode.com/uploads/2025/03/25/4149example1drawio.png) + +Query | Shortest Path | Minimum Distance +---|---|--- +[0, 3] | 0 -> 3 | 1 +[2, 4] | 2 -> 4 | 1 + +Thus, the output is `[1, 1]`. + +**Example 2:** + +**Input:** n = 5, nums = [5,3,1,9,10], maxDiff = 2, queries = [[0,1],[0,2],[2,3],[4,3]] + +**Output:** [1,2,-1,1] + +**Explanation:** + +The resulting graph is: + +![](https://assets.leetcode.com/uploads/2025/03/25/4149example2drawio.png) + +Query | Shortest Path | Minimum Distance +---|---|--- +[0, 1] | 0 -> 1 | 1 +[0, 2] | 0 -> 1 -> 2 | 2 +[2, 3] | None | -1 +[4, 3] | 3 -> 4 | 1 + +Thus, the output is `[1, 2, -1, 1]`. + +**Example 3:** + +**Input:** n = 3, nums = [3,6,1], maxDiff = 1, queries = [[0,0],[0,1],[1,2]] + +**Output:** [0,-1,-1] + +**Explanation:** + +There are no edges between any two nodes because: + + * Nodes 0 and 1: `|nums[0] - nums[1]| = |3 - 6| = 3 > 1` + * Nodes 0 and 2: `|nums[0] - nums[2]| = |3 - 1| = 2 > 1` + * Nodes 1 and 2: `|nums[1] - nums[2]| = |6 - 1| = 5 > 1` + + + +Thus, no node can reach any other node, and the output is `[0, -1, -1]`. + + + +**Constraints:** + + * `1 <= n == nums.length <= 105` + * `0 <= nums[i] <= 105` + * `0 <= maxDiff <= 105` + * `1 <= queries.length <= 105` + * `queries[i] == [ui, vi]` + * `0 <= ui, vi < n` diff --git a/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710.go b/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710.go new file mode 100644 index 0000000..faf1f3d --- /dev/null +++ b/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710.go @@ -0,0 +1,100 @@ +package main + +import "sort" + +// Path Existence Queries in a Graph II +// +// Edges depend only on values: i and j are connected when |nums[i]-nums[j]| <= maxDiff. +// Sorting nodes by value exposes the structure: +// - Connected components are maximal runs of the sorted values whose consecutive +// gaps are all <= maxDiff. Two nodes are reachable iff they share a component. +// - From a sorted position i, the greedy best single hop toward the right lands at +// far[i] = farthest index still within maxDiff (computed with two pointers). +// The shortest path between two same-component nodes is the minimum number of greedy +// jumps from the left one to the right one, answered in O(log n) per query using a +// binary-lifting table over far. +// +// Time: O(n log n + q log n) +// Space: O(n log n) +func pathExistenceQueries(n int, nums []int, maxDiff int, queries [][]int) []int { + // Sort node indices by their value. + order := make([]int, n) + for i := range order { + order[i] = i + } + sort.Slice(order, func(a, b int) bool { + return nums[order[a]] < nums[order[b]] + }) + + sortedVals := make([]int, n) // value at each sorted rank + pos := make([]int, n) // pos[node] = sorted rank of original node + for rank, node := range order { + sortedVals[rank] = nums[node] + pos[node] = rank + } + + // Component id per sorted rank: a gap larger than maxDiff starts a new component. + comp := make([]int, n) + for i := 1; i < n; i++ { + comp[i] = comp[i-1] + if sortedVals[i]-sortedVals[i-1] > maxDiff { + comp[i]++ + } + } + + // far[i] = farthest sorted index reachable from i in one hop (rightward). + // far is non-decreasing, so a single two-pointer sweep suffices. + far := make([]int, n) + j := 0 + for i := 0; i < n; i++ { + if j < i { + j = i + } + for j+1 < n && sortedVals[j+1]-sortedVals[i] <= maxDiff { + j++ + } + far[i] = j + } + + // Binary lifting: up[k][i] = position after 2^k greedy jumps from i. + LOG := 1 + for (1 << LOG) < n { + LOG++ + } + LOG++ // one extra level for safety (also keeps LOG >= 2 when n == 1) + up := make([][]int, LOG) + up[0] = far + for k := 1; k < LOG; k++ { + up[k] = make([]int, n) + prev := up[k-1] + for i := 0; i < n; i++ { + up[k][i] = prev[prev[i]] + } + } + + ans := make([]int, len(queries)) + for qi, q := range queries { + pu, pv := pos[q[0]], pos[q[1]] + if pu == pv { + ans[qi] = 0 + continue + } + if comp[pu] != comp[pv] { + ans[qi] = -1 + continue + } + l, r := pu, pv + if l > r { + l, r = r, l + } + cur, steps := l, 0 + for k := LOG - 1; k >= 0; k-- { + if up[k][cur] < r { + cur = up[k][cur] + steps += 1 << k + } + } + ans[qi] = steps + 1 // one final greedy jump reaches r + } + return ans +} diff --git a/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710_test.go b/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710_test.go new file mode 100644 index 0000000..860c307 --- /dev/null +++ b/problems/3534-path-existence-queries-in-a-graph-ii/solution_daily_20260710_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestPathExistenceQueries(t *testing.T) { + tests := []struct { + name string + n int + nums []int + maxDiff int + queries [][]int + expected []int + }{ + { + name: "example 1: two direct-edge queries", + n: 5, + nums: []int{1, 8, 3, 4, 2}, + maxDiff: 3, + queries: [][]int{{0, 3}, {2, 4}}, + expected: []int{1, 1}, + }, + { + name: "example 2: mixed distances and a disconnected pair", + n: 5, + nums: []int{5, 3, 1, 9, 10}, + maxDiff: 2, + queries: [][]int{{0, 1}, {0, 2}, {2, 3}, {4, 3}}, + expected: []int{1, 2, -1, 1}, + }, + { + name: "example 3: fully disconnected graph with a self query", + n: 3, + nums: []int{3, 6, 1}, + maxDiff: 1, + queries: [][]int{{0, 0}, {0, 1}, {1, 2}}, + expected: []int{0, -1, -1}, + }, + { + name: "edge case: single node, only a self query", + n: 1, + nums: []int{7}, + maxDiff: 5, + queries: [][]int{{0, 0}}, + expected: []int{0}, + }, + { + name: "edge case: maxDiff zero connects only equal values", + n: 4, + nums: []int{2, 2, 3, 2}, + maxDiff: 0, + queries: [][]int{{0, 1}, {0, 3}, {0, 2}, {2, 2}}, + expected: []int{1, 1, -1, 0}, + }, + { + name: "edge case: long chain needs many hops", + n: 5, + nums: []int{1, 2, 3, 4, 5}, + maxDiff: 1, + queries: [][]int{{0, 4}, {0, 3}, {1, 4}, {2, 2}}, + expected: []int{4, 3, 3, 0}, + }, + { + name: "edge case: everything within reach in one hop", + n: 4, + nums: []int{10, 11, 12, 13}, + maxDiff: 100, + queries: [][]int{{0, 3}, {1, 2}, {3, 0}}, + expected: []int{1, 1, 1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pathExistenceQueries(tt.n, tt.nums, tt.maxDiff, tt.queries) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("pathExistenceQueries(%d, %v, %d, %v) = %v, want %v", + tt.n, tt.nums, tt.maxDiff, tt.queries, got, tt.expected) + } + }) + } +}