Facebook Pixel

3102. Minimize Manhattan Distances

HardGeometryArrayMathOrdered SetSorting
Leetcode Link

Problem Description

You are given an array points containing integer coordinates of points on a 2D plane, where each point is represented as points[i] = [xi, yi].

The distance between any two points is calculated using the Manhattan distance formula: |x1 - x2| + |y1 - y2|.

Your task is to find the minimum possible value of the maximum distance between any two points after removing exactly one point from the array.

In other words:

  1. You must remove exactly one point from the given set of points
  2. After removal, calculate all pairwise Manhattan distances between the remaining points
  3. Find the maximum distance among all these pairs
  4. Return the minimum possible value of this maximum distance across all possible choices of which point to remove

For example, if you have points A, B, C, and D, you would:

  • Try removing A, then find the max distance among pairs (B,C), (B,D), (C,D)
  • Try removing B, then find the max distance among pairs (A,C), (A,D), (C,D)
  • Try removing C, then find the max distance among pairs (A,B), (A,D), (B,D)
  • Try removing D, then find the max distance among pairs (A,B), (A,C), (B,C)
  • Return the minimum of all these maximum distances
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that calculating Manhattan distances between all pairs of points after removing each point would be inefficient. Instead, we need to understand what determines the maximum Manhattan distance in a set of points.

The Manhattan distance |x1 - x2| + |y1 - y2| can be rewritten by considering all possible sign combinations. Through algebraic manipulation, we can express this as:

max((x1 + y1) - (x2 + y2), (x2 + y2) - (x1 + y1), (x1 - y1) - (x2 - y2), (x2 - y2) - (x1 - y1))

This reveals that the Manhattan distance between two points depends on two transformed coordinates:

  • The sum x + y
  • The difference x - y

The maximum Manhattan distance in a set of points will be determined by either:

  • The difference between the maximum and minimum values of x + y across all points
  • The difference between the maximum and minimum values of x - y across all points

This transformation is powerful because instead of comparing all pairs of points, we only need to track the extremes (maximum and minimum) of these two transformed values.

When we remove a point, we're essentially asking: "How does removing this point affect the range of x + y values and the range of x - y values?" If the removed point was one of the extremes, the maximum distance might decrease. If it wasn't an extreme point, removing it won't affect the maximum distance at all.

By maintaining sorted lists of x + y and x - y values for all points, we can efficiently:

  1. Remove a point from both lists
  2. Check the new maximum distance (which is the max of the two ranges)
  3. Add the point back
  4. Try the next point

This approach transforms an O(n³) brute force solution into an O(n log n) solution using ordered sets.

Learn more about Math and Sorting patterns.

Solution Approach

Based on the mathematical transformation discussed, we implement the solution using ordered sets (SortedList in Python) to efficiently maintain and query the extreme values.

Step 1: Initialize Data Structures

We create two SortedLists:

  • sl1: stores x + y values for all points
  • sl2: stores x - y values for all points
sl1 = SortedList()
sl2 = SortedList()
for x, y in points:
    sl1.add(x + y)
    sl2.add(x - y)

Step 2: Try Removing Each Point

For each point in the array, we temporarily remove it and calculate the resulting maximum distance:

ans = inf
for x, y in points:
    # Remove current point from both sorted lists
    sl1.remove(x + y)
    sl2.remove(x - y)
  
    # Calculate max distance without this point
    # Maximum distance is the max of the two ranges
    ans = min(ans, max(sl1[-1] - sl1[0], sl2[-1] - sl2[0]))
  
    # Add the point back for next iteration
    sl1.add(x + y)
    sl2.add(x - y)

Key Operations:

  • sl1[-1] - sl1[0]: gives the range of x + y values (max minus min)
  • sl2[-1] - sl2[0]: gives the range of x - y values (max minus min)
  • The maximum of these two ranges determines the maximum Manhattan distance in the remaining points

Time Complexity Analysis:

  • Initial population of sorted lists: O(n log n)
  • For each of n points:
    • Remove from sorted lists: O(log n)
    • Calculate range: O(1)
    • Add back to sorted lists: O(log n)
  • Total: O(n log n)

Space Complexity: O(n) for storing the two sorted lists

The algorithm efficiently finds the minimum possible maximum distance by leveraging the mathematical property that Manhattan distance can be expressed in terms of x + y and x - y, allowing us to avoid the brute force approach of calculating all pairwise distances.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with a small example to illustrate how the algorithm works.

Given points: [[1, 1], [3, 3], [5, 1]]

Step 1: Calculate transformed coordinates

