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 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

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 ✓

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"
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More