무엇을 만드나
측정하지 않고 세기만 해서 원주율을 구할 수 있을까요?
주어진 문제
한 변이 1인 정사각형 안에 점 여러 개의 좌표가 주어집니다. 그중 원점에서 거리가 1 이하인 점이 몇 개인지 세어 원주율의 어림값을 구해 내놓는 프로그램을 만듭니다.
자를 대고 측정하지 않고 세기만 해서 원주율이 나옵니다. 점의 비가 넓이의 비에 가까워진다는 생각 하나로 됩니다. 정확한 값이 아니라 어림값이라는 점도 함께 봐 두어야 합니다.
| 단계 | 여기서 답할 질문 |
|---|---|
| 1 요구 분석 | 좌표를 어떻게 받고 어디까지 정확해야 하는가 |
| 2 설계 | 원 안인지 어떻게 판정하고 왜 4를 곱하는가 |
| 3 구현 | 여러 줄로 들어오는 값을 어떻게 받아 셀 것인가 |
| 4 테스트 | 답이 늘 달라지는 프로그램을 어떻게 확인하는가 |
먼저 개발 문서 양식을 내려받으세요
읽기만 하지 말고 직접 채워 보세요. 단계마다 자기 답을 적어 두면 그 기록이 요구 사항 명세서이자 설계서가 됩니다. 답을 보기 전에 먼저 적어 봅니다.
요구 분석과 설계는 언어와 상관없습니다. 두 양식의 앞부분은 같고, 구현을 적는 자리만 언어에 맞춰 다릅니다. 수업에서 쓰는 언어에 맞는 것을 받으면 됩니다. 이 강의노트의 코드 예시는 파이썬으로 되어 있습니다.
1단계 · 무엇을 만들 것인가
「원주율을 구한다」는 어디까지 구하라는 걸까요?
되물어야 할 것들
- 점을 어떻게 받나요? 개수를 먼저 알려 주나요
- 좌표의 범위는? 음수도 들어오나요
- 딱 경계에 있는 점은? 거리가 정확히 1이면 안인가요 밖인가요
- 얼마나 정확해야 하나요? 3.14가 나와야 통과인가요
- 점이 하나도 없으면? 나눌 수가 없습니다
정리한 요구 사항
- 첫 줄에 점의 개수를 받고, 그다음 줄부터 한 줄에 좌표 하나씩 받는다.
- 좌표는 0 이상 1 이하의 실수 두 개를 공백으로 띄어 받는다.
- 원점에서의 거리가 1 이하이면 안쪽으로 센다. 정확히 1인 점도 안쪽이다.
- 어림값은 4 × (안쪽 점 수 ÷ 전체 점 수)로 구한다.
- 구한 값을 그대로 내놓는다. 자릿수를 줄이지 않는다.
- 점의 개수는 1 이상이 들어온다고 본다.
「3.14가 나와야 한다」는 요구 사항이 될 수 없습니다
이 프로그램의 답은 넣은 점이 무엇이냐에 따라 달라집니다. 점이 넷이면 나올 수 있는 값은 0, 1, 2, 3, 4뿐이라 3.14는 아예 나올 수 없습니다. 어림하는 프로그램에 「정확히 얼마」를 요구할 수는 없습니다. 요구할 수 있는 것은 「정해진 식대로 계산했는가」뿐입니다. 그래서 4번을 식으로 못박아 적었습니다.
요구 사항 3번에서 거리가 정확히 1인 점을 안쪽으로 넣기로 했습니다. 바깥으로 정했다면 답이 달라질까요? 그런 점이 실제로 몇 개나 나올지 생각해 보세요.
2단계 · 왜 4를 곱하나
넓이의 비를 점의 비로 바꾸는 것이 전부입니다.
| 토막 | 무엇을 정하는가 |
|---|---|
| 입력 | 점의 개수 하나, 그다음 개수만큼의 좌표 줄 |
| 처리 | 점마다 원 안인지 보고 세어, 비율에 4를 곱한다 |
| 출력 | 어림값 하나 |
넓이의 비에서 시작한다
한 변이 1인 정사각형의 넓이는 1입니다. 그 안에 든 반지름 1짜리 사분원의 넓이는 원 넓이의 4분의 1이니 원주율 ÷ 4입니다. 점을 고르게 흩어 놓으면 점이 떨어지는 비율이 넓이의 비율에 가까워집니다.
그러니 안쪽 점 ÷ 전체 점 ≈ 원주율 ÷ 4이고, 양쪽에 4를 곱하면 원주율 ≈ 4 × (안쪽 점 ÷ 전체 점)이 됩니다. 4를 곱하는 이유는 사분원이 원의 4분의 1이기 때문입니다.
원 안인지 어떻게 아나
원점에서 점까지의 거리가 1보다 크지 않으면 안쪽입니다. 그런데 거리를 실제로 구할 필요는 없습니다. 거리는 x² + y²의 제곱근인데, 제곱근을 구하기 전 값끼리 비교해도 결과가 같기 때문입니다. 1의 제곱근은 1이니까요.
그래서 x² + y²가 1 이하인지만 보면 됩니다. 계산이 줄고, 제곱근을 거치며 생길 수 있는 어림 오차 하나를 덜 수 있습니다.
푸는 순서
- 점의 개수를 입력받는다.
- 안쪽 점의 수를 0으로 놓는다.
- 개수만큼 반복한다.
- 좌표 두 개를 입력받는다.
- x² + y²가 1 이하이면 안쪽 점의 수를 1 늘린다.
- 반복이 끝나면 4 × (안쪽 점 ÷ 전체 점)을 내놓는다.
6번이 반복 밖에 있어야 합니다
내놓는 줄이 반복 안에 들어가면 점마다 한 번씩 값이 찍힙니다. 답이 하나가 아니라 여러 줄이 나옵니다. 세는 일이 다 끝난 뒤에 나눠야 합니다. 들여쓰기 한 칸이 이것을 결정합니다.
점 네 개 (0.1, 0.2) (0.9, 0.8) (0.3, 0.4) (0.5, 0.5)를 넣으면 안쪽은 몇 개이고 어림값은 얼마일까요? 손으로 제곱해서 세어 보세요.
3단계 · 파이썬으로 옮기기
여러 줄로 들어오는 값은 어떻게 받을까요?
먼저 스스로 적어 본 뒤에 코드를 보세요. 눈여겨볼 점이 하나 있습니다. 입력을 반복 안에서 받습니다.
파이.기본.py
# 점의 개수를 먼저 받고, 그다음 줄부터 좌표를 하나씩 받는다
n = int(input())
inside = 0
for _ in range(n):
x, y = map(float, input().split())
# 제곱근을 구하지 않고 제곱한 채로 비교한다
if x ** 2 + y ** 2 <= 1:
inside += 1
print(4 * inside / n)몇 번 돌지를 미리 알아야 합니다
좌표를 받기 전에 개수를 먼저 받는 이유가 여기 있습니다. 몇 번 반복할지 알아야 반복을 시작할 수 있습니다. 개수를 안 주고 「끝날 때까지」 받으려면 끝을 알리는 다른 약속이 필요해집니다. 요구 사항 1번이 개수를 먼저 받기로 정한 것은 프로그램을 단순하게 만들려는 결정입니다.
반복 안에서 x와 y는 매번 덮어쓰입니다. 그런데도 답이 맞는 이유는 무엇일까요? 무엇을 남겨 두고 있는지 찾아보세요.
4단계 · 넣어 보고 확인하기
답이 늘 달라지는 프로그램은 어떻게 확인할까요?
내가 넣은 점으로 손으로 계산한 값과 맞는지를 봅니다. 이 프로그램은 「정답」이 하나로 정해져 있지 않기 때문입니다. 점을 일부러 골라 답을 미리 알 수 있게 만듭니다.
| 넣은 값 | 나와야 할 결과 | 무엇을 보려는 값인가 |
|---|---|---|
| 1개 / (0.5, 0.5) | 4.0 | 전부 안쪽인 가장 작은 경우 |
| 1개 / (1.0, 1.0) | 0.0 | 전부 바깥인 경우 — 0이 제대로 나오는지 |
| 1개 / (1.0, 0.0) | 4.0 | 거리가 정확히 1인 경계 — 요구 사항 3번 |
| 4개 / (0.1,0.2) (0.9,0.8) (0.3,0.4) (0.5,0.5) | 3.0 | 넷 중 셋이 안쪽 — 4 × 3 ÷ 4 |
| 2개 / (0.0,0.0) (1.0,1.0) | 2.0 | 절반이 안쪽인 경우 |
결과가 어긋났다면 여기부터 봅니다
- 답이 여러 줄 나온다 — 내놓는 줄이 반복 안에 있습니다
- 언제나 4가 나온다 — 조건을 보지 않고 모든 점을 세고 있습니다
- (1.0, 0.0)이 0으로 나온다 — 「미만」으로 적었습니다. 이하여야 합니다
- 정수만 나온다 — 나눌 때 몫만 구하고 있습니다
- 답이 4보다 크다 — 안쪽 수와 전체 수를 뒤바꿔 나눴습니다
3.14가 안 나온다고 틀린 것이 아닙니다
점이 몇 개 안 되면 어림값은 3.14 근처에도 못 갑니다. 이것은 버그가 아니라 이 방법의 성질입니다. 점을 고르게 무작위로 많이 넣을수록 대체로 가까워지고, 그래도 넣을 때마다 값이 조금씩 다릅니다. 「틀렸다」와 「정확하지 않다」를 구별해서 볼 줄 알아야 이런 프로그램을 다룰 수 있습니다.
표의 4개짜리 줄을 손으로 검산해 보세요. (0.9, 0.8)은 왜 바깥일까요? 제곱해서 더해 보면 됩니다.
더 해 보기
점을 직접 만들어 보면 성질이 눈에 보입니다.
점을 손으로 넣는 데는 한계가 있습니다. 프로그램이 무작위로 점을 만들게 하면 개수를 마음껏 늘려 보며 많아질수록 정말 가까워지는지 확인할 수 있습니다.
스스로 해 본 뒤에 열어 보세요
더 알아보기: 점을 직접 만들어 쓰는 코드
좌표를 받는 줄이 만드는 줄로 바뀌었을 뿐 나머지는 그대로입니다.
random.random()은 부를 때마다 0 이상 1 미만의 실수 하나를 무작위로 돌려주므로, 좌표가 요구 사항의 범위 안에 들어옵니다.
개수를 10, 1000, 100000으로 바꿔 가며 실행해 보세요.
import random
# 점을 입력받는 대신 직접 만들어 쓴다
n = int(input())
inside = 0
for _ in range(n):
x = random.random()
y = random.random()
if x ** 2 + y ** 2 <= 1:
inside += 1
print(4 * inside / n)한 걸음 더
- 같은 개수로 여러 번 실행해 값이 얼마나 흔들리는지 보기
- 개수를 열 배씩 늘려 가며 자릿수가 얼마나 맞아 가는지 표로 적어 보기
- 점의 개수가 0일 때 나누지 않고 알리도록 막기 (요구 사항 6번을 푸는 일)
개수를 열 배 늘리면 정확한 자릿수도 열 배 늘어날까요? 직접 실행해 보고 생각과 달랐다면 무엇이 달랐는지 적어 보세요.