가중 모음을 만든 다음 임의 요소를 선택하는 방법은 무엇입니까?


34

무작위 아이템으로 채우고 싶은 전리품 상자가 있습니다. 그러나 각 항목마다 다른 선택 기회를 갖기를 원합니다. 예를 들면 다음과 같습니다.

  • 10 골드 확률 5 %
  • 칼 확률 20 %
  • 방패 확률 45 %
  • 갑옷 확률 20 %
  • 물약 10 % 확률

위의 항목 중 정확히 하나를 선택하여 해당 비율이 전리품을 얻을 수있는 기회가되는 방법은 무엇입니까?


1
참고로 이론적으로 샘플 당 O (1) 시간은 유한 분포, 항목이 동적으로 변하는 분포까지 가능합니다. 예를 들어 cstheory.stackexchange.com/questions/37648/…을 참조하십시오 .
닐 영

답변:


37

소프트 코딩 된 확률 솔루션

하드 코드 된 확률 솔루션에는 코드에서 확률을 설정해야한다는 단점이 있습니다. 런타임에는 확인할 수 없습니다. 유지하기도 어렵다.

다음은 동일한 알고리즘의 동적 버전입니다.

  1. 실제 항목 쌍과 각 항목의 가중치 배열을 만듭니다.
  2. 항목을 추가 할 때 항목의 가중치는 자체 가중치 배열에 이미있는 모든 항목의 가중치의 합이되어야합니다. 따라서 합계를 별도로 추적해야합니다. 특히 다음 단계에서 필요하기 때문입니다.
  3. 객체를 검색하려면 0과 모든 항목의 가중치 합계 사이의 난수를 생성하십시오.
  4. 난수보다 크거나 같은 가중치를 가진 항목을 찾을 때까지 배열을 처음부터 끝까지 반복하십시오.

다음은 게임에서 사용하는 모든 객체에 대해 인스턴스화 할 수있는 템플릿 클래스 형태 로 Java로 작성된 샘플 구현입니다 . 그런 다음 메소드를 사용하여 객체 .addEntry(object, relativeWeight)를 추가하고 이전에 추가 한 항목 중 하나를 선택할 수 있습니다.get()

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightedRandomBag<T extends Object> {

    private class Entry {
        double accumulatedWeight;
        T object;
    }

    private List<Entry> entries = new ArrayList<>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void addEntry(T object, double weight) {
        accumulatedWeight += weight;
        Entry e = new Entry();
        e.object = object;
        e.accumulatedWeight = accumulatedWeight;
        entries.add(e);
    }

    public T getRandom() {
        double r = rand.nextDouble() * accumulatedWeight;

        for (Entry entry: entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.object;
            }
        }
        return null; //should only happen when there are no entries
    }
}

용법:

WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();

// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold",  5.0);
itemDrops.addEntry("Sword",   20.0);
itemDrops.addEntry("Shield",  45.0);
itemDrops.addEntry("Armor",   20.0);
itemDrops.addEntry("Potion",  10.0);

// drawing random entries from it
for (int i = 0; i < 20; i++) {
    System.out.println(itemDrops.getRandom());
}

Unity, XNA 또는 MonoGame 프로젝트를 위해 C #으로 구현 된 동일한 클래스는 다음과 같습니다 .

using System;
using System.Collections.Generic;

class WeightedRandomBag<T>  {

    private struct Entry {
        public double accumulatedWeight;
        public T item;
    }

    private List<Entry> entries = new List<Entry>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void AddEntry(T item, double weight) {
        accumulatedWeight += weight;
        entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
    }

    public T GetRandom() {
        double r = rand.NextDouble() * accumulatedWeight;

        foreach (Entry entry in entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.item;
            }
        }
        return default(T); //should only happen when there are no entries
    }
}

그리고 여기 JavaScript가 있습니다 :

var WeightedRandomBag = function() {

    var entries = [];
    var accumulatedWeight = 0.0;

    this.addEntry = function(object, weight) {
        accumulatedWeight += weight;
        entries.push( { object: object, accumulatedWeight: accumulatedWeight });
    }

    this.getRandom = function() {
        var r = Math.random() * accumulatedWeight;
        return entries.find(function(entry) {
            return entry.accumulatedWeight >= r;
        }).object;
    }   
}

