랜덤확수의 확률 변경과 분포에 대한 문제
이 질문 역시 내가 면접에서 질문 받았던 문제이다. 틀렸던 문제를 다시 한번 살펴보고 공부하는 차원에서 오답노트에 적어둔다. (Q는 면접관님, A는 내가 답변한 것이다. 존칭은 생략한다.)
- Q. 랜덤함수에서 0과 1이 나올 확률이 같은가?
- A. 같다. 내가 알기로는 0~1까지의 소수가 나오는 것으로 알고 있다. (여기서 대답을 잘못했다. 플래시의 랜덤함수와 C에서의 랜덤함수를 헷갈렸던 것이다.)
- Q. 정말? 그렇다면 0~10까지 나오게 하려면 어떻게 해야하는가?
- A. r이 그 랜덤함수값이라고 한다면 (int)(r * 10)으로 구할 수 있다. (여기서도 답이 정확하지 않았다.
r이 [0, 1) 구간의 실수라면(int)(r * 10)은 0~9만 나온다. 10이 나오려면r이 정확히 1.0이어야 하는데, [0, 1) 반개구간에서는 그 값이 나오지 않는다. 0~10까지 11가지 값을 고르게 얻으려면(int)(r * 11)을 쓰거나, 애초에 실수 난수 대신rand() % 11처럼 정수 난수를 쓰는 것이 낫다.) - Q. 그렇게 한 경우에는 0~10까지 모든 수가 나올 확률은 같은가?
- A. 같다고 알고 있다.
- Q. 만약 1~5까지의 어떤 수를 뽑는 랜덤함수가 있다면 확률을 바꾸고 싶다. 1,5는 가장 적게 나오도록, 2,4는 그다음으로 많이 나오도록 3은 제일 많이 나오도록 결과적으로는 확률분포가 삼각형 모양이 되게 하고 싶다. 어떻게 하면 되겠는가?
여기까지가 질문이었는데 확률을 바꾸라는 말에 제대로 대답을 못했다.
2026년 수정 안내 이 글에는 두 가지 오류가 있었다. 10만 번 시행에서 정확히 20%가 안 나온 것을 “rand()의 분포가 고르지 않다는 증거”로 해석한 부분과, 면접관이 묻던 “1,2,4,4,2,1처럼 가운데가 볼록한 분포”를 “균등분포를 정규분포로 바꾸는 문제”로 잘못 짚은 부분이다. 둘 다 바로잡는다.
일단 첫번째는 랜덤함수는 0~1까지의 소수가 나오는 것이 아니었다. 그리고 랜덤함수의 확률 분포는 생각보다 그렇게 고르지 않다고 한다. (이에 대한 자료는 http://agebreak.tistory.com/49 와 http://ljh131.tistory.com/131 에 있다.) 내 생각에는 랜덤을 반복할 수록 점점 확률이 고르게 분포하지 않을까 싶다.
당시에는 위 면접관분이 질문했던 내용을 찾아보니 정확하게는 균등분포를 정규분포로 바꾸는 문제라고 결론 내리고, http://tadis.tistory.com/14 의 Box-Muller 변환 코드를 참조했었다. 그런데 이 진단부터 잘못됐다 — 아래 “정확히 다시 보면” 절에서 바로잡는다.
정확히 다시 보면 : 정규분포가 아니라 삼각분포다
면접관이 물었던 것은 1, 2, 3, 4, 5가 각각 10%, 20%, 40%, 20%, 10%로 — 가운데(3)가 가장 높고 양 끝으로 갈수록 낮아지는 삼각형 모양의 분포였다. 정규분포는 종형 곡선이라 삼각형 모양과 비슷해 보이지만, 이 문제는 정규분포와 무관하고 Box-Muller 변환도 필요 없다.
가장 흔히 쓰이는 정답은 균등난수 두 개를 더하는 것이다. 주사위 두 개를 굴려 합을 구하면 2와 12가 가장 드물고 7이 가장 많이 나오는, 가운데가 볼록한 삼각분포가 자연스럽게 만들어지는 것과 같은 원리다.
#include <random>
std::mt19937 engine( std::random_device{}() );
std::uniform_int_distribution<int> die( 1, 5 ); // 1~5 균등분포
int PickTriangular()
{
// 두 번 뽑아 평균을 내면 가운데(3)로 몰리는 삼각분포가 된다.
int a = die( engine );
int b = die( engine );
return ( a + b + 1 ) / 2; // 반올림하여 1~5 범위로
}
또는 원하는 비율을 직접 테이블로 정의하고 뽑는 방법도 있다. 표준 라이브러리를 쓰면 구간을 손으로 나누지 않아도 된다.
std::discrete_distribution<int> dist( { 10, 20, 40, 20, 10 } ); // 비율 그대로 가중치로
int value = dist( engine ) + 1; // 0~4 → 1~5
std::discrete_distribution은 가중치 목록을 주면 그 비율대로 뽑아주므로, 아래에서 손으로 구간을 나눴던 방식(r < 10, r < 30, …)을 대신해준다.
다음은 간단히 1부터 5까지 랜덤한 수를 100000개 뽑아보는 코드다.
#include <iostream>
#include <time.h>
using namespace std;
#define MAX_TEST 100000
int main()
{
int count[5] = {0};
int percent[5] = {10, 20, 40, 20, 10};
srand((unsigned)time(NULL));
for (int i = 0; i < MAX_TEST; i++)
{
int r = rand() % 5 + 1;
count[r - 1]++;
}
float total = 0.0;
for (int i = 0; i < 5; i++)
{
cout << i + 1 << "의 갯수 :: " << count[i] << " - " << count[i] / (float)(MAX_TEST / 100) << "%" << endl;
total += (count[i] / (float)(MAX_TEST / 100));
}
return 0;
}
여기서 알 수 있는건 분포가 정확하게 20%씩은 아니라는 점이었다. (다만 이건 rand()가 고르지 않다는 증거가 아니라, 유한 횟수 시행에서 나타나는 정상적인 표본 변동이다. 아래에서 정리한다.)
이 코드를 1부터 5까지 랜덤한 수를 정해진 비율에 맞도록 100000개 뽑아내는 코드로 변경한 것이다.
#include <iostream>
#include <time.h>
using namespace std;
#define MAX_TEST 100000
int main()
{
int count[5] = {0};
int percent[5] = {10, 20, 40, 20, 10};
srand((unsigned)time(NULL));
for (int i = 0; i < MAX_TEST; i++)
{
int r = rand() % 100;
if (r < 10)
{
count[0]++;
}
else if (r < 30)
{
count[1]++;
}
else if (r < 70)
{
count[2]++;
}
else if (r < 90)
{
count[3]++;
}
else if (r < 100)
{
count[4]++;
}
}
for (int i = 0; i < 5; i++)
{
cout << i + 1 << "의 갯수 :: " << count[i] << " - " << count[i] / (float)(MAX_TEST / 100) << "%" << endl;
}
return 0;
}
단순히는 이렇게 하면 정해진 비율에 유사하게 값이 나온다.
10만 번을 뽑아도 정확히 20%가 안 나온 이유
앞서 “20%씩 고르게 나오지 않았다”를 두고 rand()의 분포가 고르지 않은 것 아니냐고 짐작했는데, 이건 정상적인 표본 변동(sampling variation)이다. 진짜 균등분포를 따르는 이상적인 주사위를 10만 번 굴려도 각 눈이 정확히 20,000번씩 나오지는 않는다. 동전을 100번 던져 정확히 50번 앞면이 나오길 기대하지 않는 것과 같은 이야기다.
시행 횟수를 늘릴수록 관측 비율이 이론값(20%)에 가까워지는 것이 큰 수의 법칙이지, 정확히 일치하는 것이 아니다. 실제로 편차의 크기는 대략 1/√N에 비례해 줄어든다. 10만 번이면 각 항목의 표준편차가 대략 ±0.13%p 수준이라, 19.8~20.2% 사이에서 왔다 갔다 하는 것은 정상 범위다.
(다만 rand() % 5라는 방식 자체에는 별개의 문제가 있다. RAND_MAX가 5로 나누어떨어지지 않으면 낮은 값 쪽이 아주 미세하게 더 자주 나오는 모듈로 편향(modulo bias)이 생긴다. 다만 이건 관측된 편차의 원인이라기엔 너무 작고, 위에서 본 표본 변동이 주된 원인이다. C++11부터는 <random>의 std::uniform_int_distribution을 쓰면 이 편향 자체가 없다.)
추가로 난수발생에 대한 좋은 글들은 http://oops.kldp.org/node/103420 , http://agebreak.tistory.com/49 , http://terzeron.net/wp/?p=1006 에서 찾을 수 있었다.
비율이 유사하게가 아니라 정확한 비율대로 나오려면 배열에 미리 가능한 수를 넣어놓고 랜덤하게 위치를 변경시키는 방법 밖에는 아직 생각나지 않는다. 조금 더 생각하고 공부해봐야 할 문제인 것 같다.
댓글 남기기