For each point, we calculate x + y and x - y:

  • Point [1, 1]: x + y = 2, x - y = 0
  • Point [3, 3]: x + y = 6, x - y = 0
  • Point [5, 1]: x + y = 6, x - y = 4

Initialize sorted lists:

  • sl1 (x + y values): [2, 6, 6]
  • sl2 (x - y values): [0, 0, 4]

Step 2: Try removing each point

Remove Point [1, 1]:

  • Remove from sorted lists: sl1 = [6, 6], sl2 = [0, 4]
  • Calculate ranges:
    • Range of x + y: 6 - 6 = 0
    • Range of x - y: 4 - 0 = 4
    • Max distance = max(0, 4) = 4
  • Add point back: sl1 = [2, 6, 6], sl2 = [0, 0, 4]

Remove Point [3, 3]:

  • Remove from sorted lists: sl1 = [2, 6], sl2 = [0, 4]
  • Calculate ranges:
    • Range of x + y: 6 - 2 = 4
    • Range of x - y: 4 - 0 = 4
    • Max distance = max(4, 4) = 4
  • Add point back: sl1 = [2, 6, 6], sl2 = [0, 0, 4]

Remove Point [5, 1]:

  • Remove from sorted lists: sl1 = [2, 6], sl2 = [0, 0]
  • Calculate ranges:
    • Range of x + y: 6 - 2 = 4
    • Range of x - y: 0 - 0 = 0
    • Max distance = max(4, 0) = 4
  • Add point back: sl1 = [2, 6, 6], sl2 = [0, 0, 4]

Step 3: Return minimum result

All removals resulted in a maximum distance of 4, so the answer is 4.

Verification: Let's verify by checking actual Manhattan distances:

  • Without [1, 1]: distance([3, 3], [5, 1]) = |3-5| + |3-1| = 2 + 2 = 4 ✓
  • Without [3, 3]: distance([1, 1], [5, 1]) = |1-5| + |1-1| = 4 + 0 = 4 ✓
  • Without [5, 1]: distance([1, 1], [3, 3]) = |1-3| + |1-3| = 2 + 2 = 4 ✓

The algorithm correctly identifies that removing any point results in the same maximum distance of 4.

Solution Implementation

1class Solution:
2    def minimumDistance(self, points: List[List[int]]) -> int:
3        # Manhattan distance between two points (x1, y1) and (x2, y2) is |x1-x2| + |y1-y2|
4        # This can be rewritten as max(|(x1+y1) - (x2+y2)|, |(x1-y1) - (x2-y2)|)
5        # So we track x+y and x-y values for all points
6      
7        # SortedList to maintain sorted order of x+y values
8        sum_sorted = SortedList()
9        # SortedList to maintain sorted order of x-y values  
10        diff_sorted = SortedList()
11      
12        # Add all points' transformed coordinates to the sorted lists
13        for x, y in points:
14            sum_sorted.add(x + y)
15            diff_sorted.add(x - y)
16      
17        # Initialize answer to infinity
18        min_max_distance = float('inf')
19      
20        # Try removing each point one by one
21        for x, y in points:
22            # Remove current point from both sorted lists
23            sum_sorted.remove(x + y)
24            diff_sorted.remove(x - y)
25          
26            # Calculate maximum Manhattan distance among remaining points
27            # The maximum distance is the max of the ranges in both transformed dimensions
28            max_distance = max(sum_sorted[-1] - sum_sorted[0], 
29                             diff_sorted[-1] - diff_sorted[0])
30          
31            # Update minimum of maximum distances
32            min_max_distance = min(min_max_distance, max_distance)
33          
34            # Add the point back for the next iteration
35            sum_sorted.add(x + y)
36            diff_sorted.add(x - y)
37      
38        return min_max_distance
39
1class Solution {
2    public int minimumDistance(int[][] points) {
3        // Transform coordinates to simplify Manhattan distance calculation
4        // For points (x1,y1) and (x2,y2), Manhattan distance |x1-x2| + |y1-y2|
5        // can be expressed as max(|(x1+y1)-(x2+y2)|, |(x1-y1)-(x2-y2)|)
6      
7        // TreeMaps to maintain sorted order of transformed coordinates
8        // Key: transformed coordinate value, Value: count of points with this value
9        TreeMap<Integer, Integer> sumCoordinates = new TreeMap<>();  // stores x+y values
10        TreeMap<Integer, Integer> diffCoordinates = new TreeMap<>(); // stores x-y values
11      
12        // Initialize both TreeMaps with all points
13        for (int[] point : points) {
14            int xCoord = point[0];
15            int yCoord = point[1];
16          
17            // Add transformed coordinates to maps
18            sumCoordinates.merge(xCoord + yCoord, 1, Integer::sum);
19            diffCoordinates.merge(xCoord - yCoord, 1, Integer::sum);
20        }
21      
22        int minimumMaxDistance = Integer.MAX_VALUE;
23      
24        // Try removing each point and calculate the resulting maximum distance
25        for (int[] point : points) {
26            int xCoord = point[0];
27            int yCoord = point[1];
28          
29            // Remove current point from both maps
30            int sumKey = xCoord + yCoord;
31            if (sumCoordinates.merge(sumKey, -1, Integer::sum) == 0) {
32                sumCoordinates.remove(sumKey);
33            }
34          
35            int diffKey = xCoord - yCoord;
36            if (diffCoordinates.merge(diffKey, -1, Integer::sum) == 0) {
37                diffCoordinates.remove(diffKey);
38            }
39          
40            // Calculate maximum distance with current point removed
41            // Maximum distance is the max of ranges in both transformed coordinate systems
42            int maxDistanceAfterRemoval = Math.max(
43                sumCoordinates.lastKey() - sumCoordinates.firstKey(),
44                diffCoordinates.lastKey() - diffCoordinates.firstKey()
45            );
46          
47            // Update minimum of all maximum distances
48            minimumMaxDistance = Math.min(minimumMaxDistance, maxDistanceAfterRemoval);
49          
50            // Add the point back to both maps for next iteration
51            sumCoordinates.merge(sumKey, 1, Integer::sum);
52            diffCoordinates.merge(diffKey, 1, Integer::sum);
53        }
54      
55        return minimumMaxDistance;
56    }
57}
58
1class Solution {
2public:
3    int minimumDistance(vector<vector<int>>& points) {
4        // Use multisets to maintain sorted sums and differences
5        // For Manhattan distance, we transform coordinates:
6        // dist = max(|x1-x2| + |y1-y2|) = max(|(x1+y1)-(x2+y2)|, |(x1-y1)-(x2-y2)|)
7        multiset<int> sumCoordinates;      // Stores (x + y) for all points
8        multiset<int> diffCoordinates;     // Stores (x - y) for all points
9      
10        // Populate both multisets with transformed coordinates
11        for (const auto& point : points) {
12            int x = point[0];
13            int y = point[1];
14            sumCoordinates.insert(x + y);
15            diffCoordinates.insert(x - y);
16        }
17      
18        int minMaxDistance = INT_MAX;
19      
20        // Try removing each point and calculate the maximum distance
21        for (const auto& point : points) {
22            int x = point[0];
23            int y = point[1];
24          
25            // Remove current point from both multisets
26            sumCoordinates.erase(sumCoordinates.find(x + y));
27            diffCoordinates.erase(diffCoordinates.find(x - y));
28          
29            // Calculate maximum Manhattan distance without current point
30            // Maximum distance is the max of the two ranges
31            int maxSumRange = *sumCoordinates.rbegin() - *sumCoordinates.begin();
32            int maxDiffRange = *diffCoordinates.rbegin() - *diffCoordinates.begin();
33            int currentMaxDistance = max(maxSumRange, maxDiffRange);
34          
35            // Update minimum of all maximum distances
36            minMaxDistance = min(minMaxDistance, currentMaxDistance);
37          
38            // Add the point back for next iteration
39            sumCoordinates.insert(x + y);
40            diffCoordinates.insert(x - y);
41        }
42      
43        return minMaxDistance;
44    }
45};
46
1// Calculate the minimum possible maximum Manhattan distance after removing one point
2function minimumDistance(points: number[][]): number {
3    // Create two multisets to track x+y and x-y values (Manhattan distance components)
4    const sumSet = new TreapMultiSet<number>();
5    const diffSet = new TreapMultiSet<number>();
6  
7    // Add all points to both sets
8    for (const [x, y] of points) {
9        sumSet.add(x + y);
10        diffSet.add(x - y);
11    }
12  
13    let minMaxDistance = Infinity;
14  
15    // Try removing each point and calculate the resulting maximum distance
16    for (const [x, y] of points) {
17        // Temporarily remove the current point
18        sumSet.delete(x + y);
19        diffSet.delete(x - y);
20      
21        // Calculate max distance without this point
22        const maxSum = sumSet.last() - sumSet.first();
23        const maxDiff = diffSet.last() - diffSet.first();
24        minMaxDistance = Math.min(minMaxDistance, Math.max(maxSum, maxDiff));
25      
26        // Add the point back
27        sumSet.add(x + y);
28        diffSet.add(x - y);
29    }
30  
31    return minMaxDistance;
32}
33
34// Type definitions for comparison functions
35type CompareFunction<T, R extends 'number' | 'boolean'> = (
36    a: T,
37    b: T,
38) => R extends 'number' ? number : boolean;
39
40// Interface defining the public API of TreapMultiSet
41interface ITreapMultiSet<T> extends Iterable<T> {
42    add: (...value: T[]) => this;
43    has: (value: T) => boolean;
44    delete: (value: T) => void;
45  
46    bisectLeft: (value: T) => number;
47    bisectRight: (value: T) => number;
48  
49    indexOf: (value: T) => number;
50    lastIndexOf: (value: T) => number;
51  
52    at: (index: number) => T | undefined;
53    first: () => T | undefined;
54    last: () => T | undefined;
55  
56    lower: (value: T) => T | undefined;
57    higher: (value: T) => T | undefined;
58    floor: (value: T) => T | undefined;
59    ceil: (value: T) => T | undefined;
60  
61    shift: () => T | undefined;
62    pop: (index?: number) => T | undefined;
63  
64    count: (value: T) => number;
65  
66    keys: () => IterableIterator<T>;
67    values: () => IterableIterator<T>;
68    rvalues: () => IterableIterator<T>;
69    entries: () => IterableIterator<[number, T]>;
70  
71    readonly size: number;
72}
73
74// Node class for the Treap data structure
75class TreapNode<T = number> {
76    value: T;
77    count: number;      // Number of duplicate values
78    size: number;       // Total size of subtree
79    priority: number;   // Random priority for heap property
80    left: TreapNode<T> | null;
81    right: TreapNode<T> | null;
82  
83    constructor(value: T) {
84        this.value = value;
85        this.count = 1;
86        this.size = 1;
87        this.priority = Math.random();
88        this.left = null;
89        this.right = null;
90    }
91  
92    // Get size of a node (handles null)
93    static getSize(node: TreapNode<any> | null): number {
94        return node?.size ?? 0;
95    }
96  
97    // Get priority of a node (handles null)
98    static getFac(node: TreapNode<any> | null): number {
99        return node?.priority ?? 0;
100    }
101  
102    // Update size based on children
103    pushUp(): void {
104        let totalSize = this.count;
105        totalSize += TreapNode.getSize(this.left);
106        totalSize += TreapNode.getSize(this.right);
107        this.size = totalSize;
108    }
109  
110    // Right rotation for maintaining heap property
111    rotateRight(): TreapNode<T> {
112        let currentNode: TreapNode<T> = this;
113        const leftChild = currentNode.left;
114        currentNode.left = leftChild?.right ?? null;
115        if (leftChild) {
116            leftChild.right = currentNode;
117            currentNode = leftChild;
118        }
119        currentNode.right?.pushUp();
120        currentNode.pushUp();
121        return currentNode;
122    }
123  
124    // Left rotation for maintaining heap property
125    rotateLeft(): TreapNode<T> {
126        let currentNode: TreapNode<T> = this;
127        const rightChild = currentNode.right;
128        currentNode.right = rightChild?.left ?? null;
129        if (rightChild) {
130            rightChild.left = currentNode;
131            currentNode = rightChild;
132        }
133        currentNode.left?.pushUp();
134        currentNode.pushUp();
135        return currentNode;
136    }
137}
138
139// Treap-based multiset implementation for maintaining sorted collections with duplicates
140class TreapMultiSet<T = number> implements ITreapMultiSet<T> {
141    private readonly root: TreapNode<T>;
142    private readonly compareFn: CompareFunction<T, 'number'>;
143    private readonly leftBound: T;
144    private readonly rightBound: T;
145  
146    constructor(compareFn?: CompareFunction<T, 'number'>);
147    constructor(compareFn: CompareFunction<T, 'number'>, leftBound: T, rightBound: T);
148    constructor(
149        compareFn: CompareFunction<T, any> = (a: any, b: any) => a - b,
150        leftBound: any = -Infinity,
151        rightBound: any = Infinity,
152    ) {
153        // Initialize root with sentinel nodes for boundaries
154        this.root = new TreapNode<T>(rightBound);
155        this.root.priority = Infinity;
156        this.root.left = new TreapNode<T>(leftBound);
157        this.root.left.priority = -Infinity;
158        this.root.pushUp();
159      
160        this.leftBound = leftBound;
161        this.rightBound = rightBound;
162        this.compareFn = compareFn;
163    }
164  
165    // Get the number of elements (excluding sentinels)
166    get size(): number {
167        return this.root.size - 2;
168    }
169  
170    // Get the height of the tree
171    get height(): number {
172        const getHeight = (node: TreapNode<T> | null): number => {
173            if (node == null) return 0;
174            return 1 + Math.max(getHeight(node.left), getHeight(node.right));
175        };
176        return getHeight(this.root);
177    }
178  
179    /**
180     * Check if a value exists in the set
181     * @complexity O(log n)
182     */
183    has(value: T): boolean {
184        const compare = this.compareFn;
185        const search = (node: TreapNode<T> | null, value: T): boolean => {
186            if (node == null) return false;
187            const cmp = compare(node.value, value);
188            if (cmp === 0) return true;
189            if (cmp < 0) return search(node.right, value);
190            return search(node.left, value);
191        };
192        return search(this.root, value);
193    }
194  
195    /**
196     * Add values to the sorted set
197     * @complexity O(log n) per value
198     */
199    add(...values: T[]): this {
200        const compare = this.compareFn;
201      
202        const insert = (
203            node: TreapNode<T> | null,
204            value: T,
205            parent: TreapNode<T>,
206            direction: 'left' | 'right',
207        ): void => {
208            if (node == null) return;
209          
210            const cmp = compare(node.value, value);
211          
212            if (cmp === 0) {
213                // Value exists, increment count
214                node.count++;
215                node.pushUp();
216            } else if (cmp > 0) {
217                // Go left
218                if (node.left) {
219                    insert(node.left, value, node, 'left');
220                } else {
221                    node.left = new TreapNode(value);
222                    node.pushUp();
223                }
224                // Maintain heap property
225                if (TreapNode.getFac(node.left) > node.priority) {
226                    parent[direction] = node.rotateRight();
227                }
228            } else {
229                // Go right
230                if (node.right) {
231                    insert(node.right, value, node, 'right');
232                } else {
233                    node.right = new TreapNode(value);
234                    node.pushUp();
235                }
236                // Maintain heap property
237                if (TreapNode.getFac(node.right) > node.priority) {
238                    parent[direction] = node.rotateLeft();
239                }
240            }
241            parent.pushUp();
242        };
243      
244        values.forEach(value => insert(this.root.left, value, this.root, 'left'));
245        return this;
246    }
247  
248    /**
249     * Remove a value from the set
250     * @complexity O(log n)
251     */
252    delete(value: T): void {
253        const compare = this.compareFn;
254      
255        const remove = (
256            node: TreapNode<T> | null,
257            value: T,
258            parent: TreapNode<T>,
259            direction: 'left' | 'right',
260        ): void => {
261            if (node == null) return;
262          
263            const cmp = compare(node.value, value);
264          
265            if (cmp === 0) {
266                if (node.count > 1) {
267                    // Multiple instances, just decrement
268                    node.count--;
269                    node?.pushUp();
270                } else if (node.left == null && node.right == null) {
271                    // Leaf node, remove it
272                    parent[direction] = null;
273                } else {
274                    // Internal node, rotate to leaf then remove
275                    if (node.right == null || 
276                        TreapNode.getFac(node.left) > TreapNode.getFac(node.right)) {
277                        parent[direction] = node.rotateRight();
278                        remove(parent[direction]?.right ?? null, value, parent[direction]!, 'right');
279                    } else {
280                        parent[direction] = node.rotateLeft();
281                        remove(parent[direction]?.left ?? null, value, parent[direction]!, 'left');
282                    }
283                }
284            } else if (cmp > 0) {
285                remove(node.left, value, node, 'left');
286            } else {
287                remove(node.right, value, node, 'right');
288            }
289          
290            parent?.pushUp();
291        };
292      
293        remove(this.root.left, value, this.root, 'left');
294    }
295  
296    /**
297     * Find insertion point for value (before any existing equal values)
298     * @complexity O(log n)
299     */
300    bisectLeft(value: T): number {
301        const compare = this.compareFn;
302      
303        const search = (node: TreapNode<T> | null, value: T): number => {
304            if (node == null) return 0;
305          
306            const cmp = compare(node.value, value);
307            if (cmp === 0) {
308                return TreapNode.getSize(node.left);
309            } else if (cmp > 0) {
310                return search(node.left, value);
311            } else {
312                return search(node.right, value) + TreapNode.getSize(node.left) + node.count;
313            }
314        };
315      
316        return search(this.root, value) - 1;
317    }
318  
319    /**
320     * Find insertion point for value (after any existing equal values)
321     * @complexity O(log n)
322     */
323    bisectRight(value: T): number {
324        const compare = this.compareFn;
325      
326        const search = (node: TreapNode<T> | null, value: T): number => {
327            if (node == null) return 0;
328          
329            const cmp = compare(node.value, value);
330            if (cmp === 0) {
331                return TreapNode.getSize(node.left) + node.count;
332            } else if (cmp > 0) {
333                return search(node.left, value);
334            } else {
335                return search(node.right, value) + TreapNode.getSize(node.left) + node.count;
336            }
337        };
338      
339        return search(this.root, value) - 1;
340    }
341  
342    /**
343     * Find the first index of a value
344     * @complexity O(log n)
345     */
346    indexOf(value: T): number {
347        const compare = this.compareFn;
348        let found = false;
349      
350        const search = (node: TreapNode<T> | null, value: T): number => {
351            if (node == null) return 0;
352          
353            const cmp = compare(node.value, value);
354            if (cmp === 0) {
355                found = true;
356                return TreapNode.getSize(node.left);
357            } else if (cmp > 0) {
358                return search(node.left, value);
359            } else {
360                return search(node.right, value) + TreapNode.getSize(node.left) + node.count;
361            }
362        };
363      
364        const result = search(this.root, value) - 1;
365        return found ? result : -1;
366    }
367  
368    /**
369     * Find the last index of a value
370     * @complexity O(log n)
371     */
372    lastIndexOf(value: T): number {
373        const compare = this.compareFn;
374        let found = false;
375      
376        const search = (node: TreapNode<T> | null, value: T): number => {
377            if (node == null) return 0;
378          
379            const cmp = compare(node.value, value);
380            if (cmp === 0) {
381                found = true;
382                return TreapNode.getSize(node.left) + node.count - 1;
383            } else if (cmp > 0) {
384                return search(node.left, value);
385            } else {
386                return search(node.right, value) + TreapNode.getSize(node.left) + node.count;
387            }
388        };
389      
390        const result = search(this.root, value) - 1;
391        return found ? result : -1;
392    }
393  
394    /**
395     * Get element at specific index
396     * @complexity O(log n)
397     */
398    at(index: number): T | undefined {
399        // Handle negative indices
400        if (index < 0) index += this.size;
401        if (index < 0 || index >= this.size) return undefined;
402      
403        const findByRank = (node: TreapNode<T> | null, rank: number): T | undefined => {
404            if (node == null) return undefined;
405          
406            const leftSize = TreapNode.getSize(node.left);
407            if (leftSize >= rank) {
408                return findByRank(node.left, rank);
409            } else if (leftSize + node.count >= rank) {
410                return node.value;
411            } else {
412                return findByRank(node.right, rank - leftSize - node.count);
413            }
414        };
415      
416        const result = findByRank(this.root, index + 2);
417        return ([this.leftBound, this.rightBound] as any[]).includes(result) ? undefined : result;
418    }
419  
420    /**
421     * Find the largest element less than value
422     * @complexity O(log n)
423     */
424    lower(value: T): T | undefined {
425        const compare = this.compareFn;
426      
427        const search = (node: TreapNode<T> | null, value: T): T | undefined => {
428            if (node == null) return undefined;
429          
430            if (compare(node.value, value) >= 0) {
431                return search(node.left, value);
432            }
433          
434            const rightResult = search(node.right, value);
435            if (rightResult == null || compare(node.value, rightResult) > 0) {
436                return node.value;
437            }
438            return rightResult;
439        };
440      
441        const result = search(this.root, value) as any;
442        return result === this.leftBound ? undefined : result;
443    }
444  
445    /**
446     * Find the smallest element greater than value
447     * @complexity O(log n)
448     */
449    higher(value: T): T | undefined {
450        const compare = this.compareFn;
451      
452        const search = (node: TreapNode<T> | null, value: T): T | undefined => {
453            if (node == null) return undefined;
454          
455            if (compare(node.value, value) <= 0) {
456                return search(node.right, value);
457            }
458          
459            const leftResult = search(node.left, value);
460            if (leftResult == null || compare(node.value, leftResult) < 0) {
461                return node.value;
462            }
463            return leftResult;
464        };
465      
466        const result = search(this.root, value) as any;
467        return result === this.rightBound ? undefined : result;
468    }
469  
470    /**
471     * Find the largest element less than or equal to value
472     * @complexity O(log n)
473     */
474    floor(value: T): T | undefined {
475        const compare = this.compareFn;
476      
477        const search = (node: TreapNode<T> | null, value: T): T | undefined => {
478            if (node == null) return undefined;
479          
480            const cmp = compare(node.value, value);
481            if (cmp === 0) return node.value;
482            if (cmp >= 0) return search(node.left, value);
483          
484            const rightResult = search(node.right, value);
485            if (rightResult == null || compare(node.value, rightResult) > 0) {
486                return node.value;
487            }
488            return rightResult;
489        };
490      
491        const result = search(this.root, value) as any;
492        return result === this.leftBound ? undefined : result;
493    }
494  
495    /**
496     * Find the smallest element greater than or equal to value
497     * @complexity O(log n)
498     */
499    ceil(value: T): T | undefined {
500        const compare = this.compareFn;
501      
502        const search = (node: TreapNode<T> | null, value: T): T | undefined => {
503            if (node == null) return undefined;
504          
505            const cmp = compare(node.value, value);
506            if (cmp === 0) return node.value;
507            if (cmp <= 0) return search(node.right, value);
508          
509            const leftResult = search(node.left, value);
510            if (leftResult == null || compare(node.value, leftResult) < 0) {
511                return node.value;
512            }
513            return leftResult;
514        };
515      
516        const result = search(this.root, value) as any;
517        return result === this.rightBound ? undefined : result;
518    }
519  
520    /**
521     * Get the minimum element
522     * @complexity O(log n)
523     */
524    first(): T | undefined {
525        const iterator = this.inOrder();
526        iterator.next(); // Skip left bound
527        const result = iterator.next().value;
528        return result === this.rightBound ? undefined : result;
529    }
530  
531    /**
532     * Get the maximum element
533     * @complexity O(log n)
534     */
535    last(): T | undefined {
536        const iterator = this.reverseInOrder();
537        iterator.next(); // Skip right bound
538        const result = iterator.next().value;
539        return result === this.leftBound ? undefined : result;
540    }
541  
542    /**
543     * Remove and return the minimum element
544     * @complexity O(log n)
545     */
546    shift(): T | undefined {
547        const firstElement = this.first();
548        if (firstElement === undefined) return undefined;
549        this.delete(firstElement);
550        return firstElement;
551    }
552  
553    /**
554     * Remove and return the maximum element or element at index
555     * @complexity O(log n)
556     */
557    pop(index?: number): T | undefined {
558        if (index == null) {
559            const lastElement = this.last();
560            if (lastElement === undefined) return undefined;
561            this.delete(lastElement);
562            return lastElement;
563        }
564      
565        const elementToDelete = this.at(index);
566        if (elementToDelete == null) return;
567        this.delete(elementToDelete);
568        return elementToDelete;
569    }
570  
571    /**
572     * Count occurrences of a value
573     * @complexity O(log n)
574     */
575    count(value: T): number {
576        const compare = this.compareFn;
577      
578        const search = (node: TreapNode<T> | null, value: T): number => {
579            if (node == null) return 0;
580          
581            const cmp = compare(node.value, value);
582            if (cmp === 0) return node.count;
583            if (cmp < 0) return search(node.right, value);
584            return search(node.left, value);
585        };
586      
587        return search(this.root, value);
588    }
589  
590    // Iterator implementation
591    *[Symbol.iterator](): Generator<T, any, any> {
592        yield* this.values();
593    }
594  
595    /**
596     * Get an iterator for all keys
597     */
598    *keys(): Generator<T, any, any> {
599        yield* this.values();
600    }
601  
602    /**
603     * Get an iterator for all values in sorted order
604     */
605    *values(): Generator<T, any, any> {
606        const iterator = this.inOrder();
607        iterator.next(); // Skip left bound
608        const totalSteps = this.size;
609        for (let i = 0; i < totalSteps; i++) {
610            yield iterator.next().value;
611        }
612    }
613  
614    /**
615     * Get an iterator for all values in reverse order
616     */
617    *rvalues(): Generator<T, any, any> {
618        const iterator = this.reverseInOrder();
619        iterator.next(); // Skip right bound
620        const totalSteps = this.size;
621        for (let i = 0; i < totalSteps; i++) {
622            yield iterator.next().value;
623        }
624    }
625  
626    /**
627     * Get an iterator for [index, value] pairs
628     */
629    *entries(): IterableIterator<[number, T]> {
630        const iterator = this.inOrder();
631        iterator.next(); // Skip left bound
632        const totalSteps = this.size;
633        for (let i = 0; i < totalSteps; i++) {
634            yield [i, iterator.next().value];
635        }
636    }
637  
638    // In-order traversal generator
639    private *inOrder(root: TreapNode<T> | null = this.root): Generator<T, any, any> {
640        if (root == null) return;
641      
642        yield* this.inOrder(root.left);
643      
644        const duplicates = root.count;
645        for (let i = 0; i < duplicates; i++) {
646            yield root.value;
647        }
648      
649        yield* this.inOrder(root.right);
650    }
651  
652    // Reverse in-order traversal generator
653    private *reverseInOrder(root: TreapNode<T> | null = this.root): Generator<T, any, any> {
654        if (root == null) return;
655      
656        yield* this.reverseInOrder(root.right);
657      
658        const duplicates = root.count;
659        for (let i = 0; i < duplicates; i++) {
660            yield root.value;
661        }
662      
663        yield* this.reverseInOrder(root.left);
664    }
665}
666

