사건의 발달은 이러하다.
어느날처럼 1일 1백준을 하려고 문제를 보다가 '브론즈 1'인데 실패인 문제를 발견했다.
'평균은 넘겠지'라는 쉬운 문제였다. https://www.acmicpc.net/problem/4344
문제는 '사람 수와 점수가 주어졌을 때, 평균을 넘는 사람 수를 백분율로 표시하라'는 것이다.
왜 실패했을까?라는 의문으로 제출을 살펴보았는데 약 2년 전에 내가 실패했었다.
2년 전의 나약한 내 자신을 보면서, 지금의 나는 얼마나 성장했을지 궁금했다.
어라? 전혀 성장하지 않았나? 코드에는 전혀 문제가 없어보이는데... 왜 틀렸지?
그래서 처음부터 새롭게 코드를 작성해보았고, 틀렸다.
왜 틀렸는지 4시간에 걸쳐 원인을 찾았고, 구조에 대한 이해를 했다.
내가 언어에 대한 이해가 부족한 것도 맞았지만, 문제 조건도 오류가 있다고 생각한다.
이를 위해서 우선 반올림의 개념에 대해서 정확하게 짚고 넘어가야 할 필요가 있다.
반올림이란,
근갓값을 구할 때 끝수를 처리하는 방법으로, 최소 단위로 나눈 나머지가 최소 단위의 절반에 미치지 못하는 경우 버리고 초과하는 경우 올리는 셈 방법이다.
반올림에는 1가지 종류만 있는 것이 아니다.
그렇다면 이런 반올림은 왜 종류가 다양한 걸까? 너무 당연한 이유이다.
경우에 따라 '어디까지가 절반인지 애매하기 때문'이다.
숫자에는 0부터 9까지의 10개의 숫자가 있다. 이때 정확하게 중간은? 4와 5이다.
그럴 때 4와 5 중 무엇을 기준으로 버리고 올림을 할지에 따라서도 종류가 나뉜다.
또한 0이 아니라 1을 시작으로 하면 9개의 숫자가 있는데, 이때 정확하게 중간은 5가 된다.
이 5를 기준으로 버릴지, 올릴지, 아니면 다른 규칙에 의해 판단할지에 따라 종류가 나뉘기에 다양한 경우가 있는 것이다.
말이 어렵지 반올림이란 결국, 무엇을 기준으로 내림과 올림을 할지 정하는 방법을 말한다.
보통 초등학교 고학년 때 배우는 개념으로 당시 수업을 기억하는 사람은 몇 없겠지만...
우리가 흔히 사용하는 반올림 방법은 사사오입(Round Half Up)이다.
이는 0부터 4까지는 버리고, 5부터 9까지는 올리는 반올림 방법이다.
이와 비슷하게 많이 사용하는 방법은 오사오입(Round Half of Even)이다.
0부터 4까지는 버리고, 6부터 9까지는 올린다.
만일 5라면 앞자리가 짝수라면 버리고, 홀수라면 올림하는 조금은 복잡한 방법이다.
공학이나 자연과학, 경제학에서 사용하는 반올림은 오사오입 방법이다.
직접 해보면 이상하게 느껴지겠지만, 통계적으로는 합리적이기 때문이다.
└ 오사오입) 0.5를 반올림하면 0이지만, 1.5를 반올림하면 2가 된다.
맨 뒷자리가 필연적으로 손실되는 사사오입보다, 수마다 확률적으로 다른 방식을 취하는 오사오입이 차이를 줄여주기 때문이다.
이를 밑바탕으로 이해해야 프로그래밍의 반올림을 이해하기 편하다.
https://www.acmicpc.net/board/view/120014
나처럼 같은 문제에 대해서 맞왜틀?을 물어본 질문을 찾았고, 답변에서 정답을 얻었다.
위의 사진은 링크를 타고 들어가면, 답변에 달린 링크를 캡처한 사진이다.
반올림 방식이 모호하다? 사사오입 방식을 사용해야 한다?
사사오입(산술적 반올림) - Round half up | 오사오입(뱅커스 라운딩) - Round half of even |
ㆍ Python2 ㆍ C ㆍ C++ ㆍ Object-C ㆍ Go ㆍ Ruby ㆍ Scala ㆍ PHP ㆍ Javascript ㆍ Java ㆍ Erlang ㆍ Fortran ㆍ Excel ㆍ Google Spread Sheet ㆍ MySQL ㆍ Hive ㆍ Redshift ㆍ BigQuery ㆍ Oracle ㆍ SQLite ㆍ Matlab ㆍ Tableau |
ㆍ Linux shell (printf) ㆍ R ㆍ Python3 ㆍ C# ㆍ Kotlin ㆍ Visual Basic ㆍ Julia ㆍ Pascal ㆍ Cobol ㆍ Mathematica ㆍ WolframAlpha |
오사오입 방식은 금융권에서 자주 사용하는 방식으로 Banker's Rounding(뱅커스 라운딩)이라고 부른다.
또한 이 방법은 Gauss법을 기반으로 하는 방식으로 Gaussian Rounding(가우시안 라운딩)이라고도 부른다.
이 방법(오사오입)은 IEEE 754의 표준 반올림 규악이다.
└ “bankers rounding,” and is the default IEEE 754
따라서 특정 몇 개의 언어와 새로 나오는 언어들은 오사오입이 default일 확률이 높다.
위의 표를 보면 Python3는 오사오입의 방식으로 반올림을 진행한다.
또한 대부분의 반올림이 필요한 코딩 문제는 IEEE 754에 따라 오사오입의 방식으로 채점한다.
하지만 4344번 문제는 사사오입의 방식으로 문제를 채점하기에 사소한 오차가 생겼고
그것이 결국 오답으로 이어졌다.
def roundTraditional(val, digits):
return round(val + 10**(-len(str(val)) - 1), digits)
이제 나에게 필요한 것은 오사오입을 사용하는 Python3에서 사사오입을 구현하는 방법이다.
Python3에는 총 3가지의 방법이 있다고 한다.
첫 번째, 사사오입 구현을 위한 함수를 구현하는 것이다.
두 번째, 0.5를 더해주고 내린다.
세 번쨰, 0.00001 같은 아주 작은 수를 더하고 round 함수를 사용한다.
첫 번째 방식의, 함수를 구현하는 코드는 위와 같다. (stackOverflow에서 퍼왔다.)
└ https://stackoverflow.com/questions/31818050/round-number-to-nearest-integer/38239574#38239574
반올림하려는 val와 몇 번째자리까지 표현할지에 대한 digits를 변수로 받아들인다.
이건 함수로 수학적으로 계산했다고 하니 이해할 수 있다.
두 번째 방식도, 오사오입의 수학적 원리를 알면 이해할 수 있었다.
└ 앞이 짝수면 내리기 때문에, 먼저 0.5를 더해주고 내려줘야 한다.
└ 이때 계산하는 수가 음수라면 더하는 것이 아니라 빼줘야 한다.
└ 0.5를 더한다기보다는 정확하게는, 표시하려는 자릿수 아래 자리수에 .5를 더해준다.
└ Ex) 0.005를 둘째자리까지 표현하려면 0.005를 더해준다.
num1 = 0.5
num2 = 0.5000001
print(round(num1, 0)) # 0.0
print(round(num2, 0)) # 1.0
하지만 세 번째 방식은 이해가 안 됐다.
0.00001 같은 작은 수를 더해주는 것은 왜...? 사사오입의 방식이 되는지 알 수 없었다.
이를 위해서는 부동소수점으로 소수를 저장하는 방법을 또 알아야 한다. 여기서는 간단하게만 정리한다.
└ 부동소수점도 IEEE 754 규격을 따른다.
└ 부동소수점에 대한 자세한 설명은 따로 정리해놓았다. https://miny-genie.tistory.com/171
num1 = 0.0055
num2 = 0.00551
print(f'{num1 : .3f}') # 0.005
print(f'{num2 : .3f}') # 0.006
0.0055를 소수 셋째짜리까지 반올림한다고 할 때, 오사오입 방식으로 계산하면 0.006이 나와야 한다.
하지만 위처럼 0.0055는 0.005가 나오고, 0.00551은 0.006이 나온다.
num1 = 0.0055
num2 = 0.00551
print(f'{num1 : .64f}') # 0.0054999999999999996808108804202674946282058954238891601562500000
print(f'{num2 : .64f}') # 0.0055100000000000001407207683712385914986953139305114746093750000
num1 = 0.0065
num2 = 0.00650001
print(f'{num1 : .3f}') # 0.006
print(f'{num2 : .3f}') # 0.007
0.0055를 저장했지만 부동소수점에 의해 실제 값은 0.005499999...562500000 값을 저장한다.
그래서 오사오입 연산을 하더라도 0.0054에서 연산을 수행하는 셈이다.
그럼 4는 버리기에 당연히 결과는 0.005가 나온다.
이러한 소수 저장 방식의 결손치를 채워주기 위해 0.00001 같은 작은 수를 더해준 뒤, 반올림하는 것이다.
이를 바탕으로 0.0065라는 수를 반올림해보자.
num1은 그냥 0.0065를, num2는 0.00000001이라는 아주 작은 수를 더했다.
둘 다 오사오입의 연산을 수행하지만 num2의 경우에는 사사오입으로 처리됨을 알 수 있다.
고로 num1은 0.006을, num2는 0.007을 출력한다.
num1 = 0.0065
num2 = 0.00650001
print(f'{num1 : .3f}') # 0.006
print(f'{num2 : .3f}') # 0.007
num1 = 0.0065
num2 = 0.006500000000000001
print(f'{num1 : .3f}') # 0.006
print(f'{num2 : .3f}') # 0.006
이때 주의할 점은 너무 작은 수를 더해주는 거는 또 안 된다는 것이다. 적당히 작은 수를 더해야 한다.
너무 작은 수를 더해버리면 결손치를 보완을 못 해주기 때문이다.
적당히 작은 수를 더해주자.
결국 이 문제는 사사오입이라는 반올림으로 채점하는데,
오사오입을 사용하는 언어로 문제를 푼 것, 문제에서 자세하게 명시하지 않은 것 두 가지가 중첩하여 오답이 뜬 거다.
우여곡절 끝에 완전히 이해했다.
지식이 늘었고, 나는 성장했다.
지금 실력으로 브론즈 1에서 4시간이 걸릴 거라고는 상상도 못해 공부의 필요성을 더 체감했다.
P.S.1
for k in [*open(0)][1:]:N,*l=map(int,k.split());print(f"{sum(i*N>sum(l)for i in l)/N+9**-9:.3%}")
정답을 맞추고 다른 사람들은 어떻게 풀었나 궁금해서 조금 돌아다니다 엄청난 것을 발견했다.
comprehension으로도 이 문제를 풀 수 있는 거였어...?
나는 해석에 성공했다.
이 글을 읽는 누군가가 있다면 당신도 도전해보길 바란다.
P.S.2
위에서 정리 & 설명할 때 0.5와 00.5 같은 예시만 있는 이유가 있다.
반올림이라 당연하다. 0부터 4는 버리는 것이 확실하고 6부터 9는 올리는 것이 확실하다.
많은 반올림 방법이 생긴 이유이자 이 글의 핵심이다.
5를 올려야 할지 내려야 할지 애매모호하기 때문이다. 그렇기에 예시의 마지막 숫자는 늘 5이다.
글을 쓰다 마치 게슈탈트 붕괴처럼 헷갈려서 적는다.
P.S.3
채점 방식에서 절대/상대 오차를 허용하는 문제가 '스페셜 저지'임을 추가로 알게 되었다.
또한 알고리즘 용어들도 알게 되었다. 공부가 더 필요하다...
ㆍ Accepted (AC)
ㆍ Presentation Error (PE)
ㆍ Wrong Answer (WA)
ㆍ Runtime Error (RE)
ㆍ Time Limit Exceeded (TLE)
ㆍ Memory Limit Exceeded (MLE)
ㆍ Output Limit Exceeded (OLE)
ㆍ Compilation Error (CE)
ㆍ Restricted Function (RF)
ㆍ Internal Error (IE)
P.S.4
이 문제를 도와주는데 흔쾌히 시간을 내 준, 내 친구에게 감사의 말을 보낸다.
고맙다 주인장놈아! https://mekain80.tistory.com/
메카인의 지식창고
mekain80.tistory.com
+ 2023.06.27 수정
아니 이게 왜 스페셜 저지가 추가...?
정확하게 알았으니 감사하고 넘어가도록 하자
'Computer Science > 파이썬(Python)' 카테고리의 다른 글
Python print (0) | 2023.06.28 |
---|---|
Python Round(반올림) 요약 (0) | 2023.06.24 |
Python lambda (0) | 2023.04.03 |
Python f string format (0) | 2023.04.01 |
Python zip (0) | 2023.03.30 |