精度误差
引子
这是一段非常普通的代码。
#include<bits/stdc++.h>
using namespace std;
int main(){
double c=3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068;
printf("%.15lf\n",c);
}
它输出 printf("%.25lf\n",c);)时,有一件神秘的事情发生了:
它的输出是
但是我们想要的是
而如果
如果我们将
#include<bits/stdc++.h>
using namespace std;
int main(){
double c=11451419198102085;
printf("%.25lf\n",c);
}
输出的结果是:
如果把
种种迹象表明,double 类型最多只能保留
那么这个的原理是什么呢?double 是否还能稳定使用呢?
了解这个的前提是我们了解计算机存储浮点数的方法。
计算机存储浮点数的机理
在计算机中,double 类型基于 IEEE 754 标准,使用
单精度浮点数 float 则更加寒酸,只有
浮点数在计算机中的存储也是基于二进制进行的。具体来说,是如下图:
我们以单精度浮点数的存储为例。
首先,假如我们有这样一个单精度浮点数
根据二进制存储小数,
二进制存储小数的原理:
| 二进制位 | 该位数字 |
|---|---|
紧接着,我们对这个二进制小数
但是还有一种可能:这个数太大了,需要乘
所以这
然后剩下的小数部分
当然了,这只是理想状态。实际情况远比这个要残酷的多。例如小数
从二进制小数转十进制浮点数时,首先把尾数位提取出来,然后加入整数位
但是你是否注意到上面
累积误差
显然我们使用 double 并不只是为了存储浮点数,而是为了计算与比较。
假设我们现在有两个浮点数
正确答案是
但是假设由于计算机误差,导致这两个浮点数存储为了
前面勉强还能接受的误差直接溢到了整数位,显然非常烦人。
这就是累积误差。在存在小误差时,如果继续计算,小误差相乘相加可能就会对答案造成比较大的影响。
这就是你使用浮点数类型时,造成误差的最直接原因。
各种浮点数类型的极限是多少
相信比起了解计算机存储浮点数的原理,你更想要知道 float 和 double 等基本类型精度的极限是多少。
float
float 既然作为单精度浮点数,它的精度极限自然不大。
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-25;
int main(){
float a=0.114514;
printf("%.25lf\n",a);
}
你可以把这段代码的
| 输出 | |
|---|---|
在数据范围大的情况下,float 产生的误差甚至能溢到整数位,这是不可接受的。所以在算法竞赛中,不到卡空间的时候千万不要用 float!
double
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-25;
int main(){
double a=0.114514;
printf("%.25lf\n",a);
}
| 输出 | |
|---|---|
在
long double
首先要知道的是,long double 在不同环境下的二进制位数是不同的。有的地方是
U498224 float、double 与 long double 是在洛谷评测平台做的一个 long double 测试。
- long double 记录
- double 记录
评测数据在题目附件中。
可以看出,在相同数据下,long double 比 double 可以多存储
避免精度误差的方式
浮点数比较:eps
很多选手在不得不使用浮点数比较时,都会把简单的 a>b 改为 a-b>eps。这个
例如:
#include<bits/stdc++.h>
using namespace std;
int main(){
double a=1.0*522440.335527/36672.100382,b=1.0*2475081569.2203151584/173735513.1060599744;
printf("%.25lf\n%.25lf\n",a,b);
return 0;
}
//*4737.5392
输出的答案是:
但是后者其实是前者的分母分子同乘
诶!那么如果我们设一个
这就是
分数类
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=100005;
struct frac{
ll a,b;
friend bool operator <(frac f,frac g){
if(f.a*g.b>=f.b*g.a)return 0;
else return 1;
}
friend bool operator >(frac f,frac g){
if(f.a*g.b<=f.b*g.a)return 0;
else return 1;
}
friend bool operator ==(frac f,frac g){
if(f.a==g.a&&f.b==g.b)return 1;
else return 0;
}
}a[N];
frac yf(frac a){
ll t=__gcd(a.a,a.b);
frac nf;
nf.a=a.a/t;
nf.b=a.b/t;
return nf;
}
int n;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&a[i].a,&a[i].b);
a[i]=yf(a[i]);
}
int q;scanf("%d",&q);
for(int x,y;q;q--){
scanf("%d%d",&x,&y);
if(a[x]==a[y])printf("equal\n");
else if(a[x]<a[y])printf("small\n");
else printf("big\n");
}
return 0;
}
这是一个可以比较两个分数大小的代码。
基理是通分,然后比较分子。
优点是不会出现精度误差。缺点也很明显:long long 也有限制。它的分子分母最大只能是
计算可能误差
以 AT_abc418_e 为例。
假若我们采用计算斜率,排序的方法。那么会有多大的误差呢?
注意到