ACM中的浮點數精度處理

    在ACM中,精度問題很是常見。其中計算幾何頭疼的地方通常在於代碼量大和精度問題,代碼量問題只要平時注意積累模板通常就不成問題了。精度問題則很差說,有時候一個精度問題就可能成爲一道題的瓶頸,讓你debug半天都找不到錯誤出在哪。函數

1.浮點數爲啥會有精度問題:spa

浮點數(以C/C++爲準),通常用的較多的是float, double。debug

 

佔字節數設計

數值範圍code

十進制精度位數blog

float內存

4ci

-3.4e-38~3.4e38hash

6~7io

double

8

-1.7e-308~1.7e308

14~15

若是內存不是很緊張或者精度要求不是很低,通常選用double。14位的精度(是有效數字位,不是小數點後的位數)一般夠用了。注意,問題來了,數據精度位數達到了14位,但有些浮點運算的結果精度並達不到這麼高,可能準確的結果只有10~12位左右。那低幾位呢?天然就是不可預料的數字了。這給咱們帶來這樣的問題:即便是理論上相同的值,因爲是通過不一樣的運算過程獲得的,他們在低幾位有可能(通常來講都是)是不一樣的。這種現象看似沒太大的影響,卻會一種運算產生致命的影響: ==。恩,就是判斷相等。注意,C/C++中浮點數的==須要徹底同樣才能返回true。來看下面這個例子:

#include<stdio.h>
#include<math.h>

int main()

{
      double a = asin( sqrt( 2.0 ) / 2 ) * 4.0;
      double b = acos( -1.0 );
      printf( "      a = %.20lf\n", a );
      printf( "      b = %.20lf\n", b );
      printf( " a -  b = %.20lf\n", a - b );
      printf( " a == b = %d\n", a == b );
      return 0;
}

輸出:

      a = 3.14159265358979360000

      b = 3.14159265358979310000

      a - b = 0.00000000000000044409

      a == b = 0

咱們解決的辦法是引進eps,來輔助判斷浮點數的相等。

2. eps

       eps縮寫自epsilon,表示一個小量,但這個小量又要確保遠大於浮點運算結果的不肯定量。eps最多見的取值是1e-8左右。引入eps後,咱們判斷兩浮點數a、b相等的方式以下:

定義三出口函數以下: int sgn(double a){return a < -eps ? -1 : a < eps ? 0 : 1;}

則各類判斷大小的運算都應作以下修正:

傳統意義

修正寫法1

修正寫法2

a == b

sgn(a - b) == 0

fabs(a – b) < eps

a != b

sgn(a - b) != 0

fabs(a – b) > eps

a < b

sgn(a - b) < 0

a – b < -eps

a <= b

sgn(a - b) <= 0

a – b < eps

a > b

sgn(a - b) > 0

a – b > eps

a >= b

sgn(a - b) >= 0

a – b > -eps

這樣,咱們才能把相差很是近的浮點數判爲相等;同時把確實相差較大(差值大於eps)的數判爲不相等。

PS: 養成好習慣,儘可能不要再對浮點數作==判斷。例如,個人修正寫法2裏就沒有出現==。

 

3. eps帶來的函數越界

若是sqrt(a), asin(a), acos(a) 中的a是你本身算出來並傳進來的,那就得當心了。

若是a原本應該是0的,因爲浮點偏差,可能實際是一個絕對值很小的負數(好比1e-12),這樣sqrt(a)應得0的,直接因a不在定義域而出錯。

相似地,若是a原本應該是±1,則asin(a)、acos(a)也有可能出錯。

所以,對於此種函數,必需事先對a進行校訂。

4. 輸出陷阱I

這一節和下一節同樣,都是由於題目要求輸出浮點數,致使的問題。並且都和四捨五入有關。

說到四捨五入,就再扯一下相關內容,據我所知有三種常見的方法:

1. printf(「%.3lf」, a);  //保留a的三位小數,按照第四位四捨五入

2. (int)a;  //將a靠進0取整

3. ceil(a); floor(a);   //顧名思義,向上取證、向下取整。須要注意的是,這兩個函數都返回double,而非int

其中第一種很常見於輸出(nonsense…)。

如今考慮一種狀況,題目要求輸出保留兩位小數。有個case的正確答案的精確值是0.005,按理應該輸出0.01,但你的結果多是0.005000000001(恭喜),也有多是0.004999999999(悲劇),若是按照printf(「%.2lf」, a)輸出,那你的遭遇將和括號裏的字相同。

解決辦法是,若是a爲正,則輸出a+eps, 不然輸出a-eps

典型案例: POJ2826

5. 輸出陷阱II

ICPC題目輸出有個不成文的規定(有時也成文),不要輸出: -0.000

那咱們首先要弄清,何時按printf(「%.3lf\n」, a)輸出會出現這個結果。

直接給出結果好了:a∈(-0.000499999……, -0.000……1)

因此,若是你發現a落在這個範圍內,請直接輸出0.000。更保險的作法是用sprintf直接判斷輸出結果是否是-0.000再予處理。

典型案例:UVA746

 

6. 範圍越界

這個嚴格來講不屬於精度範疇了,不過湊數仍是能夠的。請注意,雖然double能夠表示的數的範圍很大,卻不是不窮大,上面說過最大是1e308。因此有些時候你得當心了,好比作連乘的時候,必要的時候要換成對數的和。

典型案例:HDU3558

7. 關於set<T>

有時候咱們可能會有這種需求,對浮點數進行 插入、查詢是否插入過 的操做。手寫hash表是一個方法(hash函數同樣要當心設計),但set不是更方便嗎。但set好像是按==來判重的呀?貌似行不通呢。經觀察,set不是經過==來判斷相等的,是經過<來進行的,具體說來,只要a<b 和 b<a 都不成立,就認爲a和b相等,能夠發現,

若是將小於定義成:      bool operator < (const Dat dat)const{return val < dat.val - eps;}就能夠解決問題了。 (基本類型不能重載運算符,因此封裝了下)

8. 輸入值波動過大

這種狀況不常見,不過能夠幫助你更熟悉eps。假如一道題輸入說,給一個浮點數a, 1e-20 < a < 1e20。那你還敢用1e-8作eps麼?合理的作法是把eps按照輸入規模縮放到合適大小。

典型案例: HUSTOJ 1361

9. 一些建議

容易產生較大浮點偏差的函數有asin、 acos。歡迎儘可能使用atan2。

另外,若是數據明確說明是整數,並且範圍不大的話,使用int或者long long代替double都是極佳選擇,這樣就不存在浮點偏差了

相關文章
相關標籤/搜索