일반적인 난수는 '균등 분포(uniform distribution)'를 따릅니다. 즉, 어떤 숫자든 선택될 확률이 모두 동일하다는 의미입니다.
하지만 특정 숫자가 다른 숫자보다 더 자주 선택되기를 원한다면 다른 전략이 필요합니다. 바로 가중 난수 생성기(weighted random number generator)입니다.
실제 활용 사례는 다음과 같습니다:
- 게임의 루트 테이블(loot table): 적이 아이템을 서로 다른 드롭률로 떨어뜨리는 경우
- 추첨 행사: 티켓을 더 많이 구매한 사람이 당첨 확률이 더 높은 경우
간단한 전략: 배열에 복사본 추가하기
추첨 예시를 생각해 보면 명확한 해결책이 떠오릅니다. 각 '티켓'마다 항목의 복사본을 하나씩 담은 배열을 만드는 것입니다.
예를 들어, John이 추첨권 4장을 구매하고 David는 1장만 구매했다면, John은 David보다 당첨 확률이 4배 높아집니다.
실제 동작하는 구현 예제입니다:
users = { john: 4, david: 1 }
raffle = []
users.map do |name, tickets|
tickets.times { raffle << name }
end
p raffle
# [:john, :john, :john, :john, :david]
p raffle.sample
# :john
구매한 티켓 수만큼 해당 사람의 이름을 목록에 추가한 뒤, 그 목록에서 무작위로 이름을 하나 뽑습니다. 목록에 더 많이 포함된 이름일수록 선택될 확률이 자연스럽게 높아집니다.
이 접근 방식은 매우 단순하다는 장점이 있으며, 한 번 목록을 만들어 두면 당첨자를 뽑는 속도도 매우 빠릅니다.
가중치의 합을 활용하는 방법
메모리를 더 효율적으로 사용할 수 있는 또 다른 방법도 있습니다. 다만 무작위 값을 뽑는 속도가 느려지는 트레이드오프가 존재합니다.
핵심 아이디어는 1부터 모든 가중치의 합까지 범위에서 난수를 하나 뽑고, 그 값보다 작거나 같은 가중치를 찾을 때까지 반복문을 돌며 탐색하는 것입니다.
코드 예제입니다:
def random_weighted(weighted)
max = sum_of_weights(weighted)
target = rand(1..max)
weighted.each do |item, weight|
return item if target <= weight
target -= weight
end
end
def sum_of_weights(weighted)
weighted.inject(0) { |sum, (item, weight)| sum + weight }
end
이 코드는 키가 항목(item), 값이 가중치(weight)인 해시(hash)를 입력으로 받습니다. 다음과 같이 호출할 수 있습니다:
random_weighted(cats: 5, dogs: 1) # :cats
이 코드가 의도대로 동작하는지 확인하려면 여러 번 실행한 후 결과 분포를 살펴보면 됩니다.
테스트 예제입니다:
counts = Hash.new(0)
def pick_number
random_weighted(cats: 2, dogs: 1)
end
1000.times { counts[pick_number] += 1 }
p counts
이 코드를 몇 번 실행해 보고 출력 결과에서 비율이 기대한 대로 나오는지 확인해 보세요.
마무리
더 정교한 알고리즘들도 존재하지만, 위에서 소개한 두 가지 방법만으로도 대부분의 상황에서 충분히 유용하게 활용할 수 있습니다. 이 글이 도움이 되었다면 주변 지인에게 공유해 주세요. 더 좋은 글을 계속 써나갈 수 있는 큰 힘이 됩니다!