Facebook Pixel

1058. Minimize Rounding Error to Meet Target πŸ”’

Problem Description

You are given an array of prices (as strings) and a target sum. Each price needs to be rounded to either its floor or ceiling value, and the sum of all rounded prices must equal the target.

The goal is to minimize the total rounding error, which is calculated as the sum of absolute differences between each rounded value and its original value: Ξ£ |Round(pi) - pi|.

Key constraints:

  • Each price pi can only be rounded to Floor(pi) or Ceil(pi)
  • The sum of all rounded prices must exactly equal the target
  • If it's impossible to achieve the target sum through any combination of floor/ceiling operations, return "-1"
  • If a valid solution exists, return the minimum rounding error as a string with exactly 3 decimal places

Example breakdown:

  • If you have prices [1.5, 2.3, 3.7] and target 7:
    • Possible rounding: [1, 2, 4] β†’ sum = 7 βœ“
    • Rounding error: |1-1.5| + |2-2.3| + |4-3.7| = 0.5 + 0.3 + 0.3 = 1.1
    • Return: "1.100"

The challenge is to determine which prices to round down (floor) and which to round up (ceiling) to achieve the exact target sum while minimizing the total rounding error.

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

Intuition

To find the minimum rounding error, let's first understand the bounds of what's possible. If we round all prices down (floor), we get the minimum possible sum. If we round all prices up (ceiling), we get the maximum possible sum. The target must fall within this range for a solution to exist.

The key insight is that rounding a number with a decimal part causes different errors depending on the direction:

  • Rounding down from x.d to x creates an error of d
  • Rounding up from x.d to x+1 creates an error of 1-d

For integers (no decimal part), there's no choice - we must keep them as is with zero error.

Now, if we start with all prices floored, we need to selectively round some up to reach the target. Each time we change a floor to a ceiling, we:

  • Increase the sum by 1
  • Change the error from d to 1-d

The net change in error is (1-d) - d = 1-2d. To minimize total error, we should prioritize rounding up the numbers with the largest decimal parts (largest d values), as this gives us the most negative change in error (1-2d).

This leads to a greedy strategy:

  1. Start with all numbers floored and calculate how many need to be ceiled to reach the target
  2. Sort the decimal parts in descending order
  3. Round up the numbers with the largest decimal parts first
  4. The final error is the sum of (1-d) for rounded up numbers plus the sum of d for rounded down numbers

Learn more about Greedy, Math and Sorting patterns.

Solution Approach

Let's walk through the implementation step by step:

Step 1: Extract decimal parts and calculate bounds

mi = 0
arr = []
for p in prices:
    p = float(p)
    mi += int(p)  # Sum of all floors
    if d := p - int(p):  # Extract decimal part
        arr.append(d)
  • Convert each price string to float
  • mi accumulates the sum of all floored values (minimum possible sum)
  • arr collects all non-zero decimal parts (these are the prices we can choose to round up)

Step 2: Check feasibility

if not mi <= target <= mi + len(arr):
    return "-1"
  • The minimum sum is mi (all floored)
  • The maximum sum is mi + len(arr) (all non-integers rounded up)
  • If target is outside this range, it's impossible to achieve

Step 3: Determine how many to round up

d = target - mi
  • Since we start with all floored (sum = mi), we need to round up exactly d numbers to reach the target
  • Each rounding up increases the sum by 1

Step 4: Greedy selection

arr.sort(reverse=True)
ans = d - sum(arr[:d]) + sum(arr[d:])
  • Sort decimal parts in descending order
  • Round up the d largest decimal parts (first d elements)
  • Calculate total error:
    • For rounded up numbers (arr[:d]): error = 1 - decimal_part for each
    • Sum of errors = d - sum(arr[:d])
    • For rounded down numbers (arr[d:]): error = decimal_part for each
    • Sum of errors = sum(arr[d:])
    • Total error = d - sum(arr[:d]) + sum(arr[d:])

Step 5: Format result

return f'{ans:.3f}'
  • Return the minimum error formatted to 3 decimal places

The algorithm runs in O(n log n) time due to sorting, with O(n) space for storing decimal parts.

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 the solution with prices = ["2.700", "3.400", "5.900"] and target = 11.