찬성:

  • 모든 무게 비율을 처리 할 수 ​​있습니다. 원하는 경우 세트에서 천문학적으로 확률이 작은 항목을 가질 수 있습니다. 가중치는 100을 더할 필요도 없습니다.
  • 런타임에 아이템과 무게를 읽을 수 있습니다
  • 배열의 항목 수에 비례하는 메모리 사용량

대조 :

  • 제대로 작동하려면 더 많은 프로그래밍이 필요합니다
  • 최악의 경우 전체 배열을 반복해야합니다 ( O(n)런타임 복잡성). 따라서 매우 큰 아이템 세트를 가지고 매우 자주 그리면 느려질 수 있습니다. 간단한 최적화는 가장 가능성이 높은 항목을 먼저 배치하여 알고리즘이 대부분의 경우 일찍 종료되도록하는 것입니다. 더 복잡한 최적화는 배열이 정렬되어 이분법 검색을 수행한다는 사실을 이용하는 것입니다. O(log n)시간 이 걸립니다 .
  • 사용하기 전에 메모리에 목록을 작성해야합니다 (런타임에 항목을 쉽게 추가 할 수 있지만 항목 제거도 추가 할 수 있지만 제거 된 항목 뒤에 오는 모든 항목의 누적 가중치를 업데이트해야합니다. 다시 O(n)최악의 경우 런타임이 있습니다)

2
C # 코드는 LINQ : return entries.FirstOrDefault (e => e.accumulatedWeight> = r)를 사용하여 작성할 수 있습니다. 더 중요한 것은, 부동 소수점 정밀도 손실로 인해 임의의 값이 누적 된 값보다 조금 더 큰 경우이 알고리즘은 null을 반환 할 가능성이 약간 있습니다. 예방책으로 마지막 요소에 작은 값 (예 : 1.0)을 추가 할 수 있지만 코드에서 목록이 최종임을 명시 적으로 명시해야합니다.
IMil

1
이것에 대한 하나의 작은 변형은 개인적으로 사용했습니다. 런타임의 가중치 값을 이전의 가중치 플러스 값으로 변경하지 않으려면 전달 된 각 항목의 가중치를 임의의 값에서 빼서 임의의 값이 현재 항목 무게보다 작습니다 (또는 무게를 빼면 값이 <0이 됨)
Lunin

2
@ BlueRaja-DannyPflughoeft 조기 최적화 ... 문제는 열린 전리품 상자에서 개체를 선택하는 것이 었습니다. 누가 초당 1000 개의 상자를 열 예정입니까?
IMil

4
@IMil : 아니요. 질문 은 임의 가중치 항목을 선택하는 일반적인 문제 입니다 . 전리품 상자의 경우,이 답변은 적은 수의 항목이 있고 확률이 변경되지 않기 때문에 아마도 적합합니다 .
BlueRaja-대니 Pflughoeft

4
그런 다음 @opa는 플래그를 속임수로 닫습니다. 이전에 질문을 받았기 때문에 좋은 답변을 공표하는 것이 실제로 잘못입니까?
Baldrickk

27

참고 : 이 정확한 문제에 대한 C # 라이브러리를 만들었습니다.

적은 수의 항목 만 있고 확률이 변경되지 않는 경우 다른 솔루션은 좋습니다. 그러나 항목이 많거나 확률이 변경되면 (예 : 항목을 선택한 후 항목 제거) 보다 강력한 기능이 필요합니다.

다음은 가장 일반적인 두 가지 솔루션입니다 (둘 다 위 라이브러리에 포함되어 있음).

워커의 별명 방법

확률이 일정하면 매우 빠른 영리한 솔루션 ( O(1)!) 입니다. 본질적으로, 알고리즘은 확률로 2D 다트 판 ( "별칭 테이블")을 생성하고 다트를 던집니다.

다트 판

자세한 내용은 온라인에서 작동하는 방법에 대한 기사많이 있습니다.

유일한 문제는 확률이 변경되면 별칭 테이블을 재생성해야한다는 것입니다. 따라서 상품을 선택한 후 제거해야하는 경우에는 해결책이 아닙니다.

트리 기반 솔루션

다른 일반적인 해결책은 각 항목이 확률과 그 이전의 모든 항목의 합계를 저장하는 배열을 만드는 것입니다. 그런 다음 [0,1)에서 난수를 생성하고 해당 숫자가 목록에서 어디에 있는지 이진 검색을 수행하십시오.

