3275. K-th Nearest Obstacle Queries
Problem Description
You have an infinite 2D plane where you'll be placing obstacles one by one. Given a positive integer k
and a 2D array queries
, you need to track the k-th nearest obstacle to the origin after each placement.
Each query queries[i] = [x, y]
represents placing an obstacle at coordinate (x, y)
. The distance from any obstacle to the origin is calculated using Manhattan distance: |x| + |y|
.
After placing each obstacle, you need to determine:
- If there are at least
k
obstacles on the plane, find the distance of the k-th nearest obstacle from the origin - If there are fewer than
k
obstacles, return-1
The function should return an array results
where results[i]
contains the k-th nearest obstacle distance after processing the i-th query.
Example walkthrough:
- If
k = 2
and queries are[[1,2], [3,4], [2,3]]
- After 1st query: Only 1 obstacle at (1,2) with distance 3. Since we need 2 obstacles, return
-1
- After 2nd query: 2 obstacles with distances 3 and 7. The 2nd nearest has distance 7
- After 3rd query: 3 obstacles with distances 3, 5, and 7. The 2nd nearest has distance 5
The solution uses a max-heap of size k
to efficiently track the k nearest obstacles. As new obstacles are added, if the heap exceeds size k
, the farthest obstacle (top of max-heap) is removed, ensuring the heap always contains the k nearest obstacles.
Intuition
The key insight is recognizing that we need to efficiently maintain and query the k-th smallest distance as we add obstacles one by one.
When we think about finding the k-th smallest element in a dynamic dataset (where elements are continuously added), a heap data structure naturally comes to mind. But which type of heap should we use?
If we used a min-heap to store all obstacles, we'd need to extract elements k times to find the k-th nearest, which would be inefficient for repeated queries. Instead, we can flip our perspective: rather than tracking all obstacles and finding the k-th smallest, we can maintain only the k nearest obstacles at any time.
This leads us to use a max-heap with a size limit of k
. Here's why this works beautifully:
- The max-heap stores at most
k
elements (the k nearest obstacles) - The root of the max-heap is always the largest among these k elements, which is exactly the k-th nearest obstacle
- When a new obstacle comes in, we add it to the heap
- If the heap size exceeds
k
, we remove the largest element (the farthest among the k+1 obstacles) - This ensures we always keep exactly the k nearest obstacles
The elegance of this approach is that we get O(1) access to the k-th nearest distance (it's always at the root), and we only need O(log k) time to process each new obstacle, regardless of how many total obstacles we've placed.
Since Python's heapq
implements a min-heap, we store negative distances to simulate a max-heap behavior - the smallest negative value (most negative) represents the largest positive distance.
Learn more about Heap (Priority Queue) patterns.
Solution Approach
The solution implements a max-heap strategy using Python's heapq
module to maintain the k nearest obstacles.
Implementation walkthrough:
-
Initialize data structures:
ans = []
: Stores the result for each querypq = []
: Priority queue (max-heap) to maintain k nearest obstacles
-
Process each query:
for i, (x, y) in enumerate(queries):
We iterate through each obstacle placement with its index.
-
Calculate distance and add to heap:
heappush(pq, -(abs(x) + abs(y)))
- Calculate Manhattan distance:
|x| + |y|
- Push the negative of the distance to simulate max-heap behavior (since Python's heapq is a min-heap)
- The most negative value (smallest in min-heap) represents the largest positive distance
- Calculate Manhattan distance:
-
Maintain heap size:
if i >= k: heappop(pq)
- Once we have more than
k
obstacles (wheni >= k
), remove the largest distance - This keeps exactly
k
nearest obstacles in the heap
- Once we have more than
-
Determine the k-th nearest distance:
ans.append(-pq[0] if i >= k - 1 else -1)
- If we have at least
k
obstacles (i >= k - 1
), the root of the max-heap contains the k-th nearest distance - Convert back to positive by negating:
-pq[0]
- Otherwise, append
-1
as we don't have enough obstacles yet
- If we have at least
Time Complexity: O(n log k) where n is the number of queries
- Each insertion: O(log k)
- Each deletion: O(log k)
- Getting the k-th element: O(1)
Space Complexity: O(k) for the heap storage
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with k = 3
and queries = [[5,5], [4,4], [3,3], [2,2], [1,1]]
.
We'll maintain a max-heap (using negative values) that stores at most 3 distances.
Initial state:
ans = []
pq = []
(empty heap)
Query 0: [5,5]
- Distance = |5| + |5| = 10
- Push -10 to heap:
pq = [-10]
- Check: i=0, k=3, so i < k-1 (0 < 2)
- Not enough obstacles yet, append -1
ans = [-1]
Query 1: [4,4]
- Distance = |4| + |4| = 8
- Push -8 to heap:
pq = [-8, -10]
(heap maintains order) - Check: i=1, k=3, so i < k-1 (1 < 2)
- Still not enough obstacles, append -1
ans = [-1, -1]
Query 2: [3,3]
- Distance = |3| + |3| = 6
- Push -6 to heap:
pq = [-6, -10, -8]
(root is -6, the smallest negative = largest positive) - Check: i=2, k=3, so i = k-1 (2 = 2)
- We have exactly k obstacles! The k-th nearest is -pq[0] = -(-6) = 10
ans = [-1, -1, 10]
Query 3: [2,2]
- Distance = |2| + |2| = 4
- Push -4 to heap:
pq = [-4, -6, -8, -10]
- Check: i=3, k=3, so i >= k (3 >= 3)
- Heap has 4 elements, pop the largest (root): removed -4,
pq = [-6, -10, -8]
- The k-th nearest is -pq[0] = -(-6) = 8
ans = [-1, -1, 10, 8]
Query 4: [1,1]
- Distance = |1| + |1| = 2
- Push -2 to heap:
pq = [-2, -6, -8, -10]
- Check: i=4, k=3, so i >= k (4 >= 3)
- Heap has 4 elements, pop the largest (root): removed -2,
pq = [-6, -10, -8]
- The k-th nearest is -pq[0] = -(-6) = 6
ans = [-1, -1, 10, 8, 6]
Key observations:
- The heap always maintains the k smallest distances (as negative values)
- The root of our max-heap (most negative value) represents the k-th smallest distance
- Once we exceed k elements, we remove the largest distance to keep only k nearest obstacles
- The final result shows how the k-th nearest distance decreases as we add closer obstacles: 10 → 8 → 6
Solution Implementation
1from typing import List
2import heapq
3
4class Solution:
5 def resultsArray(self, queries: List[List[int]], k: int) -> List[int]:
6 # Initialize result list to store answers for each query
7 result = []
8
9 # Max heap to maintain k smallest Manhattan distances
10 # Using negative values to simulate max heap with Python's min heap
11 max_heap = []
12
13 # Process each query point
14 for index, (x_coord, y_coord) in enumerate(queries):
15 # Calculate Manhattan distance from origin and push to heap
16 # Negative value for max heap simulation
17 manhattan_distance = abs(x_coord) + abs(y_coord)
18 heapq.heappush(max_heap, -manhattan_distance)
19
20 # Maintain heap size of k by removing largest element
21 # This keeps the k smallest distances
22 if index >= k:
23 heapq.heappop(max_heap)
24
25 # Add result: kth smallest distance if we have at least k points,
26 # otherwise -1
27 if index >= k - 1:
28 # Return the kth smallest distance (negate to get positive value)
29 result.append(-max_heap[0])
30 else:
31 # Not enough points yet
32 result.append(-1)
33
34 return result
35
1class Solution {
2 public int[] resultsArray(int[][] queries, int k) {
3 int queryCount = queries.length;
4 int[] result = new int[queryCount];
5
6 // Max heap to maintain the k smallest Manhattan distances
7 // Using reverse order to create a max heap (largest element at top)
8 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
9
10 for (int i = 0; i < queryCount; i++) {
11 // Calculate Manhattan distance from origin (0, 0)
12 int manhattanDistance = Math.abs(queries[i][0]) + Math.abs(queries[i][1]);
13
14 // Add current distance to the heap
15 maxHeap.offer(manhattanDistance);
16
17 // Maintain heap size of at most k elements
18 // When size exceeds k, remove the largest (furthest) distance
19 if (i >= k) {
20 maxHeap.poll();
21 }
22
23 // After processing at least k queries, the heap contains k smallest distances
24 // The top of max heap is the k-th smallest distance
25 // Otherwise, we don't have k elements yet, return -1
26 result[i] = (i >= k - 1) ? maxHeap.peek() : -1;
27 }
28
29 return result;
30 }
31}
32
1class Solution {
2public:
3 vector<int> resultsArray(vector<vector<int>>& queries, int k) {
4 vector<int> results;
5
6 // Max heap to maintain the k smallest Manhattan distances
7 // By using a max heap of size k, the top element is always the k-th smallest
8 priority_queue<int> maxHeap;
9
10 // Process each query point
11 for (const auto& query : queries) {
12 // Calculate Manhattan distance from origin (0, 0)
13 int manhattanDistance = abs(query[0]) + abs(query[1]);
14
15 // Add the distance to the heap
16 maxHeap.push(manhattanDistance);
17
18 // Maintain heap size at most k by removing the largest element
19 if (maxHeap.size() > k) {
20 maxHeap.pop();
21 }
22
23 // If we have exactly k elements, the top is the k-th smallest distance
24 // Otherwise, we don't have enough points yet, so return -1
25 if (maxHeap.size() == k) {
26 results.push_back(maxHeap.top());
27 } else {
28 results.push_back(-1);
29 }
30 }
31
32 return results;
33 }
34};
35
1/**
2 * Finds the k-th smallest Manhattan distance for each query point
3 * @param queries - Array of [x, y] coordinate pairs
4 * @param k - The k-th smallest distance to track
5 * @returns Array where each element is the k-th smallest distance after processing each query, or -1 if less than k points
6 */
7function resultsArray(queries: number[][], k: number): number[] {
8 // Max heap to maintain the k smallest Manhattan distances
9 const maxHeap = new MaxPriorityQueue();
10
11 // Result array to store the k-th smallest distance after each query
12 const result: number[] = [];
13
14 // Process each query point
15 for (const [x, y] of queries) {
16 // Calculate Manhattan distance from origin
17 const manhattanDistance: number = Math.abs(x) + Math.abs(y);
18
19 // Add current distance to the heap
20 maxHeap.enqueue(manhattanDistance);
21
22 // Maintain heap size of at most k elements
23 // If size exceeds k, remove the largest element
24 if (maxHeap.size() > k) {
25 maxHeap.dequeue();
26 }
27
28 // If we have exactly k elements, the root is the k-th smallest
29 // Otherwise, we don't have enough points yet, so return -1
30 const kthSmallest: number = maxHeap.size() === k ? maxHeap.front().element : -1;
31 result.push(kthSmallest);
32 }
33
34 return result;
35}
36
Time and Space Complexity
The time complexity is O(n × log k)
, where n
is the length of the queries array. This is because:
- The code iterates through all
n
queries once - For each query, it performs a
heappush
operation which takesO(log k)
time since the heap size is maintained at mostk
elements - When
i >= k
, aheappop
operation is performed which also takesO(log k)
time - Accessing the top element of the heap with
pq[0]
takesO(1)
time
The space complexity is O(k)
. This is because:
- The heap
pq
stores at mostk
elements at any given time (elements are popped when the size exceedsk
) - The answer list
ans
storesn
elements, but this is typically considered as output space and not counted in auxiliary space complexity - Other variables like
i
,x
,y
useO(1)
space
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusion with Heap Size Management
A common mistake is incorrectly managing when to pop from the heap. Developers often confuse the condition i >= k
with i > k
or use len(max_heap) > k
instead.
Incorrect approach:
# Wrong: This pops too early
if i > k:
heapq.heappop(max_heap)
# Or wrong: This doesn't account for 0-based indexing
if len(max_heap) > k:
heapq.heappop(max_heap)
Why it's wrong: Using i > k
means we start popping from the (k+2)th element instead of the (k+1)th element. This keeps k+1 elements in the heap instead of k.
Correct approach:
# Right: Start popping when we have more than k elements if i >= k: heapq.heappop(max_heap)
2. Forgetting to Negate Values for Max-Heap Simulation
Python's heapq only provides min-heap functionality. A common error is forgetting to negate values when pushing or retrieving from the heap.
Incorrect approach:
# Wrong: Treating it as a natural max-heap
heapq.heappush(max_heap, abs(x_coord) + abs(y_coord))
result.append(max_heap[0] if i >= k - 1 else -1)
Why it's wrong: This creates a min-heap, so max_heap[0]
would be the smallest distance, not the k-th smallest.
Correct approach:
# Push negative values to simulate max-heap
heapq.heappush(max_heap, -(abs(x_coord) + abs(y_coord)))
# Negate when retrieving to get positive distance
result.append(-max_heap[0] if i >= k - 1 else -1)
3. Off-by-One Error in Result Condition
Determining when we have exactly k elements is tricky due to 0-based indexing.
Incorrect approach:
# Wrong: This waits one query too long if i >= k: result.append(-max_heap[0]) else: result.append(-1)
Why it's wrong: When i = k-1
, we already have k elements (indices 0 to k-1), but this code would still return -1.
Correct approach:
# Right: We have k elements when index reaches k-1 if i >= k - 1: result.append(-max_heap[0]) else: result.append(-1)
4. Using Set to Track Duplicates
Some might think duplicate coordinates need special handling and try to use a set.
Incorrect approach:
seen = set()
for x, y in queries:
if (x, y) not in seen:
seen.add((x, y))
heapq.heappush(max_heap, -(abs(x) + abs(y)))
Why it's wrong: The problem asks for the k-th nearest obstacle after each placement. Multiple obstacles at the same location or with the same distance are valid and should all be counted.
Correct approach: Treat each query as a separate obstacle, even if it has the same coordinates or distance as a previous one.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!