Step 1: Extract decimal parts and calculate bounds

  • Convert to floats: [2.7, 3.4, 5.9]
  • Extract floors and decimal parts:
    • 2.7 β†’ floor = 2, decimal = 0.7
    • 3.4 β†’ floor = 3, decimal = 0.4
    • 5.9 β†’ floor = 5, decimal = 0.9
  • mi (sum of floors) = 2 + 3 + 5 = 10
  • arr (decimal parts) = [0.7, 0.4, 0.9]

Step 2: Check feasibility

  • Minimum possible sum = 10 (all floored)
  • Maximum possible sum = 10 + 3 = 13 (all ceiled)
  • Target = 11 is within [10, 13] βœ“

Step 3: Determine how many to round up

  • We need: target - mi = 11 - 10 = 1
  • So we must round up exactly 1 number

Step 4: Greedy selection

  • Sort decimal parts descending: [0.9, 0.7, 0.4]
  • Round up the 1 largest decimal (0.9):
    • 5.9 β†’ 6 (round up), error = |6 - 5.9| = 0.1
  • Keep others floored:
    • 2.7 β†’ 2 (round down), error = |2 - 2.7| = 0.7
    • 3.4 β†’ 3 (round down), error = |3 - 3.4| = 0.4

Calculate total error:

  • Using the formula: d - sum(arr[:d]) + sum(arr[d:])
  • = 1 - sum([0.9]) + sum([0.7, 0.4])
  • = 1 - 0.9 + 1.1
  • = 1.2

Step 5: Format result

  • Return "1.200"

Verification:

  • Rounded values: [2, 3, 6]
  • Sum: 2 + 3 + 6 = 11 βœ“ (matches target)
  • Total error: 0.7 + 0.4 + 0.1 = 1.2 βœ“

Solution Implementation

