소수점 자릿수 맞춰 출력하기

이현진·2023년 2월 27일
0

알고리즘

목록 보기
2/9
post-thumbnail

CODETREE 문제

https://www.codetree.ai/missions/4/problems/a-divide-b/description

문제

0 <= a, b <= 100a, b를 입력받아 a/b의 결과를 특정 자리수까지 출력하는 문제이다. 단순반복문 파트인데 생각보다 쉽지 않아 당황했다.

단순 나눗셈으로 처리하면 해당 테스트케이스는 당연히 0.6처럼 나누어 떨어진 이후의 값은 출력되지 않고, f-string formatting을 사용해 소수점 자릿수를 지정해주었더니 0.59999999999999997780이 출력된다. 왜일까?

부동소수점 (Float)

컴퓨터는 실수를 표현할 때 부동소수점 방식을 사용한다. 부동소수점 방식은 실수를 가수(유효 숫자)와 지수(소숫점의 위치)로 나누어 표현한다.

부동소수점 방식은 고정 소수점 방식보다 넓은 범위의 수를 나타낼 수 있어 과학기술 계산에 많이 사용된다.

단, 부동소수점 방식은 실수를 정확히 표현하지 못하고 근사하기 때문에 오차가 생긴다. 예를 들어, 부동소수점 방식으로 표현한 0.1의 실제값은 0.1000000014901161193847656256이다.

따라서 0.1 * 3 == 0.3를 연산했을 때의 결과는 False가 되며, 이로 인한 버그를 겪기도 한다.

(자세한 동작원리는 따로 작성해야겠다...)

시도

그냥 나눗셈을 적용해서는 안 된다는 것을 알았다. 이 문제가 부동소수점의 문제라면, int로 변환해서 해결하면 어떻게 될까?

a, b = map(int, input().split())
a *= 100000000000000000000
multiple_result = str(int(a/b))

result = ''
if len(multiple_result) > 20:
    result = (multiple_result[:len(multiple_result)-21] +
        '.' +multiple_result[len(multiple_result)-20:])
else:
    result = '0.' + multiple_result

print(result)

이처럼, a에 20자리만큼 곱해줘, 정수 부분에서 연산을 처리하고자 했다. 그 후 문자열로 변환에 알맞은 위치에 소수점을 찍어주는 방식이다. 이제 보니 유효숫자와 소수점 위치를 따로 저장하는 부동소수점의 원리와 약간 비슷한 것 같다.

그러나 이 방식은 31/48의 테스트 케이스에서 645833333333333333...6.458333333333334e+19로 저장되었고, 다시 int로 변환하니 64583333333333336064라는 값이 나와버렸다. 어떻게 된 일일까?

지수 표현

Python에서는 실수를 지수 표현(exponential notation) 혹은 과학적 표기(scientific notation)라 부르는 방법으로 나타낼 수 있다.

>>> 3.33e2
333

e는 10의 거듭제곱을 뜻하며, 3.33이라는 실수에 10^2을 곱한 값이라는 뜻이다.

특히 float형의 경우, 숫자의 크기가 커지면 자동으로 지수 표현으로 출력하게 된다. 지수표현 때문에 값에 변동이 있지는 않았을까 의심했으나, float의 지수 표현은 출력할 때만 형식이 바뀌고 저장되었을 때는 여전히 부동소수점 형태라고하니, 숫자의 값에 영향을 주는 요인은 아니었다.

그럼 부동소수점으로 저장될 때부터 값에 변동이 있었다는 것이다. print를 이용해 확인해보았다.

>>> print(6.4583333333333340000)
6.458333333333334

>>> print(6.4583333333333336064)
6.458333333333334

기존값에 변동이 생긴 결과들을 자리수만 바꾸어 출력해보았고, 둘 다 같은 값이 나옴을 확인할 수 있었다!! 이는 부동소수점으로 인해 근사된 오차인 것이다. int로 변환하여 나눗셈을 진행한다해도 결과는 float으로 변환된다는 걸 간과한 오류였던 것이다.

궁금증은 해결했지만 이 방법은 안 된다는 걸 알았으니 새로운 해법을 찾을 차례이다.

해결법

해법은 간단했다. 반복문 파트의 문제였던 만큼, 나눗셈이 몫과 나머지의 반복연산이라는 원리를 반복문을 이용해 그대로 구현하면 될 문제였다.

a, b = map(int, input().split())

# a > b 인 경우를 위해 한번 나눠준다.
print('{}.'.format(a//b), end = '')
a %= b

for _ in range(20):
    a *= 10
    print(a // b, end='')
    a %= b

처음부터 생각할 수 있는 방법이었지만, "반복 나눗셈으로 소수점 아래 20자리를 어떻게 맞추지...?"라는 생각에 막혀 우회할 방법을 찾았고, 더 깊은 늪에 빠졌다 돌아왔다. 10을 곱해주고 다시 나누고를 반복하면 되는 간단한 문제인걸...

단순한 문제였지만 파생된 질문들 사이에서 새로 알게된 지식이 많았다😊

profile
세상의 모든 지식을 담을 때까지

0개의 댓글