solved.ac 마라톤 뛰다가 튀어나왔는데 부동소수점 오차때문에 피똥싼 문제다
나같은 사람이 또 있나 궁금해서 구글링해 봤는데 없길래 내가 글을 쓴다
https://www.acmicpc.net/problem/17357
이 문제를 풀려면 세가지를 알아야 한다.
- 분산에 대한 지식 (제곱의 평균 - 평균의 제곱)으로 분산을 빠르게 구할 수 있다
- 평균을 빠르게 구하기 위해 누적합을 사용하는 방법
- 부동소수점 오차 줄이기
1. 표준편차(분산) 빠르게 구하기
표준편차의 제곱은 분산이고, 이 문제에서는 표준편차의 대소비교를 요구하는데
표준편차a <= 표준편차b 이면,
표준편차a의제곱 <= 표준편차b의제곱
그러니까 표준편차끼리 비교할 필요 없이 분산끼리 비교해도 충분하다.
분산의 = 제곱의 평균 - 평균의 제곱 이라는 공식을 활용하면 그나마 빠르게 계산이 가능하다.
2. 평균 빠르게 구하기 위해 누적합 활용하기
그런데 평균을 구할려니깐 시간복잡도가 마음에 걸린다.
알다시피 평균은 (원소들의 합)/원소들의 개수 로 구할 수 있는데, N개 원소들의 합을 구하기 위해 루프를 N번 돌면 비효율적이다(이 문제에서는 N이 1000이라서 상관없긴 하다)
그래서 누적합을 활용하고, a번 원소부터 b번 원소까지의 합을 구하기 위해 [b번까지의 누적합]-[a-1번까지의 누적합]으로 계산하면 O(N)의 시간이 걸리는 연산을 O(1)만에 할 수 있다.
3. 부동소수점 오차
CS(콤퓨타사이언스) 기본 지식이다. 컴퓨터에서 실수를 표현하는 방법에는 고정 소수점 방법과 부동 소수점 방법이 있는데, 부동 소수점 방식이 더 많은 범위의 수를 표현할 수 있기에 대부분 IEEE754 표준에 맞는 부동 소수점 방법을 채택한다.
IEEE754의 부동 소수점 방식은 어떤 수를 1.xxxxxx * 2^yyyyyyyyyyyyyy -127 이런식으로 근삿값을 구한 뒤
(부호)(xxxxxxx)(yyyyyyyyyyyyyy) 이렇게 메모리상에서 관리하는 방법임.
이런 근삿값을 구하는 방법은 채찍피티한테 물어보면 아주 잘 설명해준다.
0.765625를 부동 소수점 방식으로 나타내는 방법을 설명하겠습니다. 여기서는 IEEE 754 표준 32비트 단정도(float) 부동소수점 방식으로 나타내는 방법을 다루겠습니다.
1단계: 0.765625를 이진수로 변환
0.765625를 이진수로 변환하는 방법은 다음과 같습니다:
- 0.765625 × 2 = 1.53125 → 정수 부분 1
- 0.53125 × 2 = 1.0625 → 정수 부분 1
- 0.0625 × 2 = 0.125 → 정수 부분 0
- 0.125 × 2 = 0.25 → 정수 부분 0
- 0.25 × 2 = 0.5 → 정수 부분 0
- 0.5 × 2 = 1.0 → 정수 부분 1
따라서, 0.765625는 이진수로 0.110001입니다.
2단계: 0.765625의 이진수 표기 (Scientific Notation)
이제 이진수를 과학적 표기법으로 나타냅니다. 이진수 0.110001을 1.xxxx 형태로 나타내려면 소수점을 오른쪽으로 1번 이동해야 합니다. 따라서:
0.110001=1.10001×2−10.110001 = 1.10001 \times 2^{-1}
3단계: 부동소수점 표현
IEEE 754 32비트 부동소수점 형식은 다음과 같은 구성을 가집니다:
- 부호 비트 (1 비트)
- 지수 (8 비트)
- 가수 (mantissa) (23 비트)
부호 비트
0.765625는 양수이므로 부호 비트는 0입니다.
지수
지수는 2의 거듭제곱을 나타내는데, 이 경우 **2^{-1}**입니다. IEEE 754에서 지수는 bias 127을 사용하여 저장합니다. 따라서, 지수는:
지수=−1+127=126\text{지수} = -1 + 127 = 126
126을 이진수로 나타내면 01111110입니다.
가수 (Mantissa)
가수는 소수점 오른쪽에 있는 비트들을 나타냅니다. 이때 1.10001에서 1은 가수에 포함되지 않고, 나머지 10001000000000000000000만 저장됩니다.
4단계: 최종 부동소수점 표현
따라서, 0.765625를 IEEE 754 부동소수점 형식으로 표현하면:
- 부호 비트: 0
- 지수: 01111110
- 가수: 10001000000000000000000
전체적으로 0 01111110 10001000000000000000000가 됩니다.
최종 결과:
0.765625의 IEEE 754 32비트 단정도 부동소수점 표현은:
0 01111110 10001000000000000000000
이 값은 32비트로, 부호 비트 1비트 + 지수 8비트 + 가수 23비트로 구성됩니다.
이러한 부동 소수점 방식에는 꼭 알아야 할 문제점이 있는데. (사실 이것도 채찍피티한테 물어보면 잘 알려준다.)
부동 소수점 방식은 숫자를 근삿값으로 표현한다는 특성 때문에 필연적으로 오차가 발생하고, 코드에서 부동 소수점 연산을 많이 하면 할수록 이러한 오차가 누적되면서 틀렸습니다!! 를 받게 된다.
아래는 부동소수점 오차가 누적되는 예시 코드이다.
#include <bits/stdc++.h>
using namespace std;
int main(void) {
float n01 = 0.1;
float n03 = 0.3;
float c01 = 0;
float c03 = 0;
for (int i = 0; i < 10000; i++)
{
c01+=n01;
c03+=n03;
}
cout<<fixed<<setprecision(9)<<c01<<", "<<c03<<"\n";
//출력 결과>> 999.902893066, 3000.292236328
return 0;
}
그래서 부동소수점 오차를 어떻게 줄일 수 있을까??
실수를 이용한 연산을 최소화하고 최대한 정수를 이용해 계산하면 된다.
- 소수의 분자와 분모를 정수만 이용해 따로 계산한 뒤 마지막에 분자/분모로 계산한다.
- 계산 중간에 발생하는 소수에 적절한 값을 곱해 정수로 바꿀 수 있다면 그렇게 한 뒤 마지막에 딱 한번만 나눗셈을 한다.
- float를 쓰고 있다면 float보다 오차가 적은 double을 사용한다
나는 2 방법을 활용해 정답의 오차를 줄일 수 있었다. 문제가 어려운 문제가 아니라서 1의 방법도 되긴 할거임.
코드
#include <bits/stdc++.h>
#define fastio cin.tie(0)->sync_with_stdio(0)
#define ll long long
#define pii pair<int,int>
using namespace std;
ll arr[1001];
ll csum[1001];//arr의 누적합
ll ccsum[1001];//arr의 제곱의 누적합
int main(void){
fastio;
int N;cin>>N;
for (int i = 1; i <= N; i++)
{
cin>>arr[i];
csum[i]=csum[i-1]+arr[i];
ccsum[i]=ccsum[i-1]+arr[i]*arr[i];
}
for (int k = 1; k <= N; k++)
{
ll ans = 1;
ll mm = 0;
for (int i = 1; i+k-1 <= N; i++)
{
ll av1,av2;
av1=csum[i+k-1]-csum[i-1];//i번부터 i+k-1번 원소까지의 합
av2=ccsum[i+k-1]-ccsum[i-1];//i번부터 i+k-1번 원소까지 제곱의 합
ll tmp = av2*k-av1*av1; //분산끼리 비교하는 대신 k*분산끼리 비교(실수끼리 비교하는 대신 정수끼리 비교)
if(tmp>mm){
ans=i;mm=tmp;
}
}
cout<<ans<<"\n";
}
return 0;
}
'C++ > 문제풀이 기록' 카테고리의 다른 글
[BOJ 33040, 33041 (조금 어려움)] 마작 거신병 (C++) (1) | 2024.12.29 |
---|---|
[BOJ 26076, 무난함] RPG Extreme (C++) (0) | 2024.12.12 |
[BOJ 26076, 조금 어려움] 곰곰이의 식단 관리 2 (C++) (0) | 2024.07.01 |
[BOJ 1918, 조금 어려움] 후위 표기식 (C++) (1) | 2023.10.19 |
백준 제 1회 와쿠컵 후기(12솔) + 27965, 27972 풀이 (C++) (3) | 2023.04.21 |