2819. Minimum Relative Loss After Buying Chocolates 🔒
Problem Description
You are given an array prices
representing chocolate prices and a 2D array queries
where each query is [k_i, m_i]
.
Alice and Bob want to buy chocolates with the following payment rules:
- If a chocolate's price is less than or equal to
k_i
, Bob pays the full price - If a chocolate's price is greater than
k_i
, Bob paysk_i
and Alice pays the remaining amount
For each query, Bob wants to select exactly m_i
chocolates to minimize his relative loss. The relative loss is defined as b_i - a_i
, where:
b_i
= total amount Bob paysa_i
= total amount Alice pays
Your task is to return an array ans
where ans[i]
represents Bob's minimum possible relative loss for queries[i]
.
Example breakdown:
-
If a chocolate costs
10
andk = 7
:- Bob pays
7
, Alice pays3
- Bob's relative loss =
7 - 3 = 4
- Bob pays
-
If a chocolate costs
5
andk = 7
:- Bob pays
5
, Alice pays0
- Bob's relative loss =
5 - 0 = 5
- Bob pays
The key insight is that for chocolates priced at or below k
, Bob's loss equals the full price. For chocolates priced above k
, Bob's loss is 2k - price
. This means Bob should strategically choose which chocolates to buy: cheaper ones (≤ k) give loss of price
, while expensive ones (> k) give loss of 2k - price
.
Intuition
Let's think about Bob's relative loss for each chocolate:
-
When price ≤ k: Bob pays the full
price
, Alice pays0
. Bob's relative loss =price - 0 = price
-
When price > k: Bob pays
k
, Alice paysprice - k
. Bob's relative loss =k - (price - k) = 2k - price
This reveals a crucial insight: for cheaper chocolates (≤ k), the loss increases with price. For expensive chocolates (> k), the loss decreases as price increases!
Now, imagine we sort all chocolates by price. We have two groups:
- Left group: chocolates with
price ≤ k
(loss =price
) - Right group: chocolates with
price > k
(loss =2k - price
)
In the left group, we want the smallest prices (smallest loss). In the right group, we want the largest prices (smallest loss since 2k - price
decreases as price increases).
So our strategy becomes: pick some chocolates from the left (cheapest ones) and some from the right (most expensive ones). But how many from each side?
Here's the key observation: as we move from left to right in our selection:
- Adding one more chocolate from the left increases our loss by
prices[i]
- Removing one chocolate from the right increases our loss by
2k - prices[j]
We want to balance these choices. The optimal point is when the marginal cost of taking one more from the left equals the marginal cost of taking one less from the right. This happens when prices[mid] ≈ 2k - prices[n - right]
.
This is why we use binary search to find the optimal split point - we're looking for the position where taking chocolates from the left becomes more expensive than taking them from the right.
Learn more about Binary Search, Prefix Sum and Sorting patterns.
Solution Approach
The solution uses sorting, binary search, and prefix sums to efficiently find the minimum relative loss for each query.
Step 1: Preprocessing
- Sort the
prices
array in ascending order - Build a prefix sum array
s
wheres[i]
represents the sum of the firsti
chocolates - This allows us to calculate range sums in O(1) time
Step 2: Process Each Query [k, m]
For each query, we need to find how many chocolates to take from the left (≤ k) versus the right (> k).
First Binary Search - Finding the boundary:
- Use
bisect_right(prices, k)
to find indexr
, the first position whereprice > k
- This tells us how many chocolates have
price ≤ k
(there arer
such chocolates)
Second Binary Search - Finding the optimal split:
The helper function f(k, m)
finds the optimal number of chocolates to take from the left:
- Initialize search range:
l = 0
,r = min(m, bisect_right(prices, k))
- We can't take more than
m
chocolates total - We can't take more chocolates from the left than exist with
price ≤ k
- We can't take more than
- Binary search for the optimal split point
mid
:right = m - mid
represents chocolates to take from the right- If
prices[mid] < 2k - prices[n - right]
:- Taking chocolate at position
mid
has lower loss than the rightmost chocolate - Move
l = mid + 1
(take more from left)
- Taking chocolate at position
- Otherwise:
- Taking from the right is better
- Move
r = mid
(take fewer from left)
Step 3: Calculate the Relative Loss
Once we have the optimal split (l
from left, m - l
from right):
- Loss from left chocolates:
s[l]
(sum of firstl
prices) - Loss from right chocolates:
2k * (m - l) - (s[n] - s[n - (m - l)])
- Each of the
m - l
chocolates contributes2k - price
to the loss s[n] - s[n - (m - l)]
gives the sum of the rightmostm - l
prices
- Each of the
- Total loss:
s[l] + 2k * (m - l) - (s[n] - s[n - (m - l)])
Time Complexity:
- Sorting: O(n log n)
- Per query: O(log n) for two binary searches
- Total: O(n log n + q log n) where q is the number of queries
Space Complexity: O(n) for the prefix sum array
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 to illustrate the solution approach.
Given:
prices = [3, 5, 7, 10, 15]
query = [8, 3]
(k = 8, m = 3)
Step 1: Preprocessing
- Sort prices (already sorted):
[3, 5, 7, 10, 15]
- Build prefix sum array:
s = [0, 3, 8, 15, 25, 40]
s[1] = 3
,s[2] = 3+5 = 8
,s[3] = 8+7 = 15
, etc.
Step 2: Analyze Bob's Relative Loss for Each Chocolate
For k = 8:
- Chocolate at price 3: Bob pays 3, Alice pays 0 → Loss = 3
- Chocolate at price 5: Bob pays 5, Alice pays 0 → Loss = 5
- Chocolate at price 7: Bob pays 7, Alice pays 0 → Loss = 7
- Chocolate at price 10: Bob pays 8, Alice pays 2 → Loss = 8 - 2 = 6
- Chocolate at price 15: Bob pays 8, Alice pays 7 → Loss = 8 - 7 = 1
Notice how the loss decreases for chocolates priced above k!
Step 3: Find the Boundary
- Use binary search to find where prices exceed k = 8
bisect_right([3, 5, 7, 10, 15], 8) = 3
- So we have 3 chocolates with price ≤ 8 (indices 0, 1, 2)
Step 4: Find Optimal Split (Binary Search)
We need to select exactly m = 3 chocolates. Let's evaluate different combinations:
-
Option 1: Take 3 from left (indices 0, 1, 2)
- Loss = 3 + 5 + 7 = 15
-
Option 2: Take 2 from left, 1 from right (indices 0, 1 and 4)
- Loss = 3 + 5 + (2×8 - 15) = 8 + 1 = 9
-
Option 3: Take 1 from left, 2 from right (indices 0 and 3, 4)
- Loss = 3 + (2×8 - 10) + (2×8 - 15) = 3 + 6 + 1 = 10
-
Option 4: Take 0 from left, 3 from right (indices 2, 3, 4)
- Loss = (2×8 - 7) + (2×8 - 10) + (2×8 - 15) = 9 + 6 + 1 = 16
The binary search finds that taking 2 from left and 1 from right is optimal:
- Start with l = 0, r = 3 (can't take more than 3 from left)
- mid = 1: Check if taking chocolate at index 1 (price 5) is better than taking from right
prices[1] = 5
vs2×8 - prices[4] = 1
- 5 > 1, so taking from right is better, set r = 1
- Binary search converges to l = 2
Step 5: Calculate Final Loss
With optimal split (2 from left, 1 from right):
- Loss from left:
s[2] = 8
(sum of prices 3 and 5) - Loss from right:
2×8×1 - (s[5] - s[4]) = 16 - 15 = 1
- Total loss: 8 + 1 = 9
Answer: Bob's minimum relative loss is 9.
Solution Implementation
1from typing import List
2from bisect import bisect_right
3from itertools import accumulate
4
5
6class Solution:
7 def minimumRelativeLosses(
8 self, prices: List[int], queries: List[List[int]]
9 ) -> List[int]:
10 """
11 Calculate minimum relative losses for each query.
12
13 Args:
14 prices: List of item prices
15 queries: List of [k, m] pairs where k is reference price and m is number of items
16
17 Returns:
18 List of minimum relative losses for each query
19 """
20
21 def find_optimal_split(reference_price: int, num_items: int) -> int:
22 """
23 Binary search to find optimal number of items to buy at original price.
24
25 The goal is to find the split point where buying some items at original price
26 and selling others at double the reference price minus original price
27 minimizes the total loss.
28
29 Args:
30 reference_price: The reference price k for comparison
31 num_items: Total number of items to process
32
33 Returns:
34 Optimal number of items to buy at original price
35 """
36 # Initialize binary search bounds
37 # Can't buy more than num_items or more than items <= reference_price
38 left = 0
39 right = min(num_items, bisect_right(prices, reference_price))
40
41 while left < right:
42 mid = (left + right) >> 1 # Equivalent to // 2
43 items_to_sell = num_items - mid
44
45 # Compare the marginal cost of buying at position mid
46 # versus selling at position (n - items_to_sell)
47 # If buying is cheaper, we should buy more items
48 if prices[mid] < 2 * reference_price - prices[n - items_to_sell]:
49 left = mid + 1
50 else:
51 right = mid
52
53 return left
54
55 # Sort prices in ascending order for binary search
56 prices.sort()
57
58 # Precompute prefix sums for efficient range sum queries
59 prefix_sums = list(accumulate(prices, initial=0))
60
61 result = []
62 n = len(prices)
63
64 # Process each query
65 for reference_price, num_items in queries:
66 # Find optimal split: buy_count items at original price,
67 # sell (num_items - buy_count) items at adjusted price
68 buy_count = find_optimal_split(reference_price, num_items)
69 sell_count = num_items - buy_count
70
71 # Calculate total loss:
72 # - Cost of buying: sum of first buy_count prices
73 # - Revenue from selling: sell_count * (2 * reference_price) - sum of last sell_count prices
74 # Total loss = cost of buying + (selling price - actual revenue)
75 total_loss = (prefix_sums[buy_count] +
76 2 * reference_price * sell_count -
77 (prefix_sums[n] - prefix_sums[n - sell_count]))
78
79 result.append(total_loss)
80
81 return result
82
1class Solution {
2 private int n;
3 private int[] prices;
4
5 /**
6 * Calculate minimum relative losses for given queries
7 * @param prices Array of item prices
8 * @param queries Array of queries where each query contains [k, m]
9 * k: threshold price, m: number of items to select
10 * @return Array of minimum relative losses for each query
11 */
12 public long[] minimumRelativeLosses(int[] prices, int[][] queries) {
13 // Initialize instance variables
14 n = prices.length;
15 Arrays.sort(prices);
16 this.prices = prices;
17
18 // Build prefix sum array for quick range sum calculation
19 long[] prefixSum = new long[n + 1];
20 for (int i = 0; i < n; ++i) {
21 prefixSum[i + 1] = prefixSum[i] + prices[i];
22 }
23
24 // Process each query
25 int numQueries = queries.length;
26 long[] result = new long[numQueries];
27
28 for (int i = 0; i < numQueries; ++i) {
29 int threshold = queries[i][0];
30 int itemsToSelect = queries[i][1];
31
32 // Find optimal split point using binary search
33 int leftCount = findOptimalSplit(threshold, itemsToSelect);
34 int rightCount = itemsToSelect - leftCount;
35
36 // Calculate minimum relative loss:
37 // Sum of leftCount smallest prices +
38 // 2 * threshold * rightCount items - sum of rightCount largest prices
39 result[i] = prefixSum[leftCount] +
40 2L * threshold * rightCount -
41 (prefixSum[n] - prefixSum[n - rightCount]);
42 }
43
44 return result;
45 }
46
47 /**
48 * Find the optimal number of items to take from the left side (smallest prices)
49 * using binary search to minimize the total relative loss
50 * @param threshold The price threshold k
51 * @param totalItems Total number of items to select (m)
52 * @return Optimal number of items to take from left side
53 */
54 private int findOptimalSplit(int threshold, int totalItems) {
55 int left = 0;
56
57 // Find the upper bound: number of items with price < threshold
58 int right = Arrays.binarySearch(prices, threshold);
59 if (right < 0) {
60 right = -(right + 1); // Convert insertion point to index
61 }
62 right = Math.min(totalItems, right); // Cannot take more than totalItems
63
64 // Binary search for the optimal split point
65 while (left < right) {
66 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
67 int rightItems = totalItems - mid;
68
69 // Compare the cost of taking one more from left vs taking from right
70 // If price[mid] < 2*threshold - price[n-rightItems],
71 // it's better to take from left
72 if (prices[mid] < 2L * threshold - prices[n - rightItems]) {
73 left = mid + 1;
74 } else {
75 right = mid;
76 }
77 }
78
79 return left;
80 }
81}
82
1class Solution {
2public:
3 vector<long long> minimumRelativeLosses(vector<int>& prices, vector<vector<int>>& queries) {
4 int n = prices.size();
5
6 // Sort prices in ascending order for binary search
7 sort(prices.begin(), prices.end());
8
9 // Build prefix sum array for quick range sum calculation
10 // prefixSum[i] = sum of first i elements in sorted prices
11 long long prefixSum[n + 1];
12 prefixSum[0] = 0;
13 for (int i = 1; i <= n; ++i) {
14 prefixSum[i] = prefixSum[i - 1] + prices[i - 1];
15 }
16
17 // Lambda function to find optimal number of items to buy at original price
18 // Parameters: k (threshold price), m (number of items to select)
19 // Returns: optimal number of items to buy at original price (rest will be bought at 2k)
20 auto findOptimalSplit = [&](int k, int m) {
21 // Binary search range: [0, min(number of items <= k, m)]
22 int left = 0;
23 int right = upper_bound(prices.begin(), prices.end(), k) - prices.begin();
24 right = min(right, m);
25
26 // Binary search to find the optimal split point
27 while (left < right) {
28 int mid = (left + right) >> 1;
29 int itemsAt2k = m - mid; // Number of items to buy at price 2k
30
31 // Check if buying one more item at original price is better
32 // Compare: prices[mid] vs 2k - prices[n - itemsAt2k]
33 // If prices[mid] < 2k - prices[n - itemsAt2k], we should buy more at original price
34 if (prices[mid] < 2LL * k - prices[n - itemsAt2k]) {
35 left = mid + 1;
36 } else {
37 right = mid;
38 }
39 }
40 return left;
41 };
42
43 vector<long long> answer;
44
45 // Process each query
46 for (auto& query : queries) {
47 int k = query[0]; // Threshold price
48 int m = query[1]; // Number of items to select
49
50 // Find optimal split: buy leftCount items at original price
51 int leftCount = findOptimalSplit(k, m);
52 int rightCount = m - leftCount; // Buy rightCount items at price 2k
53
54 // Calculate total cost:
55 // - Sum of leftCount smallest prices (bought at original price)
56 // - Plus rightCount items bought at 2k each (saving the most expensive items)
57 long long totalCost = prefixSum[leftCount] +
58 2LL * k * rightCount -
59 (prefixSum[n] - prefixSum[n - rightCount]);
60
61 answer.push_back(totalCost);
62 }
63
64 return answer;
65 }
66};
67
1/**
2 * Calculate minimum relative losses for given queries
3 * @param prices - Array of item prices
4 * @param queries - Array of queries where each query contains [k, m]
5 * @returns Array of minimum relative losses for each query
6 */
7function minimumRelativeLosses(prices: number[], queries: number[][]): number[] {
8 const itemCount: number = prices.length;
9
10 // Sort prices in ascending order
11 prices.sort((a: number, b: number) => a - b);
12
13 // Build prefix sum array for quick range sum calculations
14 const prefixSum: number[] = Array(itemCount + 1).fill(0);
15 for (let i = 0; i < itemCount; ++i) {
16 prefixSum[i + 1] = prefixSum[i] + prices[i];
17 }
18
19 /**
20 * Binary search to find the first index where price > threshold
21 * @param threshold - The value to compare against
22 * @returns Index of first element greater than threshold
23 */
24 const findFirstGreaterThan = (threshold: number): number => {
25 let left: number = 0;
26 let right: number = itemCount;
27
28 while (left < right) {
29 const mid: number = (left + right) >> 1;
30 if (prices[mid] > threshold) {
31 right = mid;
32 } else {
33 left = mid + 1;
34 }
35 }
36 return left;
37 };
38
39 /**
40 * Find optimal split point for minimizing relative loss
41 * @param k - Target price point
42 * @param m - Number of items to select
43 * @returns Optimal number of items to take from left side
44 */
45 const findOptimalSplit = (k: number, m: number): number => {
46 let left: number = 0;
47 // Can't take more items from left than exist below threshold k
48 let right: number = Math.min(findFirstGreaterThan(k), m);
49
50 while (left < right) {
51 const mid: number = (left + right) >> 1;
52 const rightCount: number = m - mid;
53
54 // Compare marginal benefit of taking from left vs right
55 // If left item's distance to k is less than right item's distance to k
56 if (prices[mid] < 2 * k - prices[itemCount - rightCount]) {
57 left = mid + 1;
58 } else {
59 right = mid;
60 }
61 }
62 return left;
63 };
64
65 const results: number[] = [];
66
67 // Process each query
68 for (const [k, m] of queries) {
69 // Find optimal split: take leftCount items from left, rightCount from right
70 const leftCount: number = findOptimalSplit(k, m);
71 const rightCount: number = m - leftCount;
72
73 // Calculate total relative loss
74 // Items from left: sum of their values
75 // Items from right: 2*k*rightCount - sum of their values
76 const totalLoss: number = prefixSum[leftCount] +
77 2 * k * rightCount -
78 (prefixSum[itemCount] - prefixSum[itemCount - rightCount]);
79 results.push(totalLoss);
80 }
81
82 return results;
83}
84
Time and Space Complexity
Time Complexity: O((n + m) × log n)
The time complexity breaks down as follows:
- Sorting the
prices
array takesO(n log n)
time, wheren
is the length of the prices array - Building the prefix sum array
s
usingaccumulate
takesO(n)
time - For each query in
queries
(totalm
queries):- The function
f(k, m)
is called, which contains:bisect_right(prices, k)
takesO(log n)
time- A binary search loop that runs in
O(log n)
time (searching within a range of at mostn
elements)
- Computing the loss value takes
O(1)
time
- The function
- Processing all
m
queries takesO(m × log n)
time
Total time complexity: O(n log n) + O(n) + O(m × log n) = O((n + m) × log n)
Space Complexity: O(n)
The space complexity consists of:
- The sorted
prices
array uses the same space as the input (in-place sort or considered as input space) - The prefix sum array
s
requiresO(n + 1) = O(n)
extra space - The answer array
ans
storesm
results, but this is typically considered as output space - Other variables (
l
,r
,loss
, etc.) useO(1)
space
Total auxiliary space complexity: O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Loss Calculation for Chocolates > k
Pitfall: A common mistake is incorrectly calculating Bob's relative loss for chocolates priced above k
. Many developers might think:
- Bob pays
k
, Alice paysprice - k
- So relative loss =
k - (price - k) = 2k - price
However, they might then incorrectly assume Bob should always prefer the most expensive chocolates when price > k
to minimize his loss.
Why it's wrong: While the formula 2k - price
is correct, the interpretation can be misleading. When price
is very large (much greater than 2k
), the loss becomes negative, which seems counterintuitive. The key insight is that negative loss means Bob actually gains relative to Alice.
Solution: Understand that:
- For chocolates where
price > 2k
: Bob's relative loss is negative (he gains) - For chocolates where
k < price ≤ 2k
: Bob still has positive loss - The optimal strategy involves balancing chocolates from both ends of the sorted array
2. Incorrect Binary Search Boundary in find_optimal_split
Pitfall: Setting the wrong upper bound for the binary search:
# Wrong approach
right = min(num_items, len(prices)) # This doesn't account for the k threshold
Why it's wrong: We can only "buy at original price" (take from left) chocolates that cost ≤ k. Taking chocolates with price > k from the left side would violate the problem's payment rules.
Solution: Use bisect_right(prices, reference_price)
to find exactly how many chocolates have price ≤ k:
right = min(num_items, bisect_right(prices, reference_price))
3. Off-by-One Error in Prefix Sum Calculation
Pitfall: Incorrectly calculating the sum of the rightmost sell_count
chocolates:
# Wrong approach sum_of_rightmost = prefix_sums[n] - prefix_sums[n - sell_count - 1] # Or forgetting to handle edge cases
Why it's wrong: Prefix sum arrays require careful indexing. prefix_sums[i]
represents the sum of the first i
elements, so prefix_sums[n] - prefix_sums[n - sell_count]
gives the sum of elements from index n - sell_count
to n - 1
.
Solution: Always verify prefix sum calculations with small examples:
# Correct: sum of last sell_count elements sum_of_rightmost = prefix_sums[n] - prefix_sums[n - sell_count]
4. Not Handling Edge Cases
Pitfall: Failing to consider special cases:
- When
m > n
(requesting more chocolates than available) - When all prices are ≤ k
- When all prices are > k
- When k = 0
Solution: Add validation and handle edge cases:
def minimumRelativeLosses(self, prices: List[int], queries: List[List[int]]) -> List[int]:
n = len(prices)
prices.sort()
prefix_sums = list(accumulate(prices, initial=0))
result = []
for reference_price, num_items in queries:
# Handle case where we want more items than available
num_items = min(num_items, n)
# Handle k = 0 case (Bob pays nothing for any chocolate > 0)
if reference_price == 0:
# Bob's loss is -1 * (sum of m largest prices)
result.append(-1 * (prefix_sums[n] - prefix_sums[n - num_items]))
continue
buy_count = find_optimal_split(reference_price, num_items)
# ... rest of the calculation
5. Binary Search Comparison Logic Error
Pitfall: Using the wrong comparison in the binary search:
# Wrong: comparing indices instead of values if mid < n - items_to_sell: left = mid + 1
Why it's wrong: We need to compare the actual loss values, not the positions. The decision should be based on which chocolate contributes less to Bob's total loss.
Solution: Compare the actual loss contributions:
if prices[mid] < 2 * reference_price - prices[n - items_to_sell]: left = mid + 1 # Taking from left is better else: right = mid # Taking from right is better or equal
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!