算法比赛中的浮点数精度陷阱:从一个货币分解问题说起
算法比赛中的浮点数精度陷阱:从一个货币分解问题说起
大家好,我是算法爱好者小侯同学。今天,我想和大家聊聊一个在ACM/ICPC、LeetCode或Codeforces等比赛中经常“坑”到人的话题——浮点数精度问题。如果你曾经因为一个小数点后的误差而WA(Wrong Answer),或者在处理货币计算时莫名其妙少算了几分钱,那这篇文章绝对是为你写的。
我们以一个经典的巴西货币分解问题为例,来剖析这个“隐形杀手”。问题描述:给定一个浮点数N(代表金额,精确到两位小数),将它分解成钞票(100、50、20、10、5、2)和硬币(1、0.50、0.25、0.10、0.05、0.01)的组合,要求总张数/枚数最少。输出每个面值的数量。
这是一个典型的贪心算法问题:从最大面值开始,尽可能多用大面额,就能保证最优(题目已证明)。听起来简单?但输入是浮点数,输出涉及精确的整数计数,这就埋下了精度炸弹。
浮点数的“原罪”:为什么0.1不是0.1?
在计算机中,浮点数遵循IEEE 754标准,用二进制表示小数。这意味着像0.1、0.01这样的十进制小数,在二进制中往往是无限循环的近似值。例如:
- 0.1 在二进制中是 0.0001100110011…(无限循环),存储时被截断为约 0.1000000000000000055511151231257827021181583404541015625。
- 所以,0.1 * 10 可能不是精确的1.0,而是0.9999999999999999。
在算法比赛中,这种误差会像雪球一样滚大,尤其在累积计算或多次乘除时。常见场景包括:
- 几何问题:计算圆周率π或三角函数sin/cos,判断点是否在多边形内。
- 模拟问题:物理模拟中的速度/加速度积分。
- 金融/货币计算:正如我们的例子,金额N=576.73,*100后本该是57673,但浮点误差可能让它变成57672.999999,导致少1分钱。
忽略精度,代码会“看起来正确”,但测试用例一跑,RE(Runtime Error)或WA就来了。
以货币问题为例:精度炸弹的实战
假设输入N=576.73。我们需要将它转为“分”(cents)单位:57673分。然后用整数除法和取模分解。
直观的代码:
double N;
cin >> N;
int total_cents = (int)(N * 100); // 危险!
问题来了!由于浮点误差,N * 100 可能计算为57672.999999,(int)截断后就是57672。结果?你的硬币部分少算1分,输出如“2 moeda(s) de R$ 0.10”变成“1 moeda(s) de R$ 0.10 + 1 moeda(s) de R$ 0.01”,但总和不对,WA!
解决方案:加0.5的“四舍五入”技巧
聪明的做法是:
int total_cents = static_cast<int>(N * 100 + 0.5);
为什么加0.5?
- 如果N * 100 = 57673.0(理想),+0.5=57673.5,int截断为57673。
- 如果误差导致57672.999999,+0.5=57673.499999,int截断为57673(修正!)。
- 这相当于银行家舍入(round half to even)的简化版,确保两位小数输入的转换精确。
完整代码框架(C++):
#include <iostream>
#include <iomanip>
using namespace std;int main() {double N;cin >> N;int total_cents = static_cast<int>(N * 100 + 0.5); // 关键一行!// 钞票面值(转为分)int bill_values[] = {10000, 5000, 2000, 1000, 500, 200};int bill_counts[6] = {0};for (int i = 0; i < 6; ++i) {bill_counts[i] = total_cents / bill_values[i];total_cents %= bill_values[i];}// 硬币面值(转为分)int coin_values[] = {100, 50, 25, 10, 5, 1};int coin_counts[6] = {0};for (int i = 0; i < 6; ++i) {coin_counts[i] = total_cents / coin_values[i];total_cents %= coin_values[i];}// 输出(样例:576.73)cout << "NOTAS:" << endl;double bills[] = {100.00, 50.00, 20.00, 10.00, 5.00, 2.00};for (int i = 0; i < 6; ++i) {cout << bill_counts[i] << " nota(s) de R$ " << fixed << setprecision(2) << bills[i] << endl;}cout << "MOEDAS:" << endl;double coins[] = {1.00, 0.50, 0.25, 0.10, 0.05, 0.01};for (int i = 0; i < 6; ++i) {cout << coin_counts[i] << " moeda(s) de R$ " << fixed << setprecision(2) << coins[i] << endl;}return 0;
}
输出(正确):
NOTAS:
5 nota(s) de R$ 100.00
1 nota(s) de R$ 50.00
1 nota(s) de R$ 20.00
0 nota(s) de R$ 10.00
1 nota(s) de R$ 5.00
0 nota(s) de R$ 2.00
MOEDAS:
1 moeda(s) de R$ 1.00
1 moeda(s) de R$ 0.50
0 moeda(s) de R$ 0.25
2 moeda(s) de R$ 0.10
0 moeda(s) de R$ 0.05
3 moeda(s) de R$ 0.01
这个技巧在比赛中超级实用!类似地,对于更多小数位,可以用round(N * pow(10, digits))
(C++11的round函数)。
算法比赛中的其他精度战场
除了货币,浮点精度还藏在这些地方:
- 浮点比较:别用
==
!用epsilon:if (abs(a - b) < 1e-9)
。在几何题中,判断两点重合或线段相交时必备。 - 累积误差:多次加减小数?转为分数(用pair<long long, long long>模拟)或大整数。
- 输出精度:用
fixed << setprecision(k)
控制小数位,避免多余的0或科学计数法。 - 特殊场景:π相关计算,用
acos(-1.0)
定义π;随机数生成,避免rand()
的浮点drift。
在Python中,类似问题用round(N * 100)
或Decimal模块解决;在Java,用BigDecimal。
结语:精度是比赛的“隐形Boss”
浮点数精度问题就像算法比赛中的“隐形Boss”——不显眼,但一击致命。记住:优先用整数模拟小数,这是老司机的铁律。从这个货币问题学到的“+0.5”技巧,能救你于水火。建议大家多刷UVA或AtCoder的浮点题,亲身“中招”几次,就能练就火眼金睛。
下次比赛,遇到浮点?先问自己:“能转整数吗?”如果能,果断转!欢迎在评论区分享你的“精度血泪史”,我们一起吐槽,一起成长。