E - 2^a b^2 解説 by rsk0315

sqrt の誤差について

\(\sqrt{n}\) の計算に、浮動小数点型の sqrt を用いた際の誤差について記します。

\(\gdef\roundcirc#1{\operatorname{\circ}{(#1)}}\) \(\gdef\floor#1{\lfloor#1\rfloor}\) \(\gdef\ceil#1{\lceil#1\rceil}\)

IEEE 754 で定義される binary64 にあたる浮動小数点型で、丸めモード tiesToEven(表せる数のうち最も近いものに丸める。タイのときは仮数部が偶数になる方を選ぶ)で実数 \(x\) を丸めたときの値を \(\roundcirc{x}\) と書くことにします。

note: double (C++), float (Python), f64 (Rust) のデフォルトの挙動に相当します。

なお、C++ において sqrt(n) と書いたとき、nlong long だったとしても暗黙に ndouble に変換されます。

下記の 2 数について考えます。

\[ \begin{aligned} n_1 &= (2^{26}+1)^2-1 \\ &= 67108865^2-1 \\ &= 4503599761588224, \\ n_2 &= \ceil{2^{26.5}}^2-1 \\ &= 94906266^2-1 \\ &= 9007199326062755. \end{aligned} \]

\(2^{52} \lt n_1 \lt 2^{53} \lt n_2\le 10^{18}\) が成り立ちます。

該当の型は仮数部のビット数が 53 なので、\(\roundcirc{n_1} = n_1\) が成り立ちます。また、\(\roundcirc{n_2} = n_2+1\) が成り立ちます。

\[ \sqrt{n_1} = 67108864.999999992549419{\dots} \]

であり、\(\roundcirc{\sqrt{n_1}} = \floor{\sqrt{n_1}}+1\) となります。 また、

\[ \sqrt{n_2+1} = 94906266 \]

が成り立ちます。こうして、\(\roundcirc{\sqrt{\roundcirc{n_1}}} = \floor{\sqrt{n_1}}+1\) および \(\roundcirc{\sqrt{\roundcirc{n_2}}} = \floor{\sqrt{n_2}}+1\) がわかりました。

\(n'=\roundcirc{n}\) の計算と、\(\roundcirc{\sqrt{n'}}\) の計算の 2 箇所で誤差が生じうるため、両方を気にする必要があります。\(n_1\) は後者、\(n_2\) は前者のみの誤差が生じる例です。

対策

誤差のことを気にしなくてよくなる簡単な方法としては、整数型で二分探索する方法が挙げられます。

また、sqrt(n) の返り値 m を前後 10 個くらいの i を調べ、i * i <= n && (i + 1) * (i + 1) > n となるものを探すという方法を使っている人もしばしばいます。実は、mm - 1 についてのみ調べれば十分であることは示せます(解説ブログ)。

uint64_t uint64_floor_sqrt(uint64_t n) {
    if (n == 0) return 0; 
    uint64_t tmp_m1 = std::sqrt(n) - 1;
    return tmp_m1 * (tmp_m1 + 2) < n ? tmp_m1 + 1 : tmp_m1;
}
fn u64_floor_sqrt(n: u64) -> u64 {
    let tmp = (n as f64).sqrt() as u64;
    let tmp_m1 = tmp.saturating_sub(1);
    if tmp_m1 * (tmp_m1 + 2) < n { tmp } else { tmp_m1 }
}

また、Python では math.isqrt が用意されているので、それを使うのがよいでしょう。

投稿日時:
最終更新: