9.10 能用整數就用整數
本單元的收尾,是一條可能有點意外的原則:
以下是三個經典場景。
¶場景一:大整數,浮點數碰都別碰
要判斷輸入的 x 是否 \le 10^{18} + 3,有人圖方便用 double 讀入——立刻中招:10^{18} 已遠超 2^{53},9.6 說過此時相鄰整數在 double 裡黏在一起,10^{18} + 3 和 10^{18} + 4 根本分不出來。x 可能到 2 \times 10^{18}?long long 讀、long long 比,一步到位。
同型陷阱——想算 \lfloor 999999999999999999 / 10 \rfloor,繞去浮點數再 floor:
#include <iostream>
#include <cmath>
using namespace std;
int main() {
long long x = 999999999999999999; // 18 個 9
double a = x; // 存進 double 的瞬間就不準了
cout << (long long)floor(a / 10) << '\n';
cout << x / 10 << '\n'; // 正確做法:整數除法本來就會向下取整
return 0;
}
執行結果:
100000000000000000
99999999999999999
浮點數版差了 1——18 個 9 存進 double 被吸到 10^{18} 去了。而正整數的整數除法本來就是向下取整(2.6),什麼函式都不用。
¶場景二:上取整,也有純整數寫法
「A 除以 B 無條件進位」(例如血量 A、每刀扣 B,幾刀砍完?)不用 ceil:
long long times = (A + B - 1) / B; // A、B 是正整數:⌈A / B⌉ 的整數寫法
原理:A 恰是 B 的倍數時,+ (B-1) 不足以多進一位;差一點點時,+ (B-1) 恰好把它推過門檻。一行整數運算,沒有任何誤差風險——用 ceil(1.0 * A / B) 在 A、B 很大時可能因浮點誤差差 1,而這種 bug 測資不一定抓得到。
¶場景三:開根號後,用整數「校正」
要算 \lfloor \sqrt{x} \rfloor、而 x 大到 10^{18} 時,直接 (long long)sqrt(x) 在大數時可能差 \pm 1(浮點誤差在截斷時放大)。競程的標準手法:浮點數給個近似值,整數運算把它修正到位:
#include <iostream>
#include <cmath>
using namespace std;
int main() {
long long x;
cin >> x;
long long r = (long long)sqrtl(x); // sqrtl:long double 版,近似更準
while (r > 0 && r * r > x) r--; // 猜大了往回修
while ((r + 1) * (r + 1) <= x) r++; // 猜小了往前修
cout << r << '\n';
return 0;
}
執行結果(輸入 999999999999999999):
999999999
兩個 while 頂多修一兩步,成本趨近於零,換來絕對正確——「浮點求近似、整數做裁決」這個套路之後會一再出現。
動手試試看:相乘再向下取整給你整數 A 和「小數點後恰兩位」的小數 B,求 \lfloor A \times B \rfloor——想想怎麼完全不用浮點數解掉它(提示:B 乘 100 是整數;把 B 當字串讀,8.7 學過)。