語法書 / AA 競程語法書 下冊 / 第九單元 / 能用整數就用整數

9.10 能用整數就用整數

本單元的收尾,是一條可能有點意外的原則:

以下是三個經典場景。

場景一:大整數,浮點數碰都別碰

要判斷輸入的 x 是否 \le 10^{18} + 3,有人圖方便用 double 讀入——立刻中招:10^{18} 已遠超 2^{53}9.6 說過此時相鄰整數在 double 裡黏在一起10^{18} + 310^{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——189 存進 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)AB 很大時可能因浮點誤差差 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——想想怎麼完全不用浮點數解掉它(提示:B100 是整數;把 B 當字串讀,8.7 學過)。