Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

가중 난수(Weighted Random Number)를 생성하는 방법

일반적인 난수는 '균등 분포(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

이 코드를 몇 번 실행해 보고 출력 결과에서 비율이 기대한 대로 나오는지 확인해 보세요.

마무리

더 정교한 알고리즘들도 존재하지만, 위에서 소개한 두 가지 방법만으로도 대부분의 상황에서 충분히 유용하게 활용할 수 있습니다. 이 글이 도움이 되었다면 주변 지인에게 공유해 주세요. 더 좋은 글을 계속 써나갈 수 있는 큰 힘이 됩니다!