이 솔루션은 코딩 / 이해하기가 매우 쉽지만 Walker의 Alias ​​Method보다 선택 속도가 느리고 확률을 변경하는 것은 여전히 ​​중요 O(n)합니다. 배열을 이진 검색 트리로 전환하여 각 노드가 하위 트리의 모든 항목에서 확률 합계를 추적함으로써 배열을 개선 할 수 있습니다. 그런 다음 [0,1)에서 숫자를 생성하면 트리를 내려 가서 나타내는 항목을 찾을 수 있습니다.

이것은 우리가 제공 O(log n)항목을 선택하는 확률을 변경! 이것은 NextWithRemoval()매우 빠릅니다!

결과

위의 라이브러리에서 두 가지 접근 방식을 비교 한 빠른 벤치 마크가 있습니다.

         WeightedRandomizer 벤치 마크 | 나무 | 표
-------------------------------------------------- ---------------------------------
Add () x10000 + NextWithReplacement () x10 : | 4ms | 2ms
Add () x10000 + NextWithReplacement () x10000 : | 7ms | 4ms
Add () x10000 + NextWithReplacement () x100000 : | 35ms | 28ms
(Add () + NextWithReplacement ()) x10000 (인터리브 된) | 8ms | 5403ms
Add () x10000 + NextWithRemoval () x10000 : | 10ms | 5948ms

보시다시피 정적 (비 변경) 확률의 특수한 경우 Walker 's Alias ​​방법은 약 50-100 % 빠릅니다. 그러나보다 역동적 인 경우에는 트리가 몇 배 더 빠릅니다 !


트리 기반 솔루션은 또한 항목별로 중량을 정렬 할 때 적절한 런타임 ( nlog(n))을 제공합니다.
Nathan Merrill

2
나는 당신의 결과에 회의적이지만, 이것이 정답입니다. 이것이 실제로이 문제를 처리하는 정식 방법 임을 고려할 때 이것이 왜 최고의 대답이 아닌지 확실하지 않습니다 .
whn

어떤 파일에 트리 기반 솔루션이 포함되어 있습니까? 둘째, 벤치 마크 테이블 : Walker 's Alias가 "테이블"열입니까?
Yakk

1
@ Yakk : 트리 기반 솔루션의 코드는 여기에 있습니다 . AA-tree오픈 소스 구현 을 기반으로합니다 . 그리고 두 번째 질문에 '예'입니다.
BlueRaja-대니 Pflughoeft

1
Walker 부분은 링크 전용입니다.
누적

17

휠 오브 포춘 솔루션

항목 풀의 확률에 공통 분모가 많고 자주 추출해야 할 때이 방법을 사용할 수 있습니다.

옵션 배열을 작성하십시오. 그러나 각 요소의 중복 횟수는 나타나는 확률에 비례하여 각 요소를 여러 번 넣습니다. 위의 예에서 모든 요소에는 5 %의 승수 인 확률이 있으므로 다음과 같이 20 개의 요소 배열을 만들 수 있습니다.

10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion

그런 다음 0과 배열 길이-1 사이의 임의의 정수를 생성하여 해당 목록의 임의 요소를 선택하십시오.

단점 :

  • 항목을 처음 생성 할 때 배열을 작성해야합니다.
  • 요소 중 하나의 확률이 매우 낮을 경우 실제로 큰 배열이 생겨 많은 메모리가 필요할 수 있습니다.

장점 :

  • 이미 배열을 가지고 있고 여러 번 배열을 그리려면 매우 빠릅니다. 하나의 임의의 정수와 하나의 배열 액세스.

2
두 번째 단점을 피하기위한 하이브리드 솔루션으로 마지막 슬롯을 "기타"로 지정하고 Philipp의 어레이 접근 방식과 같은 다른 수단을 통해 처리 할 수 ​​있습니다. 따라서 마지막 슬롯에 99.9 % 확률로 약 0.1 % 확률로 확률을 제공하는 배열로 채울 수 있습니다 Epic Scepter of the Apocalypse. 이러한 2 계층 접근 방식은 두 접근 방식의 장점을 모두 활용합니다.
Cort Ammon

