LeetCode 1058. 最小化舍入误差以满足目标
题目描述
题意分析
每个非负价格都以固定三位小数字符串给出,可以独立向下取整或向上取整。要求所有取整结果之和恰好等于整数
target,并使每项取整值与原价格之差的绝对值总和最小。能满足目标时,返回保留三位小数的最小误差;无法满足时返回
"-1"。整数价格的上下取整结果相同,不能通过把它选择为“向上”来多增加一。
解法:贪心选择上取整
核心思路
[!blue]
先把全部价格都向下取整,得到最低总和
sumFloor。每个非整数价格从向下改成向上,总和恰好增加1;因此为了达到目标,必须选中恰好up = target - sumFloor个非整数价格改为向上。若
up < 0,最低总和已经超过目标;若up超过非整数价格数量,即使全部向上也不够。否则任选这么多个候选都能满足总和,所以先用这个范围检查确定可行性,再优化哪些候选最划算。为避免浮点误差,把小数部分按千分之一为单位表示成整数
fraction。全部向下时,这一项的误差就是fraction;改成向上后,误差变成1000 - fraction,变化量为1000 - 2 * fraction。小数部分越大,改向上的误差增量越小。因此选择最大的
up个小数部分即可。若某个方案选了较小小数部分,却没选更大的,把两者的取整方向交换,向上数量不变,总和仍等于目标,而误差不会增加。这个交换论证保证了差值排序的最优性。代码先累计全部向下的误差,再逐个加入被选候选的误差变化,始终使用整数精确计算。最后把千分位总误差拆成整数部分与三位小数,补齐前导零后输出。
解题步骤
- 解析每个价格的整数部分和三位小数整数,累加
sumFloor与全部向下的误差。- 只把非零小数部分加入候选列表,整数价格不占改向上的名额。
- 计算
up,若不在0..候选数量之间,返回"-1"。- 将候选按小数部分升序排序,选择末尾最大的
up项,分别把1000 - 2 * fraction加到误差中。- 用除以
1000的商与余数格式化输出,余数始终补足三位。
代码实现
class Solution {
public String minimizeError(String[] prices, int target) {
int[] fractions = new int[prices.length];
int count = 0;
int sumFloor = 0;
int errorMillis = 0;
for (String price : prices) {
int dot = price.indexOf('.');
int whole = Integer.parseInt(price.substring(0, dot));
// 将三位小数直接解析为千分位整数,避免浮点计算。
int fraction = Integer.parseInt(price.substring(dot + 1));
sumFloor += whole;
errorMillis += fraction;
// 整数价格上取整也不会增加总和,不能占用候选名额。
if (fraction != 0) {
fractions[count++] = fraction;
}
}
// 每个非整数价格改为上取整,恰好使总和增加 1。
int up = target - sumFloor;
// 向上名额必须非负,且不能超过非整数候选数。
if (up < 0 || up > count) {
return "-1";
}
java.util.Arrays.sort(fractions, 0, count);
// 选最大的若干小数部分,使误差增量最小。
for (int i = count - up; i < count; i++) {
errorMillis += 1000 - 2 * fractions[i];
}
return String.format(
java.util.Locale.ROOT, "%d.%03d", errorMillis / 1000, errorMillis % 1000);
}
}
import (
"fmt"
"sort"
)
func minimizeError(prices []string, target int) string {
fractions := make([]int, 0, len(prices))
sumFloor, errorMillis := 0, 0
for _, price := range prices {
whole, fraction := 0, 0
fmt.Sscanf(price, "%d.%d", &whole, &fraction)
sumFloor += whole
errorMillis += fraction
// 整数价格上取整也不会增加总和,不能占用候选名额。
if fraction != 0 {
fractions = append(fractions, fraction)
}
}
// 每个非整数价格改为上取整,恰好使总和增加 1。
up := target - sumFloor
// 向上名额必须非负,且不能超过非整数候选数。
if up < 0 || up > len(fractions) {
return "-1"
}
sort.Ints(fractions)
// 选最大的若干小数部分,使误差增量最小。
for i := len(fractions) - up; i < len(fractions); i++ {
errorMillis += 1000 - 2*fractions[i]
}
return fmt.Sprintf("%d.%03d", errorMillis/1000, errorMillis%1000)
}
复杂度分析
- 时间复杂度:设非整数价格数为 u,时间为 $O(n+u\log(u+1))$,只排序有效候选。
- 空间复杂度:$O(n)$,保存候选小数部分。
关键点总结
[!green]
- 目标总和先确定向上次数,剩下的才是在固定名额内最小化误差。
- 选择依据是改变取整方向的误差差值,它随小数部分增大而减小。
- 固定三位小数可以全程转换成千分位整数,精确处理计算和格式。
- 即使某次向上会增加误差,也可能是达到目标必须占用的名额,不能擅自少选。
易错点总结
[!yellow]
- 把整数价格也列入可向上加一的候选,会误判目标可达范围。
- 对每项独立四舍五入,不一定让总和恰好等于目标。
- 选择小数部分较小的价格向上,会增加更大的误差,贪心方向相反。
- 只检查名额不足或过多的一侧,无法完整排除两类无解情况。
- Java 排序包含数组未填写的尾部,会把默认零当作候选;代码只排序实际有效前缀。
- 输出未补足三位小数,或解析时把三位小数当成浮点再反复运算,都会破坏题目要求的精确结果格式。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 1029. 两地调度 | 中等 | 同样先取全部选择一侧的基准,再按切换到另一侧的误差或费用差挑固定数量。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!