Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions problems/3534-path-existence-queries-in-a-graph-ii/analysis.md
Original file line number Diff line number Diff line change
@@ -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.
107 changes: 107 additions & 0 deletions problems/3534-path-existence-queries-in-a-graph-ii/problem.md
Original file line number Diff line number Diff line change
@@ -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`
Original file line number Diff line number Diff line change
@@ -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
}
Loading