1
나는 내 자신의 프로젝트에서 약간의 변형을 사용합니다. 내가하는 일은 각 항목 및 가중치를 계산하고 배열에 저장 [('gold', 1),('sword',4),...]하고 모든 가중치를 합한 다음 임의의 숫자를 0에서 합계로 롤링 한 다음 배열을 반복하고 임의의 숫자가 도달하는 위치를 계산합니다 (예 : reduce). 자주 업데이트되고 주요 메모리 호그가없는 어레이에 적합합니다.

1
@Thebluefish이 솔루션은 다른 답변 "Soft-coded Probabilities Solution"
Philipp

7

하드 코딩 된 확률 솔루션

가중 모음에서 임의 항목을 찾는 가장 간단한 방법은 if-else 문 체인을 순회하는 것입니다. 여기에서 이전 if가 적중 할 때마다 if-else가 증가 할 것입니다.

int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
    print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
    print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
    print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
    print("You found armor!");
}
else //10% chance
{
    print("You found a potion!");
}

조건부 확률과 이전 조건부 확률 모두가 동일한 이유는 이전 조건부에서 이미 해당 항목이 될 가능성을 제거했기 때문입니다. 따라서 방패의 조건부 else if(rand <= 70)에서 70은 방패의 45 % 확률과 금의 5 % 확률, 검의 20 % 확률과 같습니다.

장점 :

  • 데이터 구조가 필요 없으므로 프로그래밍이 쉽습니다.

단점 :

  • 코드에서 드롭률을 유지해야하기 때문에 유지 관리가 어렵습니다. 런타임에는 확인할 수 없습니다. 따라서 미래의 증거를 원한다면 다른 답변을 확인해야합니다.

3
이것은 유지하기가 정말 성 가실 것입니다. 예를 들어, 금을 제거하고 물약이 그 자리에 오게하려면, 그들 사이의 모든 아이템의 확률을 조정해야합니다.
Alexander

1
@Alexander가 언급 한 문제를 피하기 위해 각 조건에 추가하는 대신 각 단계에서 현재 속도를 뺄 수 있습니다.
AlexanderJ93

2

C #에서는 Linq 스캔을 사용하여 누산기를 실행하여 0에서 100.0f 범위의 난수와 .First ()를 확인할 수 있습니다. 한 줄의 코드처럼.

그래서 같은 :

var item = a.Select(x =>
{
    sum += x.prob;
    if (rand < sum)
        return x.item;
    else
        return null;
 }).FirstOrDefault());

sum0으로 초기화 된 정수이며 aprob / item 구조체 / 튜플 / 인스턴스 목록입니다. rand범위에서 이전에 생성 된 난수입니다.

이것은 단순히 이전에 선택한 난수를 초과 할 때까지 범위 목록에 걸쳐 합계를 누적하고 항목 또는 null을 반환합니다. 여기서 난수 범위 (예 : 100)가 실수로 총 가중치 범위보다 작 으면 null이 반환됩니다. 선택한 난수가 총 가중치 범위를 벗어납니다.

그러나 OP의 가중치는 정규 분포 (벨 곡선)와 거의 일치합니다. 나는 일반적으로 특정 범위를 원하지 않을 것이라고 생각합니다. 종 곡선 주위 또는 감소하는 지수 곡선 (예 :) 주위에서 점점 가늘어지는 분포를 원할 것입니다. 이 경우 수학 공식을 사용하여 선호하는 순서대로 정렬 된 항목 배열로 색인을 생성 할 수 있습니다. 정규 분포의 CDF가 좋은 예입니다.

또한 여기 에 예가 있습니다 .

또 다른 예는 90도에서 180도 사이의 임의의 값을 취하여 원의 오른쪽 아래 사분면을 구하고 cos (r)를 사용하여 x 성분을 취하여이를 우선 순위가 지정된 목록으로 색인화 할 수 있다는 것입니다.

다른 수식을 사용하면 길이 (예 : N)의 우선 순위가 지정된 목록을 입력하고 수식의 결과 (예 : cos (x)는 0 대 1)를 곱하여 (예 : Ncos (x) ) = 0 ~ N) 인덱스를 가져옵니다.


3
한 줄이면이 ​​줄을 우리에게 줄 수 있습니까? 나는 C #에 익숙하지 않아서 당신이 무엇을 의미하는지 모르겠습니다.
HEGX64

@ HEGX64가 추가되었지만 모바일 및 편집기 사용이 작동하지 않습니다. 편집 할 수 있습니까?
센티넬

