무작위 아이템으로 채우고 싶은 전리품 상자가 있습니다. 그러나 각 항목마다 다른 선택 기회를 갖기를 원합니다. 예를 들면 다음과 같습니다.
- 10 골드 확률 5 %
- 칼 확률 20 %
- 방패 확률 45 %
- 갑옷 확률 20 %
- 물약 10 % 확률
위의 항목 중 정확히 하나를 선택하여 해당 비율이 전리품을 얻을 수있는 기회가되는 방법은 무엇입니까?
무작위 아이템으로 채우고 싶은 전리품 상자가 있습니다. 그러나 각 항목마다 다른 선택 기회를 갖기를 원합니다. 예를 들면 다음과 같습니다.
위의 항목 중 정확히 하나를 선택하여 해당 비율이 전리품을 얻을 수있는 기회가되는 방법은 무엇입니까?
답변:
하드 코드 된 확률 솔루션에는 코드에서 확률을 설정해야한다는 단점이 있습니다. 런타임에는 확인할 수 없습니다. 유지하기도 어렵다.
다음은 동일한 알고리즘의 동적 버전입니다.
다음은 게임에서 사용하는 모든 객체에 대해 인스턴스화 할 수있는 템플릿 클래스 형태 로 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;
}
}
찬성:
대조 :
O(n)
런타임 복잡성). 따라서 매우 큰 아이템 세트를 가지고 매우 자주 그리면 느려질 수 있습니다. 간단한 최적화는 가장 가능성이 높은 항목을 먼저 배치하여 알고리즘이 대부분의 경우 일찍 종료되도록하는 것입니다. 더 복잡한 최적화는 배열이 정렬되어 이분법 검색을 수행한다는 사실을 이용하는 것입니다. O(log n)
시간 이 걸립니다 . O(n)
최악의 경우 런타임이 있습니다)참고 : 이 정확한 문제에 대한 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)
)을 제공합니다.
항목 풀의 확률에 공통 분모가 많고 자주 추출해야 할 때이 방법을 사용할 수 있습니다.
옵션 배열을 작성하십시오. 그러나 각 요소의 중복 횟수는 나타나는 확률에 비례하여 각 요소를 여러 번 넣습니다. 위의 예에서 모든 요소에는 5 %의 승수 인 확률이 있으므로 다음과 같이 20 개의 요소 배열을 만들 수 있습니다.
10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion
그런 다음 0과 배열 길이-1 사이의 임의의 정수를 생성하여 해당 목록의 임의 요소를 선택하십시오.
단점 :
장점 :
Epic Scepter of the Apocalypse
. 이러한 2 계층 접근 방식은 두 접근 방식의 장점을 모두 활용합니다.
[('gold', 1),('sword',4),...]
하고 모든 가중치를 합한 다음 임의의 숫자를 0에서 합계로 롤링 한 다음 배열을 반복하고 임의의 숫자가 도달하는 위치를 계산합니다 (예 : reduce
). 자주 업데이트되고 주요 메모리 호그가없는 어레이에 적합합니다.
가중 모음에서 임의 항목을 찾는 가장 간단한 방법은 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 % 확률과 같습니다.
장점 :
단점 :
C #에서는 Linq 스캔을 사용하여 누산기를 실행하여 0에서 100.0f 범위의 난수와 .First ()를 확인할 수 있습니다. 한 줄의 코드처럼.
그래서 같은 :
var item = a.Select(x =>
{
sum += x.prob;
if (rand < sum)
return x.item;
else
return null;
}).FirstOrDefault());
sum
0으로 초기화 된 정수이며 a
prob / item 구조체 / 튜플 / 인스턴스 목록입니다. rand
범위에서 이전에 생성 된 난수입니다.
이것은 단순히 이전에 선택한 난수를 초과 할 때까지 범위 목록에 걸쳐 합계를 누적하고 항목 또는 null을 반환합니다. 여기서 난수 범위 (예 : 100)가 실수로 총 가중치 범위보다 작 으면 null이 반환됩니다. 선택한 난수가 총 가중치 범위를 벗어납니다.
그러나 OP의 가중치는 정규 분포 (벨 곡선)와 거의 일치합니다. 나는 일반적으로 특정 범위를 원하지 않을 것이라고 생각합니다. 종 곡선 주위 또는 감소하는 지수 곡선 (예 :) 주위에서 점점 가늘어지는 분포를 원할 것입니다. 이 경우 수학 공식을 사용하여 선호하는 순서대로 정렬 된 항목 배열로 색인을 생성 할 수 있습니다. 정규 분포의 CDF가 좋은 예입니다.
또 다른 예는 90도에서 180도 사이의 임의의 값을 취하여 원의 오른쪽 아래 사분면을 구하고 cos (r)를 사용하여 x 성분을 취하여이를 우선 순위가 지정된 목록으로 색인화 할 수 있다는 것입니다.
다른 수식을 사용하면 길이 (예 : N)의 우선 순위가 지정된 목록을 입력하고 수식의 결과 (예 : cos (x)는 0 대 1)를 곱하여 (예 : Ncos (x) ) = 0 ~ N) 인덱스를 가져옵니다.
확률은 하드 코딩 될 필요가 없다. 항목과 임계 값은 배열로 함께있을 수 있습니다.
for X in items’range loop
If items (X).threshold < random() then
Announce (items(X).name)
Exit loop
End if
End loop
여전히 임계 값을 누적해야하지만 코딩하는 대신 매개 변수 파일을 작성할 때 수행 할 수 있습니다.
random()
루프에서 호출을 의미 합니까?
나는이 기능을 수행했다 : 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는 숫자가 발생할 수있는 확률 가중치입니다.이 숫자는 배열의 위치에 변환 숫자를 더한 값입니다.