1class Solution:
2    def minimizeError(self, prices: List[str], target: int) -> str:
3        # Calculate the minimum possible sum (all prices floored)
4        min_sum = 0
5        # Store decimal parts for prices that can be ceiled
6        decimal_parts = []
7      
8        for price_str in prices:
9            price_float = float(price_str)
10            floor_value = int(price_float)
11            min_sum += floor_value
12          
13            # Calculate decimal part (difference between float and floor)
14            decimal_part = price_float - floor_value
15            if decimal_part > 0:  # Only add non-zero decimal parts
16                decimal_parts.append(decimal_part)
17      
18        # Check if target is achievable
19        # min_sum: all prices floored
20        # min_sum + len(decimal_parts): all prices with decimals are ceiled
21        max_sum = min_sum + len(decimal_parts)
22        if not (min_sum <= target <= max_sum):
23            return "-1"
24      
25        # Calculate how many prices need to be ceiled
26        num_to_ceil = target - min_sum
27      
28        # Sort decimal parts in descending order to minimize error
29        # Ceiling prices with larger decimals minimizes the total error
30        decimal_parts.sort(reverse=True)
31      
32        # Calculate total error:
33        # - For ceiled prices: error = (1 - decimal_part)
34        # - For floored prices: error = decimal_part
35        error_from_ceiled = num_to_ceil - sum(decimal_parts[:num_to_ceil])
36        error_from_floored = sum(decimal_parts[num_to_ceil:])
37        total_error = error_from_ceiled + error_from_floored
38      
39        # Format result to 3 decimal places
40        return f'{total_error:.3f}'
41
1class Solution {
2    public String minimizeError(String[] prices, int target) {
3        // Calculate the minimum possible sum (all prices floored)
4        int minimumSum = 0;
5        // Store fractional parts of all prices that have decimals
6        List<Double> fractionalParts = new ArrayList<>();
7      
8        // Process each price string
9        for (String priceStr : prices) {
10            double price = Double.valueOf(priceStr);
11            int floorPrice = (int) price;
12            minimumSum += floorPrice;
13          
14            // Calculate fractional part (decimal portion)
15            double fractionalPart = price - floorPrice;
16            // Only add non-zero fractional parts to the list
17            if (fractionalPart > 0) {
18                fractionalParts.add(fractionalPart);
19            }
20        }
21      
22        // Check if target is achievable
23        // Minimum sum: all prices floored
24        // Maximum sum: all prices with fractional parts ceiled (minimumSum + fractionalParts.size())
25        if (target < minimumSum || target > minimumSum + fractionalParts.size()) {
26            return "-1";
27        }
28      
29        // Calculate how many prices need to be ceiled
30        int numberToCeil = target - minimumSum;
31      
32        // Sort fractional parts in descending order
33        // We want to ceil the prices with larger fractional parts first to minimize error
34        fractionalParts.sort(Collections.reverseOrder());
35      
36        // Calculate total error
37        double totalError = numberToCeil;  // Initial error from ceiled prices
38      
39        // Subtract fractional parts of prices that will be ceiled
40        // (ceiling adds 1 - fractionalPart to the error)
41        for (int i = 0; i < numberToCeil; ++i) {
42            totalError -= fractionalParts.get(i);
43        }
44      
45        // Add fractional parts of prices that will be floored
46        // (flooring adds fractionalPart to the error)
47        for (int i = numberToCeil; i < fractionalParts.size(); ++i) {
48            totalError += fractionalParts.get(i);
49        }
50      
51        // Format the result to 3 decimal places
52        DecimalFormat decimalFormatter = new DecimalFormat("#0.000");
53        return decimalFormatter.format(totalError);
54    }
55}
56
1class Solution {
2public:
3    string minimizeError(vector<string>& prices, int target) {
4        // Calculate the minimum possible sum (all prices floored)
5        int minSum = 0;
6        vector<double> fractionalParts;
7      
8        // Process each price string
9        for (auto& priceStr : prices) {
10            double price = stod(priceStr);
11            int floorPrice = (int)price;
12            minSum += floorPrice;
13          
14            // Collect fractional parts for prices that can be rounded up
15            double fractionalPart = price - floorPrice;
16            if (fractionalPart > 0) {
17                fractionalParts.push_back(fractionalPart);
18            }
19        }
20      
21        // Check if target is achievable
22        // minSum: all prices floored
23        // maxSum: minSum + number of prices that can be rounded up
24        if (target < minSum || target > minSum + fractionalParts.size()) {
25            return "-1";
26        }
27      
28        // Calculate how many prices need to be rounded up
29        int numToRoundUp = target - minSum;
30      
31        // Sort fractional parts in descending order
32        // We want to round up the largest fractional parts to minimize error
33        sort(fractionalParts.rbegin(), fractionalParts.rend());
34      
35        // Calculate total rounding error
36        double totalError = numToRoundUp;  // Initial error from rounding up numToRoundUp prices
37      
38        // Subtract fractional parts of prices we round up (reduces error)
39        for (int i = 0; i < numToRoundUp; ++i) {
40            totalError -= fractionalParts[i];
41        }
42      
43        // Add fractional parts of prices we round down (increases error)
44        for (int i = numToRoundUp; i < fractionalParts.size(); ++i) {
45            totalError += fractionalParts[i];
46        }
47      
48        // Format result to 3 decimal places
49        string result = to_string(totalError);
50        return result.substr(0, result.find('.') + 4);
51    }
52};
53
1function minimizeError(prices: string[], target: number): string {
2    // Calculate the minimum possible sum (all prices floored)
3    let minSum = 0;
4    const fractionalParts: number[] = [];
5  
6    // Process each price string
7    for (const priceStr of prices) {
8        const price = parseFloat(priceStr);
9        const floorPrice = Math.floor(price);
10        minSum += floorPrice;
11      
12        // Collect fractional parts for prices that can be rounded up
13        const fractionalPart = price - floorPrice;
14        if (fractionalPart > 0) {
15            fractionalParts.push(fractionalPart);
16        }
17    }
18  
19    // Check if target is achievable
20    // minSum: all prices floored
21    // maxSum: minSum + number of prices that can be rounded up
22    if (target < minSum || target > minSum + fractionalParts.length) {
23        return "-1";
24    }
25  
26    // Calculate how many prices need to be rounded up
27    const numToRoundUp = target - minSum;
28  
29    // Sort fractional parts in descending order
30    // We want to round up the largest fractional parts to minimize error
31    fractionalParts.sort((a, b) => b - a);
32  
33    // Calculate total rounding error
34    let totalError = numToRoundUp;  // Initial error from rounding up numToRoundUp prices
35  
36    // Subtract fractional parts of prices we round up (reduces error)
37    for (let i = 0; i < numToRoundUp; i++) {
38        totalError -= fractionalParts[i];
39    }
40  
41    // Add fractional parts of prices we round down (increases error)
42    for (let i = numToRoundUp; i < fractionalParts.length; i++) {
43        totalError += fractionalParts[i];
44    }
45  
46    // Format result to 3 decimal places
47    return totalError.toFixed(3);
48}
49

Time and Space Complexity

Time Complexity: O(n log n) where n is the length of the prices array.

  • Converting each price string to float and calculating the floor value takes O(n) time
  • Building the array of decimal parts takes O(n) time
  • Sorting the decimal parts array takes O(k log k) time where k ≀ n is the number of non-integer prices
  • Computing the sum of subarrays takes O(k) time
  • Since k ≀ n, the dominant operation is sorting, giving us O(n log n) overall

Space Complexity: O(n)

  • The arr list stores at most n decimal parts, requiring O(n) space
  • Other variables (mi, d, ans) use constant space O(1)
  • The sorting operation may use O(log n) space for the recursive call stack (depending on the implementation)
  • Overall space complexity is O(n)

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

Common Pitfalls

1. Floating Point Precision Issues

One of the most common pitfalls in this problem is dealing with floating-point arithmetic inaccuracies. When converting strings to floats and performing calculations, small precision errors can accumulate.

Problem Example:

price = "0.1"
p = float(price)  # 0.1
floor_val = int(p)  # 0
decimal = p - floor_val  # Might be 0.09999999999999998 instead of 0.1

Solution: Use the decimal module for precise arithmetic or round intermediate calculations appropriately:

from decimal import Decimal, ROUND_HALF_UP

price = Decimal("0.1")
floor_val = int(price)
decimal_part = price - floor_val  # Exactly 0.1

2. Integer Prices Handling

The code might incorrectly handle prices that are already integers (like "5.000" or "3").

Problem Example:

price = "5.000"
p = float(price)  # 5.0
decimal = p - int(p)  # 0.0
if decimal:  # This condition fails, but "5.000" could still be in prices array
    arr.append(decimal)

Solution: Be explicit about checking for near-zero decimals:

decimal_part = price_float - floor_value
if decimal_part > 1e-9:  # Use epsilon comparison
    decimal_parts.append(decimal_part)

3. Incorrect Target Feasibility Check

A subtle bug can occur when all prices are integers. The maximum sum calculation assumes we can only increase the sum by the count of non-integer prices.

Problem Example:

prices = ["1", "2", "3"]  # All integers
target = 7
# decimal_parts = [] (empty)
# min_sum = 6, max_sum = 6 + 0 = 6
# Returns "-1" even though target 7 might seem unreachable

This is actually correct behavior, but developers might misunderstand that integer prices cannot be "rounded up" since ceil(integer) = integer.

4. String Formatting Edge Cases

The final formatting might produce unexpected results for very small errors.

Problem Example:

total_error = 0.0005
return f'{total_error:.3f}'  # Returns "0.001" due to rounding

Solution: Consider whether you want banker's rounding or always round down for consistency:

import math
# To always round down to 3 decimals:
truncated = math.floor(total_error * 1000) / 1000
return f'{truncated:.3f}'

5. Empty Decimal Parts Array

When all prices are integers, sorting an empty array and slicing it could cause issues in some implementations.

Problem Example:

decimal_parts = []
decimal_parts.sort(reverse=True)  # OK, but empty
sum(decimal_parts[:0])  # Returns 0, which is correct
sum(decimal_parts[0:])  # Returns 0, which is correct

While Python handles this gracefully, it's worth adding an early check:

if not decimal_parts and target != min_sum:
    return "-1"
if not decimal_parts:
    return "0.000"
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More