Mam skrzynkę z łupami, którą chcę wypełnić losowym przedmiotem. Ale chcę, aby każdy przedmiot miał inną szansę na wybranie. Na przykład:
- 5% szansy na 10 sztuk złota
- 20% szansy na miecz
- 45% szansy na tarczę
- 20% szansy na zbroję
- 10% szansy na miksturę
Jak mogę to zrobić, aby wybrać dokładnie jeden z powyższych przedmiotów, gdzie te procenty są odpowiednimi szansami na zdobycie łupów?
Odpowiedzi:
Miękko zakodowane prawdopodobieństwo
Wadliwe rozwiązanie prawdopodobieństwa ma tę wadę, że trzeba ustawić prawdopodobieństwa w kodzie. Nie można ich ustalić w czasie wykonywania. Jest również trudny do utrzymania.
Oto dynamiczna wersja tego samego algorytmu.
Oto przykładowa implementacja w Javie w postaci klasy szablonów, którą można utworzyć dla dowolnego obiektu używanego przez grę. Następnie możesz dodać obiekty za pomocą metody
.addEntry(object, relativeWeight)
i wybrać jeden z wpisów, które dodałeś wcześniej.get()
Stosowanie:
Oto ta sama klasa zaimplementowana w C # dla twojego projektu Unity, XNA lub MonoGame:
A oto jeden w JavaScript :
Zawodowiec:
Contra:
O(n)
złożoność środowiska wykonawczego). Więc kiedy masz bardzo duży zestaw przedmiotów i często rysujesz, może stać się powolny. Prosta optymalizacja polega na umieszczeniu najbardziej prawdopodobnych pozycji na pierwszym miejscu, aby algorytm zakończył się w większości przypadków wcześniej. Bardziej złożoną optymalizacją, jaką możesz zrobić, jest wykorzystanie faktu, że tablica jest posortowana i przeszukiwanie bisekcji. To zajmuje tylkoO(log n)
czas.O(n)
najgorszy czas działania)źródło
Uwaga: Stworzyłem bibliotekę C # dla tego konkretnego problemu
Inne rozwiązania są w porządku, jeśli masz tylko niewielką liczbę przedmiotów, a twoje prawdopodobieństwo nigdy się nie zmienia. Jednak przy wielu przedmiotach lub zmieniających się prawdopodobieństwach (np. Usuwaniu przedmiotów po ich wybraniu) będziesz potrzebować czegoś mocniejszego.
Oto dwa najczęstsze rozwiązania (oba są zawarte w powyższej bibliotece)
Alias Walkera
Sprytne rozwiązanie, które jest niezwykle szybkie (
O(1)
!), Jeśli prawdopodobieństwo jest stałe. Zasadniczo algorytm tworzy tarczę 2D („tabelę aliasów”) na podstawie twoich prawdopodobieństw i rzuca w nią strzałką.Istnieje wiele artykułów online o tym, jak to działa, jeśli chcesz dowiedzieć się więcej.
Jedynym problemem jest to, że jeśli zmienią się twoje prawdopodobieństwa, musisz ponownie wygenerować tabelę aliasów, co jest powolne. Dlatego jeśli musisz usunąć przedmioty po ich wybraniu, nie jest to dla ciebie rozwiązanie.
Rozwiązanie oparte na drzewach
Innym powszechnym rozwiązaniem jest utworzenie tablicy, w której każdy element przechowuje sumę swojego prawdopodobieństwa i wszystkie elementy przed nim. Następnie po prostu wygeneruj liczbę losową z [0,1) i przeprowadź binarne wyszukiwanie, gdzie ta liczba wyląduje na liście.
To rozwiązanie jest bardzo łatwe do kodowania / zrozumienia, ale dokonanie wyboru jest wolniejsze niż metoda aliasowa Walkera, a zmiana prawdopodobieństwa jest nadal
O(n)
. Możemy to poprawić, zmieniając tablicę w drzewo wyszukiwania binarnego, w którym każdy węzeł śledzi sumę prawdopodobieństwa wszystkich elementów w swoim poddrzewie. Następnie, gdy generujemy liczbę z [0,1), możemy po prostu zejść po drzewie, aby znaleźć element, który reprezentuje.To pozwala nam
O(log n)
wybrać przedmiot i zmienić prawdopodobieństwo! To sprawia, żeNextWithRemoval()
bardzo szybko!Wyniki
Oto kilka szybkich testów porównawczych z powyższej biblioteki, porównujących te dwa podejścia
Jak widać, w specjalnym przypadku prawdopodobieństw statycznych (niezmiennych) metoda Alias Walkera jest o około 50-100% szybsza. Ale w bardziej dynamicznych przypadkach drzewo jest o kilka rzędów wielkości szybsze !
źródło
nlog(n)
) podczas sortowania elementów według wagi.Rozwiązanie Wheel of Fortune
Możesz użyć tej metody, gdy prawdopodobieństwo w twojej puli przedmiotów ma dość duży wspólny mianownik i musisz z niego czerpać bardzo często.
Utwórz tablicę opcji. Ale wkładaj do niego każdy element wiele razy, z liczbą duplikatów każdego elementu proporcjonalną do jego szansy pojawienia się. W powyższym przykładzie wszystkie elementy mają prawdopodobieństwa, które są mnożnikami 5%, więc możesz utworzyć tablicę 20 elementów takich jak to:
Następnie po prostu wybierz losowy element z tej listy, generując jedną losową liczbę całkowitą między 0 a długością tablicy - 1.
Niedogodności:
Zalety:
źródło
Epic Scepter of the Apocalypse
. Takie dwupoziomowe podejście wykorzystuje zalety obu podejść.[('gold', 1),('sword',4),...]
sumuję wszystkie wagi, a następnie rzucam losową liczbę od 0 do sumy, a następnie iteruje tablicę i obliczam, gdzie wyląduje liczba losowa (tj. areduce
). Działa dobrze w przypadku tablic, które są często aktualizowane, i nie ma większego błędu pamięci.Rozwiązanie prawdopodobieństw zakodowanych na stałe
Najprostszym sposobem znalezienia losowego elementu z ważonej kolekcji jest przejście w dół łańcucha instrukcji if-else, w którym każde if-else zwiększa się prawdopodobnie, ponieważ poprzednia nie trafiła.
Powodem, dla którego warunki warunkowe są równe jego szansie plus wszystkie poprzednie szanse warunkowe, jest to, że poprzednie warunki warunkowe już wyeliminowały możliwość bycia tymi przedmiotami. Zatem dla warunku tarczy
else if(rand <= 70)
70 jest równe 45% szansie na tarczę, plus 5% szansie na złoto i 20% szansie na miecz.Zalety:
Niedogodności:
źródło
W języku C # można użyć skanu Linq, aby uruchomić akumulator, aby sprawdzić liczbę losową z zakresu od 0 do 100,0 f oraz First (), aby uzyskać. Tak jak jeden wiersz kodu.
Więc coś takiego:
sum
jest liczbą całkowitą zainicjowaną przez zero ia
jest listą struktur prob / item / krotek / instancji.rand
to wcześniej wygenerowana liczba losowa w zakresie.To po prostu gromadzi sumę na liście zakresów, dopóki nie przekroczy poprzednio wybranej liczby losowej, i zwraca albo element, albo null, przy czym wartość null byłaby zwracana, jeśli zakres liczb losowych (np. 100) jest przez pomyłkę całkowity zakres ważenia , a wybrana liczba losowa jest poza całkowitym zakresem ważenia.
Zauważysz jednak, że ciężary w OP ściśle pasują do rozkładu normalnego (Krzywa Bell). Myślę, że generalnie nie będziesz chciał określonych zakresów, będziesz raczej chciał rozkładu, który zwęża się albo wokół krzywej dzwonowej, albo tylko na malejącej krzywej wykładniczej (na przykład). W takim przypadku możesz po prostu użyć wzoru matematycznego do wygenerowania indeksu w tablicy elementów, posortowanych według preferowanego prawdopodobieństwa. Dobrym przykładem jest CDF w normalnej dystrybucji
Również przykład tutaj .
Innym przykładem jest to, że możesz wziąć losową wartość od 90 stopni do 180 stopni, aby uzyskać prawą dolną ćwiartkę koła, weź składnik x za pomocą cos (r) i użyj go, aby zindeksować listę priorytetową.
Przy różnych formułach można zastosować ogólne podejście, w którym wystarczy wprowadzić listę o dowolnej długości (np. N) i odwzorować wynik formuły (np. Cos (x) wynosi od 0 do 1) przez pomnożenie (np .: Ncos (x ) = Od 0 do N), aby uzyskać indeks.
źródło
Prawdopodobieństwa nie muszą być zakodowane na stałe. Elementy i progi mogą być razem w tablicy.
Trzeba nadal gromadzić progi, ale można to zrobić, tworząc plik parametrów zamiast go kodować.
źródło
random()
z pętlą?Zrobiłem tę funkcję: https://github.com/thewheelmaker/GDscript_Weighted_Random Now! w twoim przypadku możesz użyć tego w następujący sposób:
Daje tylko liczbę od 0 do 4, ale możesz umieścić ją w tablicy, w której dostałeś przedmioty.
Lub w funkcji:
Oto kod. Zrobiłem to na GDscript, możesz, ale może zmienić inny język, sprawdź także błędy logiczne:
Działa to tak: on_normal_case ([50,50], 0) Daje to 0 lub 1, ma to samo prawdopodobieństwo obu.
on_normal_case ([50,50], 1) Daje to 1 lub 2, ma to samo prawdopodobieństwo obu.
on_normal_case ([20,80], 1) Daje to 1 lub 2, ma większą zmianę, aby uzyskać dwa.
on_normal_case ([20,80,20,20,30], 1) Daje to losowe liczby w zakresie 1-5, a większe liczby są bardziej prawdopodobne niż mniejsze liczby.
on_normal_case ([20,80,0,0,20,20,20,,0,0,0,0,0,33], 45) Ten rzut kostką między liczbami 45,46,49,50,51,56 widać, kiedy tam wynosi zero, nigdy się nie zdarza.
Tak więc funkcja zwraca tylko jedną liczbę losową, która zależy od długości tej tablicy tablicowej i liczby transformmu, a liczby całkowite w tablicy są wagami prawdopodobieństwa, które mogłaby wystąpić liczba, gdzie liczba ta jest położeniem na tablicy, pluss liczba transformmu.
źródło