Facebook Pixel

1648. Sell Diminishing-Valued Colored Balls

Problem Description

You have different colored balls in your inventory, and a customer wants to buy a certain number of balls. The interesting part is how the balls are valued.

Each ball's value equals the current count of balls of that color in your inventory. For example, if you have 6 yellow balls:

  • The first yellow ball sells for 6
  • After selling it, you have 5 yellow balls left
  • The second yellow ball sells for 5
  • The third sells for 4, and so on

You're given:

  • inventory: an array where inventory[i] represents how many balls of color i you initially have
  • orders: the total number of balls the customer wants to buy

You can sell balls of any color in any order you choose. Your goal is to maximize the total value you receive from selling exactly orders balls.

Since the answer might be very large, return it modulo 10^9 + 7.

The key insight is that to maximize profit, you should always sell the most valuable balls first. Since a ball's value equals its current count, you want to sell from the colors with the highest counts, reducing them gradually to keep the values as high as possible throughout the selling process.

For example, if inventory = [2, 5] and orders = 4:

  • You could sell balls valued at: 5, 4, 3, 2 (selling from the second color until they're equal, then alternating)
  • Total value: 5 + 4 + 3 + 2 = 14
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we want to maximize profit and each ball's value equals its current count, we should always sell the highest-valued balls first. This creates a greedy strategy: keep selling from whichever color currently has the most balls.

Think of it like this - if we have colors with counts [2, 5, 8], we'd want to sell from the color with 8 balls first (getting value 8), then from 7 (getting value 7), and so on. We're essentially "shaving off" the peaks to keep all values as high as possible.

The key observation is that we'll be selling balls in layers. Imagine the inventory counts as bars in a histogram. We want to cut horizontal slices from the top:

  • First, we bring down the tallest bar(s) to match the second tallest
  • Then we bring those down together to match the third tallest
  • And so on

For efficiency, instead of selling balls one by one, we can calculate how many balls we'd sell in each "layer" at once. If we have multiple colors at the same height, we sell from all of them simultaneously.

For example, with inventory = [3, 5]:

  • We first sell 2 balls from the second color (values 5 and 4), bringing it down to 3
  • Now both colors have 3 balls, so we sell from both alternately

The mathematical insight is that when selling x balls from a color that currently has n balls, we're selling balls valued at n, n-1, n-2, ..., n-x+1. This is an arithmetic sequence with sum (first + last) * count / 2 = (n + (n-x+1)) * x / 2.

By sorting the inventory in descending order and processing these layers efficiently, we can calculate the maximum profit without simulating each individual sale.

Learn more about Greedy, Math, Binary Search, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation follows a greedy approach by processing inventory levels from highest to lowest:

  1. Sort the inventory in descending order: This allows us to easily identify and process the highest-valued balls first.

  2. Process balls in layers: We iterate through the sorted inventory, selling balls in "layers" - bringing down the tallest bars to match the next level.

  3. Key variables:

    • i: tracks how many colors are at the current highest level
    • inventory[0]: always represents the current highest count (since we're modifying it in-place)
    • nxt: the next lower level we need to bring our current colors down to
  4. Main loop logic:

    • First, count how many colors (cnt) have the same count as inventory[0]
    • Determine the next level (nxt) - either the next different value in inventory or 0
    • Calculate how many balls would be sold if we bring all cnt colors from inventory[0] down to nxt: tot = cnt * (inventory[0] - nxt)
  5. Two cases for selling:

    Case 1: We have more balls to sell than orders remaining (tot > orders):

    • Calculate how many complete levels we can drop: decr = orders // cnt
    • Use arithmetic sequence formula to calculate value: sum from (inventory[0] - decr + 1) to inventory[0], multiplied by cnt colors
    • Formula: (first + last) * count / 2 * cnt = (a1 + an) * decr / 2 * cnt
    • Handle remaining balls: (inventory[0] - decr) * (orders % cnt)

    Case 2: We can sell all balls in this layer (tot <= orders):

    • Sell all balls from inventory[0] down to nxt + 1 for all cnt colors
    • Use arithmetic sequence formula: (nxt + 1 + inventory[0]) * x / 2 * cnt
    • Update inventory[0] = nxt for the next iteration
  6. Modulo operation: Apply mod = 10^9 + 7 to keep the answer within bounds.

The algorithm efficiently calculates the maximum profit by:

  • Avoiding individual ball sales through batch calculations
  • Using arithmetic sequence formulas for sum calculations
  • Processing colors at the same level together
  • Time complexity: O(n log n) for sorting, where n is the number of colors

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 inventory = [2, 8, 4, 10, 6] and orders = 20.

Step 1: Sort inventory in descending order

  • inventory = [10, 8, 6, 4, 2]
  • We'll process from highest to lowest values

Step 2: Process first layer (10 → 8)

  • Current highest: inventory[0] = 10
  • Count of colors at this level: cnt = 1 (only one color has 10 balls)
  • Next level: nxt = 8 (the next highest count)
  • Total balls if we bring 10 down to 8: tot = 1 * (10 - 8) = 2
  • Since tot = 2 <= orders = 20, we sell all 2 balls
  • Value gained: 10 + 9 = 19 (using formula: (8+1 + 10) * 2 / 2 * 1 = 19)
  • Update: inventory[0] = 8, orders = 18, result = 19

Step 3: Process second layer (8 → 6)

  • Current highest: inventory[0] = 8
  • Count of colors at this level: cnt = 2 (two colors have 8 balls: original 10 and 8)
  • Next level: nxt = 6
  • Total balls if we bring both down to 6: tot = 2 * (8 - 6) = 4
  • Since tot = 4 <= orders = 18, we sell all 4 balls
  • Value gained: (8 + 7) * 2 = 30 (two colors selling 8 and 7 each)
  • Using formula: (6+1 + 8) * 2 / 2 * 2 = 30
  • Update: inventory[0] = 6, orders = 14, result = 49

Step 4: Process third layer (6 → 4)

  • Current highest: inventory[0] = 6
  • Count of colors at this level: cnt = 3 (three colors have 6 balls)
  • Next level: nxt = 4
  • Total balls if we bring all three down to 4: tot = 3 * (6 - 4) = 6
  • Since tot = 6 <= orders = 14, we sell all 6 balls
  • Value gained: (6 + 5) * 3 = 33 (three colors selling 6 and 5 each)
  • Using formula: (4+1 + 6) * 2 / 2 * 3 = 33
  • Update: inventory[0] = 4, orders = 8, result = 82

Step 5: Process fourth layer (4 → 2)

  • Current highest: inventory[0] = 4
  • Count of colors at this level: cnt = 4 (four colors have 4 balls)
  • Next level: nxt = 2
  • Total balls if we bring all four down to 2: tot = 4 * (4 - 2) = 8
  • Since tot = 8 == orders = 8, we sell exactly all remaining orders!
  • Value gained: (4 + 3) * 4 = 28 (four colors selling 4 and 3 each)
  • Using formula: (2+1 + 4) * 2 / 2 * 4 = 28
  • Update: orders = 0, result = 110

Final Answer: 110

The key insight is that we're always selling the highest-valued balls by processing inventory levels from top to bottom, calculating bulk sales using arithmetic sequence formulas rather than selling individual balls.

Solution Implementation

1class Solution:
2    def maxProfit(self, inventory: List[int], orders: int) -> int:
3        # Sort inventory in descending order to process highest value items first
4        inventory.sort(reverse=True)
5        MOD = 10**9 + 7
6        total_profit = 0
7        current_index = 0
8        inventory_size = len(inventory)
9      
10        while orders > 0:
11            # Find how many items have the same highest value
12            while current_index < inventory_size and inventory[current_index] >= inventory[0]:
13                current_index += 1
14          
15            # Determine the next lower price level
16            next_price_level = 0
17            if current_index < inventory_size:
18                next_price_level = inventory[current_index]
19          
20            # Number of items at current highest price
21            items_at_current_price = current_index
22          
23            # Calculate how many units we can sell from current price to next price level
24            price_difference = inventory[0] - next_price_level
25            total_units_available = items_at_current_price * price_difference
26          
27            if total_units_available > orders:
28                # Can't sell all available units, need to partially fulfill orders
29                full_levels_to_sell = orders // items_at_current_price
30              
31                # Calculate profit from selling complete levels
32                # Using arithmetic sequence sum: (first + last) * count / 2
33                first_price = inventory[0] - full_levels_to_sell + 1
34                last_price = inventory[0]
35                total_profit += (first_price + last_price) * full_levels_to_sell // 2 * items_at_current_price
36              
37                # Add profit from remaining partial level
38                remaining_orders = orders % items_at_current_price
39                total_profit += (inventory[0] - full_levels_to_sell) * remaining_orders
40              
41                orders = 0  # All orders fulfilled
42            else:
43                # Sell all units from current price down to next price level
44                first_price = next_price_level + 1
45                last_price = inventory[0]
46                total_profit += (first_price + last_price) * price_difference // 2 * items_at_current_price
47              
48                # Update the highest price to the next level
49                inventory[0] = next_price_level
50                orders -= total_units_available
51          
52            # Apply modulo to prevent integer overflow
53            total_profit %= MOD
54      
55        return total_profit
56
1class Solution {
2    private static final int MOD = (int) 1e9 + 7;
3
4    public int maxProfit(int[] inventory, int orders) {
5        // Sort inventory in ascending order
6        Arrays.sort(inventory);
7        int n = inventory.length;
8      
9        // Reverse array to get descending order (highest values first)
10        for (int left = 0, right = n - 1; left < right; ++left, --right) {
11            int temp = inventory[left];
12            inventory[left] = inventory[right];
13            inventory[right] = temp;
14        }
15      
16        long totalProfit = 0;
17        int currentIndex = 0;
18      
19        // Process orders while there are still orders to fulfill
20        while (orders > 0) {
21            // Find how many items have the same highest value
22            while (currentIndex < n && inventory[currentIndex] >= inventory[0]) {
23                ++currentIndex;
24            }
25          
26            // Get the next lower value (or 0 if we've reached the end)
27            int nextLowerValue = currentIndex < n ? inventory[currentIndex] : 0;
28          
29            // Number of items with the current highest value
30            int itemsWithSameValue = currentIndex;
31          
32            // Calculate how many items we can sell from current value down to next lower value
33            long priceReduction = inventory[0] - nextLowerValue;
34            long totalItemsToSell = itemsWithSameValue * priceReduction;
35          
36            if (totalItemsToSell > orders) {
37                // We have more items available than orders remaining
38                // Calculate how many full rounds we can sell
39                int fullRounds = orders / itemsWithSameValue;
40              
41                // Calculate profit using arithmetic sequence sum formula
42                // Sum from (inventory[0] - fullRounds + 1) to inventory[0]
43                long lowestPrice = inventory[0] - fullRounds + 1;
44                long highestPrice = inventory[0];
45                totalProfit += (lowestPrice + highestPrice) * fullRounds / 2 * itemsWithSameValue;
46              
47                // Add profit from remaining partial round
48                int remainingOrders = orders % itemsWithSameValue;
49                totalProfit += (lowestPrice - 1) * remainingOrders;
50            } else {
51                // We can sell all items from current value down to next lower value
52                // Calculate profit using arithmetic sequence sum formula
53                // Sum from (nextLowerValue + 1) to inventory[0]
54                long lowestPrice = nextLowerValue + 1;
55                long highestPrice = inventory[0];
56                totalProfit += (lowestPrice + highestPrice) * priceReduction / 2 * itemsWithSameValue;
57              
58                // Update the highest value to the next lower value
59                inventory[0] = nextLowerValue;
60            }
61          
62            // Update remaining orders
63            orders -= totalItemsToSell;
64          
65            // Apply modulo to prevent overflow
66            totalProfit %= MOD;
67        }
68      
69        return (int) totalProfit;
70    }
71}
72
1class Solution {
2public:
3    int maxProfit(vector<int>& inventory, int orders) {
4        const long MOD = 1e9 + 7;
5        long totalProfit = 0;
6        int currentIndex = 0;
7        int n = inventory.size();
8      
9        // Sort inventory in descending order to process highest values first
10        sort(inventory.rbegin(), inventory.rend());
11      
12        while (orders > 0) {
13            // Find how many balls have the same highest value
14            while (currentIndex < n && inventory[currentIndex] >= inventory[0]) {
15                currentIndex++;
16            }
17          
18            // Get the next lower value (or 0 if we've processed all groups)
19            int nextValue = (currentIndex < n) ? inventory[currentIndex] : 0;
20          
21            // Number of colors with the current highest value
22            int colorsWithMaxValue = currentIndex;
23          
24            // Calculate how many balls we can sell from current value down to nextValue
25            long valueDifference = inventory[0] - nextValue;
26            long totalBallsToSell = colorsWithMaxValue * valueDifference;
27          
28            if (totalBallsToSell > orders) {
29                // We can't sell all balls down to nextValue, so sell partially
30              
31                // Calculate how many complete levels we can sell
32                int completeLevels = orders / colorsWithMaxValue;
33              
34                // Calculate profit from complete levels using arithmetic sequence sum
35                long firstTerm = inventory[0] - completeLevels + 1;
36                long lastTerm = inventory[0];
37                totalProfit += (firstTerm + lastTerm) * completeLevels / 2 * colorsWithMaxValue;
38              
39                // Add profit from remaining balls at the next level
40                long remainingBalls = orders % colorsWithMaxValue;
41                totalProfit += (firstTerm - 1) * remainingBalls;
42            } else {
43                // We can sell all balls from current value down to nextValue
44              
45                // Calculate profit using arithmetic sequence sum formula
46                long firstTerm = nextValue + 1;
47                long lastTerm = inventory[0];
48                totalProfit += (firstTerm + lastTerm) * valueDifference / 2 * colorsWithMaxValue;
49              
50                // Update the highest value to nextValue for next iteration
51                inventory[0] = nextValue;
52            }
53          
54            // Update remaining orders
55            orders -= totalBallsToSell;
56          
57            // Apply modulo to prevent overflow
58            totalProfit %= MOD;
59        }
60      
61        return totalProfit;
62    }
63};
64
1function maxProfit(inventory: number[], orders: number): number {
2    const MOD = 1e9 + 7;
3    let totalProfit = 0;
4    let currentIndex = 0;
5    const n = inventory.length;
6  
7    // Sort inventory in descending order to process highest values first
8    inventory.sort((a, b) => b - a);
9  
10    while (orders > 0) {
11        // Find how many balls have the same highest value
12        while (currentIndex < n && inventory[currentIndex] >= inventory[0]) {
13            currentIndex++;
14        }
15      
16        // Get the next lower value (or 0 if we've processed all groups)
17        const nextValue = (currentIndex < n) ? inventory[currentIndex] : 0;
18      
19        // Number of colors with the current highest value
20        const colorsWithMaxValue = currentIndex;
21      
22        // Calculate how many balls we can sell from current value down to nextValue
23        const valueDifference = inventory[0] - nextValue;
24        const totalBallsToSell = colorsWithMaxValue * valueDifference;
25      
26        if (totalBallsToSell > orders) {
27            // We can't sell all balls down to nextValue, so sell partially
28          
29            // Calculate how many complete levels we can sell
30            const completeLevels = Math.floor(orders / colorsWithMaxValue);
31          
32            // Calculate profit from complete levels using arithmetic sequence sum
33            const firstTerm = inventory[0] - completeLevels + 1;
34            const lastTerm = inventory[0];
35            totalProfit += (firstTerm + lastTerm) * completeLevels / 2 * colorsWithMaxValue;
36          
37            // Add profit from remaining balls at the next level
38            const remainingBalls = orders % colorsWithMaxValue;
39            totalProfit += (firstTerm - 1) * remainingBalls;
40          
41            // All orders fulfilled, exit loop
42            orders = 0;
43        } else {
44            // We can sell all balls from current value down to nextValue
45          
46            // Calculate profit using arithmetic sequence sum formula
47            const firstTerm = nextValue + 1;
48            const lastTerm = inventory[0];
49            totalProfit += (firstTerm + lastTerm) * valueDifference / 2 * colorsWithMaxValue;
50          
51            // Update the highest value to nextValue for next iteration
52            inventory[0] = nextValue;
53          
54            // Update remaining orders
55            orders -= totalBallsToSell;
56        }
57      
58        // Apply modulo to prevent overflow
59        totalProfit %= MOD;
60    }
61  
62    return totalProfit;
63}
64

Time and Space Complexity

Time Complexity: O(n log n + n * k) where n is the length of inventory and k is the number of distinct price levels we process.

  • Sorting the inventory takes O(n log n)
  • In the worst case, we process each distinct value level in the inventory. For each level:
    • The inner while loop finds all items at the current maximum level, which collectively across all iterations examines each element once: O(n) total
    • Each iteration processes a batch of orders, reducing at least one element's value
  • The number of distinct price levels k is bounded by the maximum value in inventory, but in practice, we process groups of items together
  • Overall: O(n log n + n * k) where k ≤ max(inventory)

Space Complexity: O(1) if we don't count the space used for sorting (or O(log n) for the sorting stack space in Python's Timsort)

  • The algorithm uses only a constant amount of extra variables: mod, ans, i, n, nxt, cnt, x, tot, decr, a1, an
  • The inventory array is modified in-place
  • No additional data structures are created

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

Common Pitfalls

1. Integer Overflow in Arithmetic Calculations

The most critical pitfall in this problem is integer overflow when calculating the sum using the arithmetic sequence formula. Even though Python handles arbitrary precision integers, in languages like Java or C++, or when dealing with very large numbers, the intermediate calculations can overflow.

The Problem: When calculating (first_price + last_price) * count // 2 * items_at_current_price, the multiplication operations can produce values exceeding the integer limit before applying the modulo operation.

Example of Overflow:

# Problematic calculation
total_profit += (first_price + last_price) * full_levels_to_sell // 2 * items_at_current_price

If first_price = 10^9, last_price = 10^9, and items_at_current_price = 10^5, the intermediate result would be astronomical.

Solution: Apply modulo operations at each step and use modular arithmetic properties:

# Safe calculation with modulo at each step
sum_value = ((first_price + last_price) % MOD * full_levels_to_sell % MOD) % MOD
sum_value = (sum_value * pow(2, MOD - 2, MOD)) % MOD  # Modular division by 2
total_profit = (total_profit + sum_value * items_at_current_price % MOD) % MOD

Or alternatively, rearrange the formula to minimize large intermediate values:

# Rearrange to avoid large intermediate products
if full_levels_to_sell % 2 == 0:
    total_profit += ((first_price + last_price) % MOD) * (full_levels_to_sell // 2 % MOD) % MOD * items_at_current_price % MOD
else:
    total_profit += ((first_price + last_price) * full_levels_to_sell % MOD) * pow(2, MOD - 2, MOD) % MOD * items_at_current_price % MOD

2. Incorrect Handling of Edge Cases

The Problem: Not properly handling cases where:

  • All inventory counts are the same
  • Orders exceed total available balls
  • Single color in inventory

Solution: Add boundary checks:

# Add validation at the start
if not inventory or orders == 0:
    return 0

# Ensure we don't process beyond available inventory
total_available = sum(inventory)
orders = min(orders, total_available)

3. Off-by-One Errors in Price Calculations

The Problem: Confusion about inclusive vs exclusive ranges when calculating sums. For example, when bringing prices from inventory[0] down to next_price_level, should we include next_price_level in our sum?

Incorrect:

# This would include next_price_level in the sum
total_profit += (next_price_level + inventory[0]) * (inventory[0] - next_price_level + 1) // 2

Correct:

# We sell from inventory[0] down to (next_price_level + 1)
# So the range is [next_price_level + 1, inventory[0]] inclusive
first_price = next_price_level + 1
last_price = inventory[0]
count = last_price - first_price + 1  # This should equal price_difference
total_profit += (first_price + last_price) * count // 2 * items_at_current_price

4. Not Updating the Inventory Correctly

The Problem: After processing a batch of sales, forgetting to update the inventory state for the next iteration, or updating it incorrectly.

Solution: Ensure inventory[0] is properly updated to reflect the new highest value:

# After selling all units down to next_price_level
inventory[0] = next_price_level  # Critical update for next iteration

5. Inefficient Implementation Using Priority Queue

The Problem: Using a max-heap and popping/pushing individual elements, which seems intuitive but is inefficient:

# Inefficient approach - O(orders * log n)
import heapq
heap = [-x for x in inventory]
heapq.heapify(heap)
total = 0
for _ in range(orders):
    val = -heapq.heappop(heap)
    total = (total + val) % MOD
    heapq.heappush(heap, -(val - 1))

Solution: Use the batch processing approach shown in the main solution, which groups same-valued items and processes them together, achieving O(n log n) complexity.

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

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More