3281. Maximize Score of Numbers in Ranges
Problem Description
You are given an array start
containing integers and an integer d
. These represent n
intervals where each interval i
is defined as [start[i], start[i] + d]
.
Your task is to choose exactly n
integers, where:
- The
i
-th integer must be chosen from thei
-th interval[start[i], start[i] + d]
- Each interval contributes exactly one integer to your selection
The score of your chosen integers is defined as the minimum absolute difference between any two integers in your selection. In other words, if you choose integers a₁, a₂, ..., aₙ
, the score is the smallest value of |aᵢ - aⱼ|
for all pairs where i ≠ j
.
Your goal is to maximize this score by strategically selecting one integer from each interval.
Example to illustrate:
- If
start = [1, 4]
andd = 2
- The intervals are
[1, 3]
and[4, 6]
- You need to pick one integer from
[1, 3]
and one from[4, 6]
- If you pick
3
from the first interval and4
from the second, the score is|3 - 4| = 1
- If you pick
1
from the first interval and6
from the second, the score is|1 - 6| = 5
- The maximum possible score would be
5
The problem asks you to return the maximum possible score that can be achieved.
Intuition
The key insight is that to maximize the minimum difference between chosen integers, we want to spread them out as much as possible. Think of it like placing n
points on a number line where each point must fall within its designated interval - we want these points to be as evenly spaced as possible.
Since we're maximizing the minimum difference, we should consider what happens when we fix a minimum difference value. If we can successfully place all integers with at least distance x
between consecutive ones, then we know x
is achievable. If x
is achievable, then any smaller value x' < x
is also achievable (we can always place integers closer together if needed).
This monotonic property suggests binary search: we can search for the largest minimum difference that's still achievable.
Another crucial observation is that sorting the intervals by their start positions helps us make greedy choices. Once sorted, we can process intervals from left to right, and for each interval, we want to place our integer as far left as possible while maintaining the minimum distance from the previous integer. This greedy approach works because:
- Placing an integer further left gives more room for subsequent integers
- We must respect each interval's boundaries
[start[i], start[i] + d]
The strategy becomes:
- Sort intervals by start position
- Binary search on the answer (minimum difference)
- For each candidate difference, greedily check if we can place all integers from left to right
- When checking, always place each integer at the earliest valid position: either at the interval's start or at
last_position + minimum_difference
, whichever is larger
This way, we transform the optimization problem into a series of decision problems ("Can we achieve minimum difference of at least x
?"), which we can efficiently solve using binary search combined with a greedy validation algorithm.
Learn more about Greedy, Binary Search and Sorting patterns.
Solution Approach
The solution implements a binary search combined with a greedy validation function.
Step 1: Sorting
First, we sort the start
array in ascending order. This allows us to process intervals from left to right and make greedy decisions about where to place each integer.
Step 2: Binary Search Setup We set up binary search boundaries:
- Left boundary:
l = 0
(minimum possible difference) - Right boundary:
r = start[-1] + d - start[0]
(maximum possible difference between the rightmost point of the last interval and the leftmost point of the first interval)
Step 3: Binary Search Implementation We use binary search to find the largest minimum difference that satisfies our constraints:
while l < r: mid = (l + r + 1) >> 1 # equivalent to (l + r + 1) // 2 if check(mid): l = mid # mid is achievable, try larger values else: r = mid - 1 # mid is not achievable, try smaller values
Step 4: Validation Function check(mi)
The core logic lies in the check
function that determines if a minimum difference mi
is achievable:
def check(mi: int) -> bool:
last = -inf # Track the position of the last placed integer
for st in start:
if last + mi > st + d:
return False # Cannot place an integer in this interval
last = max(st, last + mi) # Place integer at the earliest valid position
return True
The validation works as follows:
- Initialize
last = -inf
to represent that no integer has been placed yet - For each interval
[st, st + d]
:- Check if we can place an integer at least
mi
distance from the previous one - If
last + mi > st + d
, it means even placing at the rightmost positionst + d
wouldn't maintain the minimum distance, so returnFalse
- Otherwise, place the integer at
max(st, last + mi)
:- If
last + mi ≤ st
, place at the interval's startst
- If
last + mi > st
, place atlast + mi
(the earliest position maintaining the minimum distance)
- If
- Check if we can place an integer at least
- If all intervals can accommodate an integer with the required spacing, return
True
Time Complexity: O(n log(max_range))
where n
is the number of intervals and max_range
is the difference between the maximum and minimum possible positions.
Space Complexity: O(1)
if we don't count the sorting space, otherwise O(n)
for sorting.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with start = [2, 6, 13, 13]
and d = 5
.
Step 1: Understanding the intervals
- Interval 0:
[2, 7]
(from 2 to 2+5) - Interval 1:
[6, 11]
(from 6 to 6+5) - Interval 2:
[13, 18]
(from 13 to 13+5) - Interval 3:
[13, 18]
(from 13 to 13+5)
Step 2: Sort the intervals
After sorting by start positions: [2, 6, 13, 13]
(already sorted)
Step 3: Set up binary search
- Left boundary:
l = 0
- Right boundary:
r = 13 + 5 - 2 = 16
Step 4: Binary search iterations
Iteration 1:
mid = (0 + 16 + 1) // 2 = 8
- Check if minimum difference of 8 is achievable:
- Place in interval
[2, 7]
:last = -∞
, so place atmax(2, -∞ + 8) = 2
. Updatelast = 2
- Place in interval
[6, 11]
: need at least2 + 8 = 10
, place atmax(6, 10) = 10
. Updatelast = 10
- Place in interval
[13, 18]
: need at least10 + 8 = 18
, place atmax(13, 18) = 18
. Updatelast = 18
- Place in interval
[13, 18]
: need at least18 + 8 = 26
, but interval ends at 18. Cannot place!
- Place in interval
- Result:
check(8) = False
, sor = 7
Iteration 2:
mid = (0 + 7 + 1) // 2 = 4
- Check if minimum difference of 4 is achievable:
- Place in interval
[2, 7]
: place atmax(2, -∞ + 4) = 2
. Updatelast = 2
- Place in interval
[6, 11]
: place atmax(6, 2 + 4) = 6
. Updatelast = 6
- Place in interval
[13, 18]
: place atmax(13, 6 + 4) = 13
. Updatelast = 13
- Place in interval
[13, 18]
: place atmax(13, 13 + 4) = 17
. Updatelast = 17
- Place in interval
- Result:
check(4) = True
, sol = 4
Iteration 3:
mid = (4 + 7 + 1) // 2 = 6
- Check if minimum difference of 6 is achievable:
- Place in interval
[2, 7]
: place at2
. Updatelast = 2
- Place in interval
[6, 11]
: place atmax(6, 2 + 6) = 8
. Updatelast = 8
- Place in interval
[13, 18]
: place atmax(13, 8 + 6) = 14
. Updatelast = 14
- Place in interval
[13, 18]
: need at least14 + 6 = 20
, but interval ends at 18. Cannot place!
- Place in interval
- Result:
check(6) = False
, sor = 5
Iteration 4:
mid = (4 + 5 + 1) // 2 = 5
- Check if minimum difference of 5 is achievable:
- Place in interval
[2, 7]
: place at2
. Updatelast = 2
- Place in interval
[6, 11]
: place atmax(6, 2 + 5) = 7
. Updatelast = 7
- Place in interval
[13, 18]
: place atmax(13, 7 + 5) = 13
. Updatelast = 13
- Place in interval
[13, 18]
: place atmax(13, 13 + 5) = 18
. Updatelast = 18
- Place in interval
- Result:
check(5) = True
, sol = 5
Iteration 5:
- Now
l = 5
andr = 5
, so the loop terminates.
Final Answer: 5
The optimal selection is: [2, 7, 13, 18]
- Difference between 2 and 7:
|2 - 7| = 5
- Difference between 7 and 13:
|7 - 13| = 6
- Difference between 13 and 18:
|13 - 18| = 5
- Difference between 2 and 13:
|2 - 13| = 11
- Difference between 2 and 18:
|2 - 18| = 16
- Difference between 7 and 18:
|7 - 18| = 11
The minimum difference among all pairs is 5, which matches our answer.
Solution Implementation
1class Solution:
2 def maxPossibleScore(self, start: List[int], d: int) -> int:
3 """
4 Find the maximum possible minimum distance between chosen positions.
5 Each position can be chosen from interval [start[i], start[i] + d].
6
7 Args:
8 start: List of starting positions for each interval
9 d: The range/width of each interval
10
11 Returns:
12 Maximum possible minimum distance between all chosen positions
13 """
14
15 def can_achieve_min_distance(min_distance: int) -> bool:
16 """
17 Check if it's possible to choose positions such that
18 the minimum distance between any two positions is at least min_distance.
19
20 Args:
21 min_distance: The minimum distance to check
22
23 Returns:
24 True if achievable, False otherwise
25 """
26 # Track the last chosen position
27 last_position = float('-inf')
28
29 # Greedily try to place each position
30 for start_pos in sorted_start:
31 # Check if we can place the next position with required minimum distance
32 if last_position + min_distance > start_pos + d:
33 # Cannot maintain minimum distance even at the rightmost position of interval
34 return False
35
36 # Choose the leftmost valid position in the current interval
37 # Either at the start of interval or at last_position + min_distance
38 last_position = max(start_pos, last_position + min_distance)
39
40 return True
41
42 # Sort intervals by their starting positions for greedy approach
43 sorted_start = sorted(start)
44
45 # Binary search bounds for the minimum distance
46 left = 0 # Minimum possible distance
47 right = sorted_start[-1] + d - sorted_start[0] # Maximum possible distance
48
49 # Binary search for the maximum achievable minimum distance
50 while left < right:
51 # Use ceiling division to avoid infinite loop
52 mid = (left + right + 1) >> 1
53
54 if can_achieve_min_distance(mid):
55 # If mid is achievable, try for a larger minimum distance
56 left = mid
57 else:
58 # If mid is not achievable, try for a smaller minimum distance
59 right = mid - 1
60
61 return left
62
1class Solution {
2 private int[] positions;
3 private int range;
4
5 /**
6 * Finds the maximum possible minimum distance between chosen positions.
7 * Each position can be adjusted within a range [positions[i], positions[i] + d].
8 *
9 * @param start Array of starting positions
10 * @param d Maximum adjustment range for each position
11 * @return Maximum possible minimum distance between all chosen positions
12 */
13 public int maxPossibleScore(int[] start, int d) {
14 // Sort positions to process them in order
15 Arrays.sort(start);
16 this.positions = start;
17 this.range = d;
18
19 int n = start.length;
20
21 // Binary search bounds:
22 // - Minimum possible distance: 0
23 // - Maximum possible distance: difference between rightmost and leftmost possible positions
24 int left = 0;
25 int right = start[n - 1] + d - start[0];
26
27 // Binary search for the maximum valid minimum distance
28 while (left < right) {
29 // Calculate mid point (using unsigned shift to avoid overflow)
30 int mid = (left + right + 1) >>> 1;
31
32 // Check if we can achieve at least 'mid' distance between all positions
33 if (canAchieveMinDistance(mid)) {
34 // If possible, try for a larger minimum distance
35 left = mid;
36 } else {
37 // If not possible, reduce the target minimum distance
38 right = mid - 1;
39 }
40 }
41
42 return left;
43 }
44
45 /**
46 * Checks if it's possible to place all items with at least 'minDistance' between them.
47 * Uses greedy approach: place each item as early as possible while maintaining minimum distance.
48 *
49 * @param minDistance The minimum distance to maintain between consecutive positions
50 * @return true if the arrangement is possible, false otherwise
51 */
52 private boolean canAchieveMinDistance(int minDistance) {
53 // Track the position where we placed the last item
54 long lastPlacedPosition = Long.MIN_VALUE;
55
56 // Try to place each item greedily
57 for (int startPosition : positions) {
58 // Check if placing at the rightmost position still violates minimum distance
59 if (lastPlacedPosition + minDistance > startPosition + range) {
60 // Cannot maintain minimum distance even at the rightmost possible position
61 return false;
62 }
63
64 // Place the current item at the earliest valid position
65 // Either at its start position or at minimum distance from the last placed item
66 lastPlacedPosition = Math.max(startPosition, lastPlacedPosition + minDistance);
67 }
68
69 return true;
70 }
71}
72
1class Solution {
2public:
3 int maxPossibleScore(vector<int>& start, int d) {
4 // Sort the starting positions to process them in order
5 sort(start.begin(), start.end());
6
7 // Lambda function to check if a minimum distance is achievable
8 // Parameters: minDistance - the minimum distance we want to maintain between consecutive chosen positions
9 // Returns: true if we can select positions with at least minDistance apart
10 auto canAchieveMinDistance = [&](int minDistance) -> bool {
11 // Track the last chosen position (initialize to negative infinity)
12 long long lastChosenPosition = LLONG_MIN;
13
14 // Try to place each interval while maintaining minimum distance
15 for (int intervalStart : start) {
16 // Check if next valid position exceeds the interval's right boundary
17 // Next position must be at least lastChosenPosition + minDistance
18 if (lastChosenPosition + minDistance > intervalStart + d) {
19 return false; // Cannot place within this interval
20 }
21
22 // Choose the leftmost valid position in current interval
23 // Either the interval's start or lastChosenPosition + minDistance, whichever is larger
24 lastChosenPosition = max(static_cast<long long>(intervalStart),
25 lastChosenPosition + minDistance);
26 }
27 return true; // Successfully placed all positions
28 };
29
30 // Binary search for the maximum achievable minimum distance
31 int left = 0; // Minimum possible distance
32 int right = start.back() + d - start[0]; // Maximum possible distance
33
34 // Binary search to find the maximum minimum distance
35 while (left < right) {
36 // Calculate mid point (ceiling division to avoid infinite loop)
37 int mid = left + (right - left + 1) / 2;
38
39 if (canAchieveMinDistance(mid)) {
40 // If mid distance is achievable, try for a larger distance
41 left = mid;
42 } else {
43 // If mid distance is not achievable, try smaller distance
44 right = mid - 1;
45 }
46 }
47
48 return left; // Return the maximum achievable minimum distance
49 }
50};
51
1/**
2 * Finds the maximum possible minimum distance between consecutive chosen positions
3 * @param start - Array of starting positions
4 * @param d - Maximum distance that can be added to each starting position
5 * @returns The maximum possible minimum distance between consecutive positions
6 */
7function maxPossibleScore(start: number[], d: number): number {
8 // Sort the starting positions in ascending order
9 start.sort((a, b) => a - b);
10
11 // Initialize binary search boundaries
12 // Left boundary: minimum possible distance (0)
13 // Right boundary: maximum possible distance (last position + d - first position)
14 let left = 0;
15 let right = start[start.length - 1] + d - start[0];
16
17 /**
18 * Checks if a given minimum distance is achievable
19 * @param minDistance - The minimum distance to check
20 * @returns true if the minimum distance is achievable, false otherwise
21 */
22 const isValidDistance = (minDistance: number): boolean => {
23 // Track the last chosen position
24 let lastPosition = -Infinity;
25
26 // Try to place each element with at least minDistance apart
27 for (const startPosition of start) {
28 // Check if we can place the current element
29 // If the required next position exceeds the maximum allowed position
30 if (lastPosition + minDistance > startPosition + d) {
31 return false;
32 }
33 // Place the element at the earliest valid position
34 lastPosition = Math.max(startPosition, lastPosition + minDistance);
35 }
36
37 return true;
38 };
39
40 // Binary search for the maximum achievable minimum distance
41 while (left < right) {
42 // Calculate mid point (biased towards upper half)
43 const mid = left + Math.floor((right - left + 1) / 2);
44
45 if (isValidDistance(mid)) {
46 // If mid is achievable, try for a larger value
47 left = mid;
48 } else {
49 // If mid is not achievable, search for a smaller value
50 right = mid - 1;
51 }
52 }
53
54 return left;
55}
56
Time and Space Complexity
Time Complexity: O(n × log M)
, where n
is the length of the start
array and M
represents the maximum possible score range, which is start[-1] + d - start[0]
after sorting.
- Sorting the
start
array takesO(n log n)
time. - The binary search performs
O(log M)
iterations, whereM = start[-1] + d - start[0]
is the search range. - In each binary search iteration, the
check
function is called, which iterates through alln
elements in the sortedstart
array, takingO(n)
time. - Therefore, the binary search contributes
O(n × log M)
to the time complexity. - Since
O(n × log M)
typically dominatesO(n log n)
for large values ofM
, the overall time complexity isO(n × log M)
.
Space Complexity: O(1)
if we don't count the space used for sorting (in-place sorting), or O(log n)
if we consider the recursive stack space used by the sorting algorithm.
- The algorithm uses only a constant amount of extra space for variables like
l
,r
,mid
, andlast
in thecheck
function. - The sorting operation may use
O(log n)
space for the recursive call stack in typical implementations like Timsort (Python's default). - Following the reference answer's convention, the space complexity is considered
O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Boundary Calculation
Pitfall: Using mid = (left + right) // 2
instead of mid = (left + right + 1) // 2
when updating left = mid
.
This leads to an infinite loop when left = right - 1
. For example:
- If
left = 3
andright = 4
- Using
mid = (3 + 4) // 2 = 3
- If the check passes, we set
left = mid = 3
- The loop continues infinitely since
left
doesn't change
Solution: Always use mid = (left + right + 1) // 2
(or (left + right + 1) >> 1
) when the update pattern is left = mid
and right = mid - 1
.
2. Not Sorting the Intervals Before Processing
Pitfall: Forgetting to sort the start
array before applying the greedy algorithm in the validation function.
Without sorting, the greedy approach fails because we can't guarantee that placing integers from left to right will give us an optimal solution. Consider:
start = [10, 1]
,d = 2
- Without sorting, we'd try to place at interval
[10, 12]
first, then[1, 3]
- This makes validation unnecessarily complex and potentially incorrect
Solution: Always sort the intervals first: sorted_start = sorted(start)
3. Off-by-One Error in Binary Search Loop Condition
Pitfall: Using while left <= right
instead of while left < right
.
With left <= right
, when left == right
, the loop continues unnecessarily, and the final assignment could be incorrect depending on the last check result.
Solution: Use while left < right
for this binary search pattern where we're looking for the maximum valid value.
4. Incorrect Greedy Placement in Validation
Pitfall: Placing integers at arbitrary positions within intervals instead of following the greedy principle of placing at the earliest valid position.
For example, placing at last_position + min_distance
without checking if it exceeds the interval start:
# Wrong approach last_position = last_position + min_distance
This fails when the calculated position is before the interval start.
Solution: Use last_position = max(start_pos, last_position + min_distance)
to ensure we place at the earliest valid position within the interval.
5. Integer Overflow in Large Inputs
Pitfall: Not considering potential overflow when calculating positions or differences, especially with large values of start[i]
and d
.
Solution: In Python, this is generally not an issue due to arbitrary precision integers, but in other languages, use appropriate data types (e.g., long long
in C++) or check for overflow conditions.
How does merge sort divide the problem into subproblems?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!