4
특정 언어로 된 특정 구현이 아니라 그 개념을 설명하기 위해이 답변을 변경할 수 있습니까?
Raimund Krämer

@ RaimundKrämer Erm, 다 했습니까?
Sentinel

설명없이 공감 : 쓸모없고 반사회적.
WGroleau

1

확률은 하드 코딩 될 필요가 없다. 항목과 임계 값은 배열로 함께있을 수 있습니다.

for X in itemsrange loop
  If items (X).threshold < random() then
     Announce (items(X).name)
     Exit loop
  End if
End loop

여전히 임계 값을 누적해야하지만 코딩하는 대신 매개 변수 파일을 작성할 때 수행 할 수 있습니다.


3
올바른 임계 값을 계산하는 방법에 대해 자세히 설명해 주시겠습니까? 예를 들어, 각각 확률이 33 % 인 3 개의 아이템이 있다면이 테이블을 어떻게 구축할까요? 매번 새로운 random ()이 생성되므로 첫 번째에는 0.3333이 필요하고 두 번째에는 0.5가 필요하고 마지막에는 1.0이 필요합니다. 아니면 알고리즘을 잘못 읽었습니까?
파이프

당신은 그것을 다른 사람들이 그들의 답변에서했던 방식으로 계산합니다. X 항목의 확률이 같으면 첫 번째 임계 값은 1 / X, 두 번째, 2 / X 등입니다.
WGroleau

이 알고리즘에서 3 개의 항목에 대해이를 수행하면 임계 값은 1/3, 2/3 및 3/3이되지만 결과 확률은 첫 번째, 두 번째 및 세 번째 항목에 대해 1/3, 4/9 및 2/9가됩니다. 당신은 정말로 random()루프에서 호출을 의미 합니까?
파이프

아니, 그건 분명히 버그입니다. 각 검사에는 동일한 난수가 필요합니다.
WGroleau

0

나는이 기능을 수행했다 : https://github.com/thewheelmaker/GDscript_Weighted_Random Now! 귀하의 경우 다음과 같이 사용할 수 있습니다 :

on_normal_case([5,20,45,20,10],0)

0에서 4 사이의 숫자 만 제공하지만 항목을 얻은 배열에 넣을 수 있습니다.

item_array[on_normal_case([5,20,45,20,10],0)]

또는 기능 :

item_function(on_normal_case([5,20,45,20,10],0))

코드는 다음과 같습니다. 나는 그것을 GDscript로 만들었지 만, 다른 언어를 바꿀 수도 있고 논리 오류를 확인할 수도 있습니다.

func on_normal_case(arrayy,transformm):
    var random_num=0
    var sum=0
    var summatut=0
    #func sumarrays_inarray(array):
    for i in range(arrayy.size()):
        sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
    random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
    if 0<=random_num and random_num<=arrayy[0]:
        #print(random_num)
        #print(array[0])
        return 0+ transformm
    summatut=summatut+arrayy[0]
    for i in range(arrayy.size()-1):
        #they must pluss together
        #if array[i]<=random_num and random_num<array[i+1]:
        if summatut<random_num and random_num<=summatut+arrayy[i+1]:
            #return i+1+transform
            #print(random_num)
            #print(summatut)
            return i+1+ transformm

        summatut=summatut+arrayy[i+1]
    pass

다음과 같이 작동합니다. on_normal_case ([50,50], 0) 0 또는 1을 제공하며 둘 다 동일한 확률을 갖습니다.

on_normal_case ([50,50], 1) 1 또는 2를 주는데, 둘 다 확률이 같습니다.

on_normal_case ([20,80], 1) 1 또는 2를 제공하며, 2를 얻기 위해 더 큰 변화가 있습니다.

on_normal_case ([20,80,20,20,30], 1) 1-5 범위의 난수를 지정하고 큰 숫자는 작은 숫자보다 가능성이 높습니다.

on_normal_case ([20,80,0,0,20,20,30,0,0,0,0,33], 45)이 던져는 숫자 45,46,49,50,51,56 사이에서 오지 절대 발생하지 않는 0입니다.

따라서이 함수는 해당 배열 배열 및 변환 숫자의 길이에 따라 달라지는 하나의 난수 만 반환하며 배열의 int는 숫자가 발생할 수있는 확률 가중치입니다.이 숫자는 배열의 위치에 변환 숫자를 더한 값입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.