2557. Maximum Number of Integers to Choose From a Range II


Problem Description

We are given an integer array banned, a positive integer n indicating the upper limit of an integer range starting from 1, and a positive integer maxSum which serves as the cap for the sum of integers we can select. The task is to choose a collection of distinct integers within this range from 1 to n, ensuring two conditions: first, that none of the integers are present in the banned array, and second, that the cumulative sum of all chosen integers does not exceed maxSum. The objective is to maximize the number of integers chosen while adhering to the stated constraints.

Intuition

To maximize the number of chosen integers, while considering the maxSum limitation and banned list, we must take a strategic approach in picking the integers. A naive approach might be to start from 1 and keep adding the next non-banned integer until we reach or exceed maxSum. However, this might not optimize for the maximum number of integers since larger values will use up the allowed sum more quickly.

Instead, the intuition is to proceed in a way that we try to include as many small non-banned numbers as possible because smaller numbers have smaller sums, enabling us to pick more numbers while staying under maxSum.

We can achieve this by:

  • Extending the banned list with two sentinel values, 0 and n+1, which represent the lower and upper boundaries beyond which we can't select integers.
  • Sorting the extended banned list to be able to iterate through banned ranges efficiently.
  • Using binary search to find the maximum count of integers that can be selected between each pair of consecutive banned numbers, without exceeding maxSum.

This can be seen in the provided solution code where the banned list is augmented and then sorted. The pairwise function (assumed to be similar to itertools.pairwise) iterates through adjacent elements providing pairs of banned integers so we can calculate the potential numbers between them.

The binary search within the loop efficiently finds the maximum number of selectable integers in each range. If adding one more integer exceeds maxSum, the binary search moves left, else it moves right to expand the count. Once the range is found, we add it to the ans tally and deduct the sum of those integers from maxSum to ensure we don't exceed the overall allowed sum.

The answer is returned as soon as maxSum is not enough to add more integers, guaranteeing the focus on maximizing quantity with the smallest possible numbers while adhering to maxSum.

Learn more about Greedy, Binary Search and Sorting patterns.

Solution Approach

The code below implements a strategic method to solve the problem by maximizing the number of integers chosen within the given constraints. Here's a deeper dive into the solution's methodology:

First, the solution uses both the concept of sorting and binary search, combining these algorithms for efficient computation. The banned list is modified to include two extra elements, 0 and n + 1, to handle edge cases seamlessly.

The data structures used include:

  • A list (banned) that contains the original banned numbers plus the two sentinels (0 and n + 1).
  • A set, to ensure all elements in banned are unique.
  • A sorted list version of that set, to allow for binary search operations.

The code then traverses the sorted banned list, searching for the maximum number of selectable integers between each pair of consecutive banned numbers (now including the sentinels). This is done using the pairwise iteration which is assumed to yield a tuple (i, j) where i and j are adjacent banned numbers.

For each pair (i, j), the algorithm finds the potential gap (j - i - 1) between them to determine the number of selectable integers. It then employs binary search within this gap to find the maximum count of integers (left) which can be chosen without exceeding maxSum. The binary search checks if the sum of an arithmetic series from i + 1 to i + mid is less than or equal to maxSum.

The arithmetic series sum is calculated with the formula:

(i + 1 + i + mid) * mid / 2

This sum formula computes the sum of the first mid terms of the natural numbers starting at i + 1.

After finding the left value for each range, the algorithm updates the answer (ans) with the count of new integers added, and subtracts the sum of those integers from maxSum.

The process stops when maxSum becomes zero or negative, indicating that no additional integers can be chosen without exceeding the maximum sum constraint at which point the current value of ans is returned.

Through this approach, the solution efficiently maximizes the number of integers chosen without exceeding maxSum and while respecting the banned list.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's consider an example to illustrate the solution approach.

Suppose we are given the following parameters:

  • banned = [3, 6, 10]
  • n = 15
  • maxSum = 25

Now, let's use the solution approach step by step:

  1. Extend and Sort the banned List: The banned array is extended to include two sentinel values, which here would be 0 and n+1 = 16. After including these, the banned array is sorted. The updated banned list looks like this: [0, 3, 6, 10, 16].

  2. Iterate Using Pairwise: Using the pairwise function, we obtain the adjacent pairs of banned numbers: (0, 3), (3, 6), (6, 10), and (10, 16).

  3. Binary Search on Each Range: For each pair (i, j), we perform a binary search to find the maximum count of integers that can be selected between i+1 and j-1 without exceeding maxSum. Here's how it works for each pair:

    • For (0, 3), the selectable range is 1 and 2. Since the sum 1 + 2 is 3, which is less than maxSum, we include both numbers. Now, maxSum is reduced to 22.

    • For (3, 6), the selectable range is 4 and 5. The sum 4 + 5 is 9, which is within the remaining maxSum of 22. We include these, and maxSum reduces to 13.

    • Next, for (6, 10), the selectable range is 7, 8, and 9. The binary search finds that we can only take 7 and 8 without exceeding the new maxSum of 13, because their sum is 15, and adding 9 would bring the total to 24, over the cap. So we add 7 and 8 and reduce maxSum to 13 - (7 + 8) = -2. Now maxSum is negative.

    • At this point, we cannot choose any more numbers because the remaining maxSum is not enough to include more integers. We stop and do not proceed to the pair (10, 16).

  4. End Result: The maximum number of integers chosen without exceeding maxSum and avoiding the banned list is [1, 2, 4, 5, 7, 8]. The ans would be 6, which is the count of these chosen integers.

