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 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 position st + d wouldn't maintain the minimum distance, so return False
    • Otherwise, place the integer at max(st, last + mi):
      • If last + mi ≤ st, place at the interval's start st
      • If last + mi > st, place at last + mi (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.

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 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. 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 and right = 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.

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

How does merge sort divide the problem into subproblems?


Recommended Readings

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

Load More