高精度误差

关于高精度误差

高精度问题

高精度问题总是带给竞赛选手噩梦般的体验,每个oier选手都或多或少遇到过。有必要对高精度做一个说明,避免掉入高精度的大坑。

对于oier选手而言,float基本可以弃用了。表示实数,最少也要用double的。

数据类型字节数有效位数/数据范围
double816~17位
long double1619位
int410位 (-2147483648 ~2147483647)
long long int818位 (-9223372036854775808 ~ 9223372036854775807 )

一. 类型转换问题

1. 整数转成double类型(看有效位数)

对于double类型和long double类型,有效位数决定了该类型能够表示数值的精度。

如果一个 long long int 转成 double,因为double的有效位数只有16~17位,所以如果long long int 的数位超过17位,则转化为double后会出错。

long long int a = 123456789123456789;
double b = a;
printf("%.1f", b);
// 输出为:123456789123456784.0, 最后一位已经无法表示了。

而int最大只有10位,所以int类型转成double类型不会丢失有效位,没有任何问题。

总结:int 转成 double 是毫无问题, long long int 只有当数据特别大(超过16位)时,转成double会产生问题,丢失有效位; 不管是int,还是long long int, 转成long double 是没有任何问题的。因为long double的有效位数超过了int和long long int。
2. double类型转成整数
(1)double直接转成整数,是会去掉小数部分,保留整数部分。

而如果double的整数部分超过int范围,转成int,会发生溢出。

 	double a = 12345678901234.567;
    int d = (int) a;
    printf("%d", d);//此处发生溢出,输出的是-2147483648. double转int,只要发生溢出,都会得到这个值-2147483648。
(2)放大转换:

有时double的小数位数不太多,我们可能会将double放大若干倍,将它变成整数再处理,以避免精度问题。这种放大转换也有问题。

比如:

	double a = 19.40;
    int c = (int) (a * 10);
	printf("%d\n", c); //此处输出194
    int d =(int) (a * 100);
    printf("%d\n", d); //此处输出1939
    //原因是19.40无法用二进制精确表示,采用的是近似值:19.3999999999。让人崩溃的是第一个输出的为何不是193。

放大转换时,如果放大的倍数已经保证没有小数位数了,可以采用round,这样得到的结果就没有问题。

    double a = 19.40;
    int c = round (a * 10);
    int d =round (a * 100);
    printf("%d\n%d\n", c, d); // 输出194和1940

总结:double的小数部分,并不能精确的表达所有数值,许多时候,都是用一些近似值表示的,显示出来的结果是四舍五入的结果。

比如上面的19.40,实际上使用19.39999999近似表示的。所以,double总是会有一些莫名其妙的精度问题。

3. floor、ceil

floor(a): 取小于等于a的最大整数值。

ceil(a):取大于等于a的最小整数值。

round(a): 对小数部分四舍五入。

floor(-3.9) : 值为-4
floor(3.9): 值为3
ceil(-3.9): 值为-3
ceil(3.9): 值为4
round(3.9): 值为4
round(-3.9):值为-4

因为double许多时候是以近似值来表示的,比如 ( 0. 5 2 − 0. 3 2 − 0. 4 2 ) (0.5^2 - 0.3^2 - 0.4^2) (0.520.320.42), 本来应该是0,但是因为 0.3 , 0.4 0.3,0.4 0.3,0.4都是近似值的关系,最后的结果实际上是接近0的一个近似值.看下面的例子:

 	double a = 0.3, b = 0.4, c = 0.5;
    printf("%.0f %.0f", floor(a - sqrt(c * c - b * b)), ceil(a - sqrt(c * c - b * b)));
    //输出为:0 1

采用long double也会有相似的问题。

    long double a = 0.0003, b = 0.0004, c = 0.0005;
    printf("%.0Lf %.0Lf", floor(a - sqrt(c * c - b * b)), ceil(a - sqrt(c * c - b * b)));
    //这下可好,输出的是 -1 0

为了解决floor,ceil的问题,我们需要用一个很小的量去修正它。

比如floor(x),因为 x x x可能是一个整点附近的值。我们应当给 x x x加上一个很小的量 e p s eps eps,比如 e p s eps eps取值为 1 0 − 13 10^{-13} 1013

而求ceil(x), 我们需要让 x x x减去一个很小的量 e p s eps eps.

例1:表格点
题意描述

有一个半径为 R R R的圆,圆心为 ( X , Y ) (X,Y) (X,Y).
求圆内及圆周上的所有表格点的数量,所谓表格点,即整数坐标点。

输入格式

第一行三个数,表示 X , Y , R X,Y,R X,Y,R, 每个数最多包含4位小数。

输出格式

一个整数,表示答案。

数据范围

∣ X ∣ ≤ 1 0 5 , ∣ Y ∣ ≤ 1 0 5 , 0 < R ≤ 1 0 5 |X| \leq 10^5, |Y| \leq 10^5, 0 \lt R \leq 10^5 X105,Y105,0<R105

分析:本题可以枚举所有的与圆相交或相切的 y y y值为整数的水平线,求出它与圆的两个交点 ( x 1 , y ) , ( x 2 , y ) (x_1,y),(x_2,y) (x1,y),(x2,y),设 x 1 ≤ x 2 x_1 \leq x_2 x1x2,则该水平线上一共有 f l o o r ( x 2 ) − c e i l ( x 1 ) + 1 floor(x_2)-ceil(x_1)+1 floor(x2)ceil(x1)+1 个表格点。