This example walk-through demonstrates how the problem is solved using the described approach, ensuring that we maximize the number of integers chosen within the constraints.

Solution Implementation

1class Solution:
2    def maxCount(self, banned: List[int], n: int, maxSum: int) -> int:
3        # Extend the banned list to include 0 and n+1 to simplify range calculations
4        banned.extend([0, n + 1])
5        # Ensure the banned list is unique and sorted
6        banned = sorted(set(banned))
7      
8        # Initialize the answer to 0
9        answer = 0
10      
11        # Loop through each pair of consecutive banned numbers (Using pairwise requires itertools in Python 3.8+)
12        for lower_bound, upper_bound in zip(banned, banned[1:]):
13            # We use binary search to find the maximum count of numbers
14            # between a pair of banned numbers that we can sum without exceeding maxSum
15            left, right = 0, upper_bound - lower_bound - 1
16          
17            while left < right:
18                # Calculate the midpoint for binary search
19                mid = (left + right + 1) // 2
20              
21                # Calculate the sum of an arithmetic series from lower_bound+1 to lower_bound+mid
22                if ((lower_bound + 1 + lower_bound + mid) * mid) // 2 <= maxSum:
23                    # If the sum is less than or equal to maxSum, move the left bound up
24                    left = mid
25                else:
26                    # Otherwise, move the right bound down
27                    right = mid - 1
28          
29            # Add the count of numbers found to the answer
30            answer += left
31            # Decrease maxSum by the sum of numbers we've added to the answer
32            maxSum -= ((lower_bound + 1 + lower_bound + left) * left) // 2
33          
34            # If maxSum becomes zero or negative, we cannot add any more numbers
35            if maxSum <= 0:
36                break
37      
38        # Return the total count of numbers we can sum
39        return answer
40
1class Solution {
2    // Method to calculate the maximum count of consecutive numbers not exceeding maxSum
3    // with the given constraints.
4    public int maxCount(int[] banned, int n, long maxSum) {
5        // Create a hash set to store banned numbers along with the boundaries.
6        Set<Integer> bannedIndicesSet = new HashSet<>();
7        bannedIndicesSet.add(0);        // Add lower boundary
8        bannedIndicesSet.add(n + 1);    // Add upper boundary
9      
10        // Add all banned numbers to the set
11        for (int bannedNumber : banned) {
12            bannedIndicesSet.add(bannedNumber);
13        }
14      
15        // Convert the set to a list and sort it to process intervals between banned numbers.
16        List<Integer> bannedIndicesList = new ArrayList<>(bannedIndicesSet);
17        Collections.sort(bannedIndicesList);
18      
19        // Initialize the answer which will store the maximum count of consecutive numbers
20        int maxCount = 0;
21      
22        for (int k = 1; k < bannedIndicesList.size(); ++k) {
23            // Get the interval boundaries from the sorted list
24            int intervalStart = bannedIndicesList.get(k - 1);
25            int intervalEnd = bannedIndicesList.get(k);
26          
27            // Initialize binary search bounds
28            int left = 0;
29            int right = intervalEnd - intervalStart - 1;
30          
31            while (left < right) {
32                // Use unsigned right shift for division by 2 to avoid negative overflow
33                int mid = (left + right + 1) >>> 1;
34              
35                // Check if the sum of consecutive numbers from intervalStart+1 to intervalStart+mid
36                // fits in the maxSum using the arithmetic progression sum formula
37                if ((intervalStart + 1 + intervalStart + mid) * 1L * mid / 2 <= maxSum) {
38                    left = mid;
39                } else {
40                    right = mid - 1;
41                }
42            }
43          
44            // Update maxCount and decrease maxSum by the sum of consecutive numbers found
45            maxCount += left;
46            maxSum -= (intervalStart + 1 + intervalStart + left) * 1L * left / 2;
47          
48            // If maxSum is exhausted, break out of the loop
49            if (maxSum <= 0) {
50                break;
51            }
52        }
53      
54        // Return the calculated maximum count
55        return maxCount;
56    }
57}
58
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6    // This function calculates the maximum count of numbers that can be added
7    // to the intervals between banned numbers without exceeding maxSum.
8    int maxCount(std::vector<int>& banned, int n, long long maxSum) {
9        // Add boundary markers for the start and end of possible number range.
10        banned.push_back(0);           // Add a banned number at the start.
11        banned.push_back(n + 1);       // Add a banned number at the end.
12      
13        // Sort the banned numbers to process them in order.
14        std::sort(banned.begin(), banned.end());
15      
16        // Remove duplicate entries if any, to handle only unique banned numbers.
17        banned.erase(std::unique(banned.begin(), banned.end()), banned.end());
18      
19        int answer = 0;  // Initialize the answer to zero.
20      
21        // Iterate through the intervals between banned numbers.
22        for (int index = 1; index < banned.size(); ++index) {
23            int start = banned[index - 1], end = banned[index];
24            int low = 0, high = end - start - 1;  // Low and high limits for current interval.
25          
26            // Perform binary search within the interval to find the max count.
27            while (low < high) {
28                int mid = low + ((high - low + 1) / 2);
29                // Calculate the sum of arithmetic progression.
30                if ((start + 1 + start + mid) * 1LL * mid / 2 <= maxSum) {
31                    low = mid;  // If the sum is within limit, search right side.
32                } else {
33                    high = mid - 1;  // If sum exceeds limit, search left side.
34                }
35            }
36          
37            answer += low;  // Add the found count to the answer.
38            // Subtract the sum of the found numbers from maxSum.
39            maxSum -= (start + 1 + start + low) * 1LL * low / 2;
40          
41            if (maxSum <= 0) {  // If there's no sum left to add numbers, break out.
42                break;
43            }
44        }
45      
46        return answer;  // Return the total count of numbers that can be added.
47    }
48};
49
1function maxCount(banned: number[], n: number, maxSum: number): number {
2  // Add boundary markers for the start and end of the possible number range.
3  banned.push(0);        // Add a banned number at the start.
4  banned.push(n + 1);    // Add a banned number at the end.
5
6  // Sort the banned numbers to process them in order.
7  banned.sort((a, b) => a - b);
8
9  // Remove duplicate entries if any, to handle only unique banned numbers.
10  banned = Array.from(new Set(banned));
11
12  let answer = 0;  // Initialize the answer to zero.
13
14  // Iterate through the intervals between banned numbers.
15  for (let index = 1; index < banned.length; ++index) {
16      let start = banned[index - 1], end = banned[index];
17      let low = 0, high = end - start - 1;  // Low and high limits for the current interval.
18
19      // Perform binary search within the interval to find the max count.
20      while (low < high) {
21          let mid = low + Math.floor((high - low + 1) / 2);
22          // Calculate the sum of the arithmetic progression.
23          if ((start + 1 + start + mid) * mid / 2 <= maxSum) {
24              low = mid;  // If the sum is within the limit, search the right side.
25          } else {
26              high = mid - 1;  // If sum exceeds the limit, search the left side.
27          }
28      }
29
30      answer += low;  // Add the found count to the answer.
31      // Subtract the sum of the found numbers from maxSum.
32      maxSum -= (start + 1 + start + low) * low / 2;
33
34      if (maxSum <= 0) {  // If there's no sum left to add numbers, break out.
35          break;
36      }
37  }
38
39  return answer;  // Return the total count of numbers that can be added.
40}
41
42// Example usage:
43// let bannedNumbers = [3, 6, 14];
44// let n = 15;
45// let maxSum = 25;
46// console.log(maxCount(bannedNumbers, n, maxSum)); // Should print the result of the function.
47

Time and Space Complexity

Time Complexity

The time complexity of the code is determined by mainly two operations: sorting the banned list and performing a binary search for each pair in the list after incorporating the additional elements and removing duplicates.

  1. Sorting the banned list has a time complexity of O(K * log(K)), where K is the number of elements in the banned list after extending it with [0, n + 1]. Since ban is constructed by converting banned to a set and sorting, the number of elements K is at most the original length of banned plus 2.

  2. The pairwise function results in O(K - 1) iterations since it will create pairs of adjacent elements in the sorted ban list.

  3. For each pair (i, j) produced by pairwise, there is a binary search running to determine how many integers can be counted between i and j without exceeding maxSum. The binary search runs in O(log(N)) time in the worst case, where N is the difference (j - i - 1).

Therefore, if M is the maximum value in the list before sorting (and after adding 0 and n + 1), then the worst-case scenario for the binary search is O(log(M)) time for each of K - 1 iterations.

Combining these, the total time complexity is O(K * log(K) + (K - 1) * log(M)) = O(K * log(K) + K * log(M)). Since K can be at most n + 2, the time complexity in terms of n is O((n + 2) * log(n + 2) + (n + 1) * log(n)).

Space Complexity

The space complexity of the code involves the additional space used by the ban list and the space for the variables used in the binary search.

  1. The ban list stores at most K elements, which is at most the length of banned plus 2, thus giving us a space complexity of O(K).

  2. The variables used in the binary search (left, right, mid, ans, and maxSum) require constant space O(1).

Therefore, the space complexity of the algorithm is O(K). This is O(n) in terms of the maximum possible length of the ban list when n is large compared to the number of banned elements.

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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


Load More