誤差は怖い!!!!!!!
問題概要
整数 と、"x.yz" のフォーマットで与えられる実数 が与えられる。
の小数点以下を切り捨て、結果を整数として出力せよ。
制約
考えたこと
数値誤差が怖いというテーマの問題は、パナソニックコンでもあった。かの有名な「 ですか?」という問題!!!
今回も数値誤差が怖くて、たとえば次のようなコードだと、WA になってしまう。
int main() { long long A; double B; cin >> A >> B; cout << (long long)(A * B) << endl; }
このようになってしまう原因は、「double 型には 15〜16 桁程度の精度しかない」というところにある。今回、
- の有効数字は 15 桁
- の有効数字は 3 桁
なので、 の計算には、18 桁程度の有効数字が欲しいのだ。double 型では足りない。
考えられる対策としては、
- double 型より精度の高い型を用いる
- 整数計算で済ませる
が挙げられる。
解法 (1): long double 型を使う
long double 型ならば、処理系依存ではあるが、double 型の倍の精度を出すことができるので、それで一応通る。
なお、得られた long double 型の値を単純に long long 型にキャストするのでは、88888.00 といった値が内部的には 88887.99999999 のように扱われて 88887 になったりするのが怖い。なので、0.001 といった微小な値を足しておく必要がある (今回は、微小値を足す処理をしなくても通ったが、それはテストケースが弱かったためっぽい)。
#include <bits/stdc++.h> using namespace std; int main() { long double A, B; cin >> A >> B; cout << (long long)(A * B + 0.001) << endl; }
補足:数の表現と誤差
驚くべきことに、計算機では 0.1 のような単純な値でさえ正確には表すことができてない。なぜなら、コンピュータ内部では、数値は基本的に二進数で扱う。0.1 を二進法で表すと、
0.000110011001100110011...
という具合に無限小数になるのだ。どこかで区切らないといけないので、誤差が生じてしまう。このような誤差はとても怖い。実は今回の問題でも、たとえ long double 型を使ったとしても、
cout << (long long)(A * B) << endl;
ではダメなケースが存在するようだ。具体的には、
- A = 100
- B = 0.53
でダメになる。このケースはテストケースに含まれていなかった模様。なのでやはり
- EPS = 0.001
といった微小値を用意してあげて、
cout << (long long)(A * B + EPS) << endl;
という風にしてあげる必要がある。
解法 (2): 基本的に整数演算で済ませる
基本的には整数演算のみで済ませることができるならば、その方が安心かなと思う。
今回、 は小数点以下 2 桁までしかないことがわかっている。つまり、 を 倍すると整数になるのだ。そのことを利用して、
/
を計算すれば、整数演算のみですべてを済ませることができる! の値の計算は、 を文字列として受け取るのが明快だと思う。
大きな罠 (100B の計算)
なお、 を計算する部分でも大きな罠があって、
- を double 型として受け取る
- それを 100 倍して long long 型に cast する
という素朴な方法では、実は WA になってしまう!!!!!!!!!
フェネック「誤差が出たら嫌な問題はできるだけ整数の範囲で計算するのがよくて、今回はA×(B×100)/100みたいに計算すればいいんだけど、B×100を整数に変換するときの誤差にも気をつけないとダメだね。例えばB=2.51のとき、B×100=250.999999999999971…になるよー」
— 競技プログラミングをするフレンズ (@kyopro_friends) 2020年5月31日
文字列として扱うのが確実だと思われるが、どうしても必要であれば、
long long B100 = (long long)(B * 100 + 0.001);
という具合に、0.001 といった「微小値 EPS」を足してから long long 型にキャストするようにすれば通すことができる。
解答例 2-1: B を文字列として扱う方法
#include <bits/stdc++.h> using namespace std; int main() { long long A; string B; cin >> A >> B; long long B100 = (B[0]-'0')*100 + (B[2]-'0')*10 + (B[3]-'0'); cout << A * B100 / 100 << endl; }
解答例 2-2: 100B に微小値を足す方法
#include <bits/stdc++.h> using namespace std; int main() { long long A; double B; cin >> A >> B; long long B100 = (long long)(B * 100 + 0.001); cout << A * B100 / 100 << endl; }