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 whereinventory[i]
represents how many balls of colori
you initially haveorders
: 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
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:
-
Sort the inventory in descending order: This allows us to easily identify and process the highest-valued balls first.
-
Process balls in layers: We iterate through the sorted inventory, selling balls in "layers" - bringing down the tallest bars to match the next level.
-
Key variables:
i
: tracks how many colors are at the current highest levelinventory[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
-
Main loop logic:
- First, count how many colors (
cnt
) have the same count asinventory[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 frominventory[0]
down tonxt
:tot = cnt * (inventory[0] - nxt)
- First, count how many colors (
-
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)
toinventory[0]
, multiplied bycnt
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 tonxt + 1
for allcnt
colors - Use arithmetic sequence formula:
(nxt + 1 + inventory[0]) * x / 2 * cnt
- Update
inventory[0] = nxt
for the next iteration
- Calculate how many complete levels we can drop:
-
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 EvaluatorExample 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 inner while loop finds all items at the current maximum level, which collectively across all iterations examines each element once:
- 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)
wherek ≤ 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.
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
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!