【注意】Math.pow関数は誤った結果を出すことがあります

プログラミング
この記事は約4分で読めます。

こんにちは!ペンすけです!
競プロに参加していたら、「Javaさんのバグか?」と思ってしまうような症状に苦しんだので記事に起こします。
(正確にはバグではなく、後述の浮動小数点計算の誤差によるものです)

ペンすけ
ペンすけ

社内システムやアプリでMath.powを使う場合、
システムエラーにならないよう、取り扱う値の範囲には十分注意してください!

\(10^{18}\)の計算を行った際にMath.powが壊れた

苦しむことになったのはこちらの問題です。

ペンすけ
ペンすけ

整数\(B\)が与えられて、それが\(N^N\)で表せるか判定する問題です!

解法としては、\(N = 1^1,~ 2^2,~ 3^3,~ \ldots \)と計算していき、\(B\)と一致するかif文で確かめる方法になります。

具体例

\(B = 256\) のとき、

\(1^1 = 1\)
\(2^2 = 4\)
\(3^3 = 27\)
\(4^4 = 256\) 一致!

と、\( 4^4 \) で表せる。

ペンすけ
ペンすけ

int型じゃなくてlong型にだけするのを忘れないようにすれば大丈夫でしょ!

と、意気揚々に実装しました。

実装したもの
// 整数Bの入力
long B = Long.parseLong(sc.next());

long num = 1;
long res = -1;

// ここで検証!!!
while(Math.pow(num, num) <= B){
  if(Math.pow(num, num) == B){
    res = num;
    break;
  }
  num++;
}

// 結果の出力
if(res < 0){
  // N^Nで表せない場合は-1
  System.out.println(-1);
}
else{
  System.out.println(res);
}

結果はなんと「不正解」
(# ゚Д゚)ナンデヤネン

元凶は浮動小数点の計算誤差

原因を探るべく\(N^N\)の結果を出力してみました。
すると、

1^1 =   1
2^2 =   4
3^3 =   27
4^4 =   256
5^5 =   3125
6^6 =   46656
7^7 =   823543
8^8 =   16777216
9^9 =   387420489
10^10 = 10000000000
11^11 = 285311670611
12^12 = 8916100448256
13^13 = 302875106592253
14^14 = 11112006825558016
15^15 = 437893890380859392
ペンすけ
ペンすけ

\(15^{15}\)の1の位って必ず5になるはずじゃ…!

どうやらMath.pow関数を使うと、\(10^{15}\)くらいから結果が怪しくなるようなので、
自作でpow関数を作って使うのが良いです。

private static long myPow(long base, long exp){
    long res = 1;

    for(long i=0; i<exp; ++i){
        res *= base;
    }

    return res;
}
15^15 = 437893890380859375

無事「正解」することができました。

浮動小数点の誤差は2進数↔10進数の変換で起こる誤差

いわゆる「丸め誤差」です。

情報処理技術者試験の経験がある方は「序盤に勉強したあれか!」となるかもしれません。
手元で検証してみます。

System.out.println(0.1);
System.out.println(new BigDecimal(0.1));

このようなコードを書きました。BigDecimalというのは、限りなく正確な値を入れておくための型です。
どういうことかは次の結果を見るとわかります。

0.1
0.10000000000000000555...
ペンすけ
ペンすけ

後ろの555…以降ってなに!?!

コンピューターは2進数で計算処理しています。
そのため、10進数と2進数の変換の際に、誤差が含まれてしまうことがあります。

通常は気にする程でもないですが、\(10^{15}\)のように桁数が大きい処理をしようとすると、その誤差が顕著に表れてしまいます。

まとめ

double型として処理するMath.pow等は、浮動小数点の誤差を引き起こす場合があります。

誤差を生じさせたくない場合は、int型やlong型等の整数型のみで実装をするか、BigDecimal型を使うと良いです。

ペンすけ
ペンすけ

誤差が含まれていることを知った上で、適切に使っていきましょう!

コメント

タイトルとURLをコピーしました