2817. Minimum Absolute Difference Between Elements With Constraint


Problem Description

In this problem, we're provided with an array of integers, nums, and a separate integer x. The objective is to find the smallest possible absolute difference between two elements of the array, given that these elements' indices are x or more positions apart. More formally, we need to find indices i and j (where i can be before or after j) in such a way that abs(i - j) >= x, while simultaneously minimizing the value of abs(nums[i] - nums[j]).

The problem defines an absolute difference function abs, which calculates the non-negative difference between two numbers. In this context, it's applied twice: first to ensure that the indices i and j are sufficiently separated by at least x indices; second, to measure the difference in value between the array entries at those indices.

Intuition

The main intuition behind the solution is that to find the minimum absolute difference between elements that are at least x indices apart, we don't need to compare each element with every other element that meets the index distance requirement. That would be inefficient, especially for large arrays. Instead, we can maintain a subset of the array elements that allows us to quickly find the closest elements in value to any given element in the array. We use a data structure that maintains the elements in sorted order and allows for efficient lookups - an ordered set.

The ordered set is implemented using SortedList, which comes from the sortedcontainers Python library. This data structure supports logarithmic time complexity for insertion, deletion, and lookup operations—a key benefit when dealing with a potentially large number of elements.

To implement the solution, we iterate over the array starting from index x, for each element, adding the element that is exactly x indices before it to the ordered set. Now, for every nums[i] (where i >= x), we only need to compare it with the closest values that are already in the ordered set. These closest values are the ones that could potentially yield the minimum absolute difference.

The bisect_left function from the Python's bisect module is used to find the position where nums[i] would fit in the sorted list. This gives us two candidates: the element right before and right after the position found by bisect_left. Compare nums[i] with these two candidates, and the smaller difference is a potential answer. Update the answer if this difference is smaller than all previously seen differences.

This way, we efficiently narrow our search for the minimum absolute difference to just two comparisons per array element considered, using the ordered set to quickly find the best candidates for comparison.

Learn more about Binary Search patterns.

Solution Approach

The solution is implemented in Python and involves the use of the SortedList data structure to maintain a dynamically ordered subset of the array elements for quick comparisons. This ordered set allows us to find the minimum difference efficiently.

Here's a step-by-step breakdown of how the algorithm works:

  1. We initialize our ordered set sl by importing SortedList. The set is initially empty because we populate it as we iterate through the array.

  2. We start with a variable ans initialized to inf, representing the smallest absolute difference found so far. inf is a placeholder for infinity, ensuring that any real absolute difference found will be smaller and thus will replace inf.

  3. We enumerate the elements of nums starting from index x (i.e., the first element that can have another element in the array which is x indices apart is at index x).

  4. As we visit each element nums[i] where i >= x, we add the element nums[i - x] to our ordered set sl. This is because nums[i - x] is exactly x indices before nums[i], satisfying the distance constraint.

  5. We then use the bisect_left function from the bisect module to find the position in the ordered set where nums[i] would fit based on its value.

  6. Two adjacent positions in the ordered set are considered:

    • The position found by bisect_left (sl[j]), which is the first element in the ordered set that is not less than nums[i].
    • The position immediately before (sl[j - 1]), which is the largest element in the ordered set that is less than nums[i] (if it exists).
  7. If j is less than the length of the ordered set, it means there's an element on its right; we calculate the difference between this element and nums[i], and if it's smaller than ans, we update ans.

  8. If j is not 0 (which means that there is an element in the set smaller than nums[i]), we find the difference with the left neighbor sl[j - 1] and update ans if this new difference is smaller.

  9. After iterating through all elements from index x to the end of the array, the variable ans holds the minimum absolute difference, and we return this value.

Through the use of SortedList and the binary search function bisect_left, this approach optimizes what could have been an O(n^2) brute force comparison process into an O(n log n) process, since each of the n - x iterations takes O(log n) due to operations on the ordered set.

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

How does merge sort divide the problem into subproblems?

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Consider the array nums = [4, 2, 5, 1, 3] and x = 2. We want to find the smallest absolute difference between two elements where the indices are at least x positions apart.

  1. We begin by initializing SortedList as sl and setting ans to inf.

  2. Now, we start iterating over the array from the index x, which is 2 in this case. So we look at nums[2] which is 5.

  3. Before we consider nums[2], we add nums[0] (4 in this case) to sl. Now sl = [4].

  4. We then check for the closest elements in sl to 5. Since sl has only one value, we only need to check 4. The absolute difference abs(5 - 4) = 1, so we update ans from inf to 1.

  5. We move to the next index, i = 3. We add nums[1] (2) to sl, so now sl = [2, 4].

  6. nums[3] is 1. Using bisect_left, we find that 1 would be inserted at the beginning of sl, so we compare it to the first element (2). The absolute difference abs(1 - 2) = 1. Our ans is already 1, so it remains unchanged.

  7. Moving to i = 4, we add nums[2] (5) to sl. Now, sl becomes [2, 4, 5].

  8. nums[4] is 3. Using bisect_left, we determine 3 would fit between 2 and 4 in sl. Hence, we compare 3 with both sides: abs(3 - 2) = 1 and abs(3 - 4) = 1. ans remains 1.

  9. Having checked all elements from index x to the end of nums, we conclude that the smallest absolute difference we found is 1, so we return ans which is 1.

Thus, the minimum absolute difference between elements at least x indices apart is 1. This example demonstrates the efficacy of the algorithm in finding the solution with fewer comparisons, thanks to the ordered set sl and binary search techniques.

Solution Implementation

1from sortedcontainers import SortedList
2from bisect import bisect_left
3
4class Solution:
5    def minAbsoluteDifference(self, nums, target):
6        # Create a SortedList to store the current window of numbers
7        sorted_list = SortedList()
8        # Initialize the answer with infinity, representing a large number
9        min_diff = float('inf')
10
11        # Start iterating from the target index to the end of the list
12        for i in range(target, len(nums)):
13            # Add the (current index - target)th element to maintain the window
14            sorted_list.add(nums[i - target])
15            # Find the index in the sorted list where nums[i] would fit
16            insert_pos = bisect_left(sorted_list, nums[i])
17          
18            # If there is an element on the right, update min_diff
19            if insert_pos < len(sorted_list):
20                min_diff = min(min_diff, sorted_list[insert_pos] - nums[i])
21            # If there is an element on the left, update min_diff
22            if insert_pos > 0:
23                min_diff = min(min_diff, nums[i] - sorted_list[insert_pos - 1])
24
25        # Return the minimum absolute difference found
26        return min_diff
27
1class Solution {
2    public int minAbsoluteDifference(List<Integer> nums, int windowSize) {
3        // TreeMap to store the occurrences of the numbers in the current window
4        TreeMap<Integer, Integer> frequencyMap = new TreeMap<>();
5        // Initialize the answer with the maximum possible positive integer value
6        int minimumDifference = Integer.MAX_VALUE;
7      
8        // Start from index windowSize, as we want to have a complete window
9        for (int i = windowSize; i < nums.size(); ++i) {
10            // Add or update the frequency of the starting element of the current window
11            frequencyMap.merge(nums.get(i - windowSize), 1, Integer::sum);
12            // Find the smallest number greater than or equal to the current number
13            Integer higherOrEqual = frequencyMap.ceilingKey(nums.get(i));
14            if (higherOrEqual != null) {
15                // If found, update the minimum difference
16                minimumDifference = Math.min(minimumDifference, higherOrEqual - nums.get(i));
17            }
18            // Find the greatest number less than or equal to the current number
19            Integer lowerOrEqual = frequencyMap.floorKey(nums.get(i));
20            if (lowerOrEqual != null) {
21                // If found, update the minimum difference
22                minimumDifference = Math.min(minimumDifference, nums.get(i) - lowerOrEqual);
23            }
24        }
25        // Return the found minimum absolute difference
26        return minimumDifference;
27    }
28}
29
1#include <vector>
2#include <set>
3#include <algorithm> // For std::min and std::max
4#include <climits> // For INT_MAX
5
6class Solution {
7public:
8    // Function to find the minimum absolute difference
9    int minAbsoluteDifference(vector<int>& nums, int x) {
10        // Initialize the answer with the maximum possible int value
11        int minAbsDiff = INT_MAX;
12
13        // Use a multiset to maintain a sorted window of the last 'x' elements
14        multiset<int> windowElements;
15
16        // Loop to calculate the minimum absolute difference
17        for (int i = x; i < nums.size(); ++i) {
18            // Insert elements into the window so it always contains 'x' elements
19            windowElements.insert(nums[i - x]);
20
21            // Find the lower_bound of the current number in our window
22            auto it = windowElements.lower_bound(nums[i]);
23
24            // If the iterator is not at the end, update the minimum difference
25            if (it != windowElements.end()) {
26                minAbsDiff = min(minAbsDiff, abs(*it - nums[i]));
27            }
28
29            // Check the predecessor of the current element if it exists
30            if (it != windowElements.begin()) {
31                --it; // Move the iterator to the left element
32                minAbsDiff = min(minAbsDiff, abs(nums[i] - *it));
33            }
34        }
35        // Return the minimum absolute difference found
36        return minAbsDiff;
37    }
38};
39
1module TreeMultiSetModule {
2    // Define a comparator type for comparing elements.
3    type Compare<T> = (lhs: T, rhs: T) => number;
4
5    // Red-Black Tree node class with generics support.
6    class RBTreeNode<T = number> {
7        data: T;
8        count: number;
9        left: RBTreeNode<T> | null;
10        right: RBTreeNode<T> | null;
11        parent: RBTreeNode<T> | null;
12        color: number;
13
14        constructor(data: T) {
15            this.data = data;
16            this.left = this.right = this.parent = null;
17            this.color = 0; // Use 0 for red, 1 for black.
18            this.count = 1; // Counter for duplicates.
19        }
20
21        // Helper function to get the sibling of the node.
22        sibling(): RBTreeNode<T> | null {
23            if (!this.parent) {
24                return null; // No sibling if no parent.
25            }
26            return this.isOnLeft() ? this.parent.right : this.parent.left;
27        }
28
29        // Check if the node is the left child of its parent.
30        isOnLeft(): boolean {
31            return this === this.parent!.left;
32        }
33
34        // Check if the node has at least one red child.
35        hasRedChild(): boolean {
36            return (
37                (this.left !== null && this.left.color === 0) ||
38                (this.right !== null && this.right.color === 0)
39            );
40        }
41    }
42
43    // Red-Black Tree class definition.
44    class RBTree<T> {
45        root: RBTreeNode<T> | null;
46        lt: (l: T, r: T) => boolean; // Function to compare two nodes.
47
48        constructor(compare: Compare<T> = (l: T, r: T) => (l < r ? -1 : l > r ? 1 : 0)) {
49            this.root = null; // Root initially set to null.
50            this.lt = (l: T, r: T) => compare(l, r) < 0;
51        }
52
53        // Rotate the subtree to the left at the given node.
54        rotateLeft(pt: RBTreeNode<T>): void {
55            const right = pt.right!;
56            pt.right = right.left;
57
58            if (pt.right) {
59                pt.right.parent = pt;
60            }
61            right.parent = pt.parent;
62
63            if (!pt.parent) {
64                this.root = right;
65            } else if (pt === pt.parent.left) {
66                pt.parent.left = right;
67            } else {
68                pt.parent.right = right;
69            }
70
71            right.left = pt;
72            pt.parent = right;
73        }
74
75        // Rotate the subtree to the right at the given node.
76        rotateRight(pt: RBTreeNode<T>): void {
77            const left = pt.left!;
78            pt.left = left.right;
79
80            if (pt.left) {
81                pt.left.parent = pt;
82            }
83            left.parent = pt.parent;
84
85            if (!pt.parent) {
86                this.root = left;
87            } else if (pt === pt.parent.left) {
88                pt.parent.left = left;
89            } else {
90                pt.parent.right = left;
91            }
92
93            left.right = pt;
94            pt.parent = left;
95        }
96
97        // Swap colors between two nodes.
98        swapColor(p1: RBTreeNode<T>, p2: RBTreeNode<T>): void {
99            const tmp = p1.color;
100            p1.color = p2.color;
101            p2.color = tmp;
102        }
103
104        // Swap data between two nodes.
105        swapData(p1: RBTreeNode<T>, p2: RBTreeNode<T>): void {
106            const tmp = p1.data;
107            p1.data = p2.data;
108            p2.data = tmp;
109        }
110      
111        // Additional RBTree methods would go here...
112    }
113
114    // TreeMultiSet class definition with generics support, using a Red-Black Tree.
115    class TreeMultiSet<T = number> {
116        private _size: number; // The number of elements in the multiset.
117        private tree: RBTree<T>; // The underlying Red-Black tree.
118        private compare: Compare<T>; // Comparator function for the elements.
119
120        constructor(
121            collection: T[] | Compare<T> = [],
122            compare: Compare<T> = (l: T, r: T) => (l < r ? -1 : l > r ? 1 : 0)
123        ) {
124            if (typeof collection === 'function') {
125                compare = collection;
126                collection = [];
127            }
128            this._size = 0;
129            this.compare = compare;
130            this.tree = new RBTree(compare);
131            for (const val of collection) {
132                this.add(val);
133            }
134        }
135
136        // Returns the size of the multiset.
137        size(): number {
138            return this._size;
139        }
140
141        // Checks if the multiset contains a value.
142        has(val: T): boolean {
143            return !!this.tree.find(val);
144        }
145
146        // Adds a value to the multiset.
147        add(val: T): boolean {
148            const successful = this.tree.insert(val);
149            if (successful) {
150                this._size++;
151            }
152            return successful;
153        }
154
155        // Deletes one occurrence of the value from the multiset.
156        delete(val: T): boolean {
157            const successful = this.tree.delete(val);
158            if (successful) {
159                this._size--;
160            }
161            return successful;
162        }
163
164        // Additional TreeMultiSet methods would go here...
165    }
166
167    // Export the TreeMultiSet class from the module.
168    export { TreeMultiSet };
169
170    // The rest of the TreeMultiSet class methods... (for brevity, not shown here)
171}
172
173// Export the module for use elsewhere.
174export = TreeMultiSetModule;
175

Time and Space Complexity

The time complexity of the provided code is O(n * log(x)) where n is the length of the array nums and x is the window size given as a parameter. This is due to the fact that we are iterating through n - x elements, and for each element, we perform operations like add and bisect_left which are O(log(x)). Since x is less than or equal to n, in the worst case, the time complexity could be mistaken as O(n * log(n)), but it is crucial to note that x dictates the actual size of the sorted list, not n.

The space complexity of the code is O(x) as the space is used only for maintaining the sorted list with a maximum of x elements at any time, rather than O(n) as inferred in the reference. If x were to be considered constant or a value that does not scale with n, the space complexity could be considered as O(1).

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


Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings


Got a question? Ask the Monster Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

Tired of the LeetCode Grind?

Our structured approach teaches you the patterns behind problems, so you can confidently solve any challenge. Get started now to land your dream tech job.

Get Started

🪄