Facebook Pixel

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 the i-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] and d = 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 and 4 from the second, the score is |3 - 4| = 1
  • If you pick 1 from the first interval and 6 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Placing an integer further left gives more room for subsequent integers
  2. 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 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 feasible(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        last_true_index = 0  # Track the answer (0 is always achievable)
49
50        # Binary search for the maximum achievable minimum distance
51        # Using template: find last position where feasible(mid) is true
52        while left <= right:
53            mid = (left + right) // 2
54
55            if feasible(mid):
56                # mid is achievable, record it and try for larger value
57                last_true_index = mid
58                left = mid + 1
59            else:
60                # mid is not achievable, try smaller value
61                right = mid - 1
62
63        return last_true_index
64
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        int lastTrueIndex = 0;  // Track the answer (0 is always achievable)
27
28        // Binary search for the maximum valid minimum distance
29        // Using template: find last position where feasible(mid) is true
30        while (left <= right) {
31            int mid = left + (right - left) / 2;
32
33            if (feasible(mid)) {
34                // mid is achievable, record it and try for larger value
35                lastTrueIndex = mid;
36                left = mid + 1;
37            } else {
38                // mid is not achievable, try smaller value
39                right = mid - 1;
40            }
41        }
42
43        return lastTrueIndex;
44    }
45
46    /**
47     * Checks if it's possible to place all items with at least 'minDistance' between them.
48     * Uses greedy approach: place each item as early as possible while maintaining minimum distance.
49     *
50     * @param minDistance The minimum distance to maintain between consecutive positions
51     * @return true if the arrangement is possible, false otherwise
52     */
53    private boolean feasible(int minDistance) {
54        // Track the position where we placed the last item
55        long lastPlacedPosition = Long.MIN_VALUE;
56
57        // Try to place each item greedily
58        for (int startPosition : positions) {
59            // Check if placing at the rightmost position still violates minimum distance
60            if (lastPlacedPosition + minDistance > startPosition + range) {
61                // Cannot maintain minimum distance even at the rightmost possible position
62                return false;
63            }
64
65            // Place the current item at the earliest valid position
66            // Either at its start position or at minimum distance from the last placed item
67            lastPlacedPosition = Math.max(startPosition, lastPlacedPosition + minDistance);
68        }
69
70        return true;
71    }
72}
73
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 feasible = [&](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        int lastTrueIndex = 0;  // Track the answer (0 is always achievable)
34
35        // Binary search using template: find last position where feasible(mid) is true
36        while (left <= right) {
37            int mid = left + (right - left) / 2;
38
39            if (feasible(mid)) {
40                // mid is achievable, record it and try for larger value
41                lastTrueIndex = mid;
42                left = mid + 1;
43            } else {
44                // mid is not achievable, try smaller value
45                right = mid - 1;
46            }
47        }
48
49        return lastTrueIndex;  // Return the maximum achievable minimum distance
50    }
51};
52
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: number = 0;
15    let right: number = start[start.length - 1] + d - start[0];
16    let lastTrueIndex: number = 0;  // Track the answer (0 is always achievable)
17
18    /**
19     * Checks if a given minimum distance is achievable
20     * @param minDistance - The minimum distance to check
21     * @returns true if the minimum distance is achievable, false otherwise
22     */
23    const feasible = (minDistance: number): boolean => {
24        // Track the last chosen position
25        let lastPosition: number = -Infinity;
26
27        // Try to place each element with at least minDistance apart
28        for (const startPosition of start) {
29            // Check if we can place the current element
30            // If the required next position exceeds the maximum allowed position
31            if (lastPosition + minDistance > startPosition + d) {
32                return false;
33            }
34            // Place the element at the earliest valid position
35            lastPosition = Math.max(startPosition, lastPosition + minDistance);
36        }
37
38        return true;
39    };
40
41    // Binary search using template: find last position where feasible(mid) is true
42    while (left <= right) {
43        const mid: number = Math.floor((left + right) / 2);
44
45        if (feasible(mid)) {
46            // mid is achievable, record it and try for larger value
47            lastTrueIndex = mid;
48            left = mid + 1;
49        } else {
50            // mid is not achievable, try smaller value
51            right = mid - 1;
52        }
53    }
54
55    return lastTrueIndex;
56}
57

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: left = 0 (minimum possible difference)
  • Right boundary: right = 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 the standard boundary-finding template to find the last true index - the largest minimum difference that satisfies our constraints:

left, right = 0, max_range
last_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):  # can we achieve minimum distance of mid?
        last_true_index = mid
        left = mid + 1  # mid works, try larger values
    else:
        right = mid - 1  # mid doesn't work, try smaller values

return last_true_index

