Facebook Pixel

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 pays k_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 pays
  • a_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 and k = 7:

    • Bob pays 7, Alice pays 3
    • Bob's relative loss = 7 - 3 = 4
  • If a chocolate costs 5 and k = 7:

    • Bob pays 5, Alice pays 0
    • Bob's relative loss = 5 - 0 = 5

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about Bob's relative loss for each chocolate:

  1. When price ≤ k: Bob pays the full price, Alice pays 0. Bob's relative loss = price - 0 = price

  2. When price > k: Bob pays k, Alice pays price - 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 where s[i] represents the sum of the first i 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 index r, the first position where price > k
  • This tells us how many chocolates have price ≤ k (there are r 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
  • 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)
    • 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 first l prices)
  • Loss from right chocolates: 2k * (m - l) - (s[n] - s[n - (m - l)])
    • Each of the m - l chocolates contributes 2k - price to the loss
    • s[n] - s[n - (m - l)] gives the sum of the rightmost m - l prices
  • 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 Evaluator

Example 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 vs 2×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 takes O(n log n) time, where n is the length of the prices array
  • Building the prefix sum array s using accumulate takes O(n) time
  • For each query in queries (total m queries):
    • The function f(k, m) is called, which contains:
      • bisect_right(prices, k) takes O(log n) time
      • A binary search loop that runs in O(log n) time (searching within a range of at most n elements)
    • Computing the loss value takes O(1) time
  • Processing all m queries takes O(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 requires O(n + 1) = O(n) extra space
  • The answer array ans stores m results, but this is typically considered as output space
  • Other variables (l, r, loss, etc.) use O(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 pays price - 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More