x 1 , x 2 x_1,x_2 x1,x2怎么求呢?

可以通过勾股定理来求,如下图所示:

x 1 = X − ( R 2 − ( Y − y ) ∗ ( Y − y ) ) x_1=X-\sqrt{(R^2-(Y-y)*(Y-y))} x1=X(R2(Yy)(Yy))

x 2 = X + ( R 2 − ( Y − y ) ∗ ( Y − y ) ) x_2=X+\sqrt{(R^2-(Y-y)*(Y-y))} x2=X+(R2(Yy)(Yy))

但本题有两个细节要格外注意,否则可能因精度问题出错。

1. 需要用long double存储,因为double精度已经不够了。

比如 R = 1 0 5 , Y − y = 1 0 − 5 R=10^5,Y-y=10^{-5} R=105,Yy=105时, ( R 2 − ( Y − y ) ∗ ( Y − y ) ) \sqrt{(R^2-(Y-y)*(Y-y))} (R2(Yy)(Yy)) 的数值已经超过了double的有效位数,导致double不能准确的表示平方根,会带来误差。所以应该用long double,或者将所有坐标换成整数,用long long int。

2. 使用floor和ceil时,会有精度的问题。

比如 x 1 = 0.0003 − ( 0.000 5 2 − 0.000 4 2 ) x_1=0.0003 - \sqrt{(0.0005^2-0.0004^2)} x1=0.0003(0.000520.00042) .

x 1 x_1 x1应该为 0 0 0,但是因为精度问题, x 1 x_1 x1里面存储的是0的近似值,可能比 x 1 x_1 x1大,比如 0.000000000000000101 0.000000000000000101 0.000000000000000101, 它与 0 0 0是很接近的。此时, c e i l ( x 1 ) ceil(x_1) ceil(x1)就到了 1 1 1了,而 f l o o r ( x ) floor(x) floor(x)没有问题。

也有可能 x 1 x_1 x1存储的是小于 0 0 0的一个近似值,比如 − 0.000000000000001 -0.000000000000001 0.000000000000001之类,此时 c e i l ( x 1 ) ceil(x_1) ceil(x1)没有问题, 但 f l o o r ( x 1 ) floor(x_1) floor(x1)就等于 − 1 -1 1了。这样,我们在统计答案 a n s = f l o o r ( x 2 ) − c e i l ( x 1 ) + 1 ans=floor(x_2)-ceil(x_1)+1 ans=floor(x2)ceil(x1)+1时,就可能出现错误。

可以设一个极小的值: E P S = 1 E − 14 EPS=1E-14 EPS=1E14,将答案修正为:

a n s = f l o o r ( x 2 + E P S ) − c e i l ( x 1 − E P S ) + 1 ans = floor(x_2+EPS)-ceil(x_1-EPS)+1 ans=floor(x2+EPS)ceil(x1EPS)+1

参考代码如下:

#include <iostream>
#include <cmath>
using namespace std;
#define rep(i, n) for (int i = 0; i < (n); i++)
using ll = long long;
using ld = long double;
using P = pair<int, int>;

ld X, Y, R;
const ld EPS = 1e-14;

int main() {
    cin >> X >> Y >> R;
    R += EPS;
    ll ans = 0;
    for (ll y = ceil(Y - R); y <= floor(Y + R); y++) {
        ll xl = ceil(X - sqrt(R * R - ((ld)y - Y) * ((ld)y - Y)) - EPS); //此处用EPS进行了修正
        ll xr = floor(X + sqrt(R * R - ((ld)y - Y) * ((ld)y - Y)) + EPS); // 此处用EPS进行了修正
        if (xr < xl)
            continue;
        ans += xr - xl + 1;
    }
    cout << ans << endl;
    return 0;
}
第二种方法:放大10000倍,将所有坐标点变成整数。

因为输入数据限定带4位小数,所以,放大10000倍,所有输入都变成了整数。但是,有些double数据,乘以10000倍,得到的整数却不是我们期望的值。

 double a = 0.0003;

 int b = a * 10000;

 printf("%d", b); //得到的是2

我们在转换时,要用round四舍五入,这样才能正确。

参考代码:

#include <bits/stdc++.h>
using namespace std;
#define LL long long int
#define ld long double
double X, Y, R;
#define EPS 1e-12
LL ymin, ymax, x1, x2, xd, yd, rd;
LL ans;
int main(){
    scanf("%lf %lf %lf", &X, &Y, &R);
    ymin = ceil(Y - R) * 10000;
    ymax = floor(Y + R) * 10000;
    xd = round(X * 10000), yd = round(Y * 10000), rd = round(R * 10000);
    //cerr << xd << ' ' << yd << ' ' << rd << ' '<< endl;
    for(LL i = ymin; i <= ymax; i = i + 10000){
        LL tmp = sqrt((ld) rd * rd - (ld) (yd - i) * (yd - i)); // 此处一定要转成ld,否则会有精度误差
        x1 = xd - tmp;
        x2 = xd + tmp;
        x1 = ceil(1.0 * x1 / 10000);
        x2 = floor(1.0 * x2 / 10000);
 
        ans += (x2 - x1) + 1;
    }
    printf("%lld", ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值