Note: For "maximize minimum" problems, we're looking for the last position where the condition is true (the array looks like true, true, true, false, false...). When feasible, we record the answer and search right for a larger value.

Step 4: Validation Function feasible(min_distance) The core logic lies in the feasible function that determines if a minimum difference min_distance is achievable:

def feasible(min_distance: int) -> bool:
    last = -inf  # Track the position of the last placed integer
    for st in start:
        if last + min_distance > st + d:
            return False  # Cannot place an integer in this interval
        last = max(st, last + min_distance)  # Place at 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 min_distance from the previous one
    • If last + min_distance > st + d, it means even placing at the rightmost position st + d wouldn't maintain the minimum distance, so return False
    • Otherwise, place the integer at max(st, last + min_distance):
      • If last + min_distance ≤ st, place at the interval's start st
      • If last + min_distance > st, place at last + min_distance (the earliest position maintaining the minimum distance)
  • 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 Evaluator

Example 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 at max(2, -∞ + 8) = 2. Update last = 2
    • Place in interval [6, 11]: need at least 2 + 8 = 10, place at max(6, 10) = 10. Update last = 10
    • Place in interval [13, 18]: need at least 10 + 8 = 18, place at max(13, 18) = 18. Update last = 18
    • Place in interval [13, 18]: need at least 18 + 8 = 26, but interval ends at 18. Cannot place!
  • Result: check(8) = False, so r = 7

Iteration 2:

  • mid = (0 + 7 + 1) // 2 = 4
  • Check if minimum difference of 4 is achievable:
    • Place in interval [2, 7]: place at max(2, -∞ + 4) = 2. Update last = 2
    • Place in interval [6, 11]: place at max(6, 2 + 4) = 6. Update last = 6
    • Place in interval [13, 18]: place at max(13, 6 + 4) = 13. Update last = 13
    • Place in interval [13, 18]: place at max(13, 13 + 4) = 17. Update last = 17
  • Result: check(4) = True, so l = 4

Iteration 3:

  • mid = (4 + 7 + 1) // 2 = 6
  • Check if minimum difference of 6 is achievable:
    • Place in interval [2, 7]: place at 2. Update last = 2
    • Place in interval [6, 11]: place at max(6, 2 + 6) = 8. Update last = 8
    • Place in interval [13, 18]: place at max(13, 8 + 6) = 14. Update last = 14
    • Place in interval [13, 18]: need at least 14 + 6 = 20, but interval ends at 18. Cannot place!
  • Result: check(6) = False, so r = 5

Iteration 4:

  • mid = (4 + 5 + 1) // 2 = 5
  • Check if minimum difference of 5 is achievable:
    • Place in interval [2, 7]: place at 2. Update last = 2
    • Place in interval [6, 11]: place at max(6, 2 + 5) = 7. Update last = 7
    • Place in interval [13, 18]: place at max(13, 7 + 5) = 13. Update last = 13
    • Place in interval [13, 18]: place at max(13, 13 + 5) = 18. Update last = 18
  • Result: check(5) = True, so l = 5

Iteration 5:

  • Now l = 5 and r = 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.

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 takes O(n log n) time.
  • The binary search performs O(log M) iterations, where M = start[-1] + d - start[0] is the search range.
  • In each binary search iteration, the check function is called, which iterates through all n elements in the sorted start array, taking O(n) time.
  • Therefore, the binary search contributes O(n × log M) to the time complexity.
  • Since O(n × log M) typically dominates O(n log n) for large values of M, the overall time complexity is O(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, and last in the check 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. Using Wrong Binary Search Template Variant

Pitfall: Using a non-standard binary search template that doesn't properly track the answer. Many developers use while left < right with left = mid and right = mid - 1, which requires ceiling division to avoid infinite loops.

Incorrect (prone to infinite loop):

while left < right:
    mid = (left + right) // 2  # WRONG! Should use ceiling division
    if feasible(mid):
        left = mid
    else:
        right = mid - 1

Correct (using template with lastTrueIndex):

left, right = 0, max_range
last_true_index = 0  # Track the answer

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        last_true_index = mid
        left = mid + 1  # Try for larger value
    else:
        right = mid - 1

return last_true_index

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. Confusing First-True vs Last-True Binary Search

Pitfall: For "maximize minimum" problems, the feasibility pattern is true, true, true, false, false... We need to find the last true position (maximum feasible value), not the first true.

Key difference:

  • First true (minimize): When feasible(mid), record answer and search left (right = mid - 1)
  • Last true (maximize): When feasible(mid), record answer and search right (left = mid + 1)

This problem requires the "last true" pattern since we want to maximize the minimum distance.

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.

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

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More