Time and Space Complexity

The time complexity is O(n log n), where n is the number of points.

Time Complexity Analysis:

  • Initially, we add all n points to two SortedLists (sl1 and sl2). Each insertion into a SortedList takes O(log n) time, so this initialization phase takes O(n log n) time.
  • Then we iterate through all n points:
    • For each point, we remove it from both SortedLists: O(log n) per removal, so O(log n) total
    • We access the first and last elements of both SortedLists: O(1) time
    • We add the point back to both SortedLists: O(log n) per addition, so O(log n) total
    • Each iteration takes O(log n) time
  • The loop runs n times, giving us O(n log n) for the main loop
  • Overall time complexity: O(n log n) + O(n log n) = O(n log n)

The space complexity is O(n).

Space Complexity Analysis:

  • Two SortedLists (sl1 and sl2) each store n values (transformed coordinates x + y and x - y)
  • Each SortedList requires O(n) space
  • Total space used: O(n) + O(n) = O(n)

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

Common Pitfalls

1. Incorrect Manhattan Distance Formula Implementation

A common mistake is trying to directly calculate Manhattan distance as max(|x1-x2|, |y1-y2|) instead of |x1-x2| + |y1-y2|. This confusion often arises from mixing up Manhattan distance with Chebyshev distance.

Wrong approach:

# Incorrect - this is Chebyshev distance, not Manhattan
distance = max(abs(x1 - x2), abs(y1 - y2))

Correct approach:

# Correct Manhattan distance
distance = abs(x1 - x2) + abs(y1 - y2)
# Or using the transformation
distance = max(abs((x1+y1) - (x2+y2)), abs((x1-y1) - (x2-y2)))

2. Edge Case: Array with Only 3 Points

When the array has exactly 3 points, after removing one point, only 2 points remain. The code might break if not handling empty ranges properly.

Problematic scenario:

# If points array has less than 3 points after removal
if len(sum_sorted) < 2:
    # sum_sorted[-1] - sum_sorted[0] would fail or give 0

Solution:

# Add a check before calculating distances
if len(points) <= 2:
    return 0  # No distance if only 1 point remains after removal

# Or handle in the main loop
if len(sum_sorted) == 0:
    max_distance = 0
else:
    max_distance = max(sum_sorted[-1] - sum_sorted[0], 
                      diff_sorted[-1] - diff_sorted[0])

3. Using Regular List Instead of SortedList

Attempting to use a regular Python list with manual sorting leads to inefficient O(n²log n) complexity due to repeated sorting operations.

Inefficient approach:

# Wrong - this requires O(n log n) sorting for each removal
sum_list = []
for x, y in points:
    sum_list.append(x + y)
sum_list.sort()  # O(n log n) each time

# For each point removal
sum_list.remove(x + y)  # O(n) to find and remove
sum_list.sort()  # Another O(n log n) - very inefficient!

Correct approach:

from sortedcontainers import SortedList
# SortedList maintains order automatically
sum_sorted = SortedList()
# O(log n) for add/remove operations
sum_sorted.add(x + y)
sum_sorted.remove(x + y)

4. Forgetting to Add Points Back After Temporary Removal

A critical error is forgetting to add the point back after calculating the maximum distance without it. This would corrupt the data for subsequent iterations.

Bug-prone code:

for x, y in points:
    sum_sorted.remove(x + y)
    diff_sorted.remove(x - y)
  
    max_distance = max(sum_sorted[-1] - sum_sorted[0], 
                      diff_sorted[-1] - diff_sorted[0])
    min_max_distance = min(min_max_distance, max_distance)
  
    # Forgot to add back! Next iteration will have wrong data
    # sum_sorted.add(x + y)  # Missing!
    # diff_sorted.add(x - y)  # Missing!

5. Not Handling Duplicate Points Correctly

If the input contains duplicate points, removing one instance might accidentally affect calculations for another identical point.

Potential issue:

# If points = [[1,1], [1,1], [3,3]]
# When removing first [1,1], we might accidentally affect the second [1,1]

Solution: The current implementation using SortedList.remove() only removes one instance, which is correct. However, be aware that if using sets or other data structures, duplicates need special handling.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More