Ağırlıklı bir koleksiyon nasıl oluştururum ve ondan rastgele bir elemanı nasıl seçerim?


34

Rasgele bir eşya ile doldurmak istediğim bir ganimet kutusu var. Ancak her bir öğenin seçilme şansının farklı olmasını istiyorum. Örneğin:

  • 10 altın% 5 şansı
  • % 20 kılıç şansı
  • % 45 kalkan imkanı
  • % 20 zırh şansı
  • % 10 iksir şansı

Bunu, yukarıdakilerden tam olarak birini seçmem için nasıl yapabilirim ki, bu yüzdeler yağma alma şansını arttırıyor?


1
FYI, teoride, herhangi bir sonlu dağıtım için, girişleri dinamik olarak değişen bir dağıtım bile, örnek başına O (1) süre mümkündür. Bakınız örneğin cstheory.stackexchange.com/questions/37648/… .
Neal Young,

Yanıtlar:


37

Yumuşak Kodlu Olasılık Çözümü

Kodlanmış olasılık çözümü, kodunuzdaki olasılıkları belirlemeniz gereken dezavantajlara sahiptir. Çalışma zamanında bunları belirleyemezsiniz. Ayrıca bakımı zordur.

İşte aynı algoritmanın dinamik bir versiyonu.

  1. Her bir öğenin gerçek çiftinin ve ağırlığının bir çiftini oluşturun
  2. Bir öğe eklediğinizde, öğenin ağırlığının kendi ağırlığının yanı sıra dizideki tüm öğelerin ağırlıklarının toplamı olması gerekir. Yani toplamı ayrı ayrı izlemelisiniz. Özellikle bir sonraki adım için ihtiyacınız olacak çünkü.
  3. Bir nesneyi almak için, 0 ile tüm öğelerin ağırlıklarının toplamı arasında rastgele bir sayı oluşturun.
  4. diziyi baştan sona yineleyin, rastgele sayıdan daha büyük veya eşit bir ağırlığa sahip bir giriş buluncaya kadar

İşte Java'da oyununuzun kullandığı herhangi bir nesne için örnekleyebileceğiniz bir şablon sınıfı şeklinde örnek bir uygulama . Daha sonra yöntemle nesne ekleyebilir .addEntry(object, relativeWeight)ve daha önce eklediğiniz girişlerden birini seçebilirsiniz..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
    }
}

Kullanımı:

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());
}

İşte Unity, XNA veya MonoGame projeniz için C # 'da uygulanan sınıf :

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
    }
}

Ve işte JavaScript'te bir tane :

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;
    }   
}

Pro:

  • Herhangi bir ağırlık oranını kaldırabilir. İsterseniz sette astronomik olarak küçük olasılıkları olan eşyalarınız olabilir. Ağırlıkları da 100 kadar eklemek gerekmez.
  • Çalışma zamanındaki öğeleri ve ağırlıkları okuyabilirsiniz
  • Dizideki öğelerin sayısıyla orantılı bellek kullanımı

Contra:

  • Doğru olması için biraz daha programlama gerektirir
  • En kötü durumda, tüm diziyi yinelemeniz gerekebilir ( O(n)çalışma zamanı karmaşıklığı). Bu nedenle, çok büyük bir öğe grubunuz olduğunda ve çok sık çizim yaptığınızda, yavaşlayabilir. Basit bir optimizasyon en muhtemel maddeleri ilk sıraya koymaktır, böylece algoritma çoğu durumda erken sona erer. Yapabileceğiniz daha karmaşık bir optimizasyon, dizinin sıralı olduğu gerçeğinden yararlanmak ve bir bisection araştırması yapmaktır. Bu sadece O(log n)zaman alır .
  • Listeyi kullanmadan önce hafızada oluşturmalısınız (çalışma zamanında kolayca öğe ekleyebilseniz de. Öğeleri kaldırmak da eklenebilir, ancak bu, kaldırılan girişten sonra gelen tüm öğelerin birikmiş ağırlıklarını güncellemenizi gerektirir; Yine O(n)en kötü vaka çalışma zamanına sahip)

2
C # kodu LINQ: return return.FirstOrDefault (e => e.accumulatedWeight> = r) kullanılarak yazılabilir. Daha da önemlisi, rastgele değer birikmiş değerden sadece küçük bir bit büyükse, kayan nokta hassasiyet kaybı nedeniyle bu algoritmanın boşa dönmesi olasılığı çok düşüktür. Bir önlem olarak, son öğeye küçük bir değer (örneğin 1.0) ekleyebilirsin, ancak kodun içinde listenin son olduğunu açıkça belirtmen gerekir.
IMil

1
Bununla ilgili küçük bir değişken, kişisel olarak kullandım, çalışma zamanındaki ağırlık değerlerinin ağırlık-artı-önceki değerine değiştirilmemesini istiyorsanız, geçen her girişin ağırlığını rasgele değerinizden çıkarabilir, rastgele değer geçerli öğelerin ağırlığından daha düşük (veya ağırlığı çıkarırken değeri <0 yapar)
Lunin

2
@ BlueRaja-DannyPflughoeft erken optimizasyon ... soru, açık bir ganimet kutusundan bir nesne seçmekti. Kim saniyede 1000 kutu açacak?
IMil

4
@IMil: Hayır, soru rastgele ağırlıklı öğeleri seçmek için genel bir sorun . Özellikle lootbox'lar için bu cevap muhtemelen iyidir çünkü az sayıda ürün vardır ve olasılıklar değişmez (ancak, genellikle bir sunucuda yapıldığı için, 1000 / sn popüler bir oyun için gerçekçi değildir) .
BlueRaja - Danny Pflughoeft

4
@opa sonra bir dupe olarak kapatmak için bayrak. Sadece soru daha önce sorulduğu için iyi bir cevabı yükseltmek yanlış mıdır?
Baldrickk,

27

Not: Bu kesin problem için bir C # kütüphanesi oluşturdum

Çok az sayıda öğeniz varsa ve olasılıklarınız asla değişmezse, diğer çözümler gayet iyi. Bununla birlikte, birçok öğe veya olasılık değiştirme (örneğin seçtikten sonra öğeleri kaldırma) ile daha güçlü bir şey isteyeceksiniz.

İşte en yaygın iki çözüm (ikisi de yukarıdaki kitaplığa dahil edilmiştir)

Walker Alias ​​Yöntemi

Olasılıklarınız sabitse, son derece hızlı ( O(1)!) Akıllı bir çözüm . Temelde, algoritma olasılıkların dışında bir 2D dart tahtası ("alias tablosu") yaratır ve ona bir dart atar.

Dart tahtası

Orada çevrimiçi makalelerin bol Daha fazlasını öğrenmek istiyorsanız nasıl çalıştığı hakkında.

Tek sorun, olasılıklarınız değişirse, yavaş olan takma ad tablosunu yeniden oluşturmanız gerekmesidir. Dolayısıyla, öğeleri seçildikten sonra çıkarmanız gerekirse, bu sizin için bir çözüm değildir.

Ağaç tabanlı çözüm

Diğer ortak çözüm, her bir öğenin olasılık toplamını ve ondan önceki tüm öğeleri sakladığı bir dizi yapmaktır. Sonra sadece [0,1) 'den rastgele bir sayı oluşturun ve bu sayının listeye girdiği yer için ikili bir arama yapın.

Bu çözümü kodlaması / anlaması çok kolaydır, ancak seçim yapmak Walker's Alias ​​Metodundan daha yavaştır ve olasılıkları değiştirmek hala kolaydır O(n). Diziyi, her düğümün alt ağacındaki tüm öğelerde olasılıkların toplamını takip ettiği bir ikili arama ağacına dönüştürerek geliştirebiliriz. Sonra sayıyı [0,1) 'den ürettiğimizde, temsil ettiği öğeyi bulmak için ağacın üzerinden yürüyebiliriz.

Bu bize O(log n)bir eşya seçmemizi ve olasılıkları değiştirmemizi sağlar! Bu NextWithRemoval()son derece hızlı yapar !

Sonuçlar

İşte bu iki yaklaşımı karşılaştırarak yukarıdaki kütüphaneden bazı hızlı kriterler.

         WeightedRandomizer Benchmarks | Ağaç | tablo
-------------------------------------------------- ---------------------------------
Ekle () x10000 + NextWithReplacement () x10: | 4 ms | 2 ms
Ekle () x10000 + NextWithReplacement () x10000: | 7 ms | 4 ms
Ekle () x10000 + NextWithReplacement () x100000: | 35 ms | 28 ms
(Ekle () + NextWithReplacement ()) x10000 (araya eklenir) | 8 ms | 5403 ms
Ekle () x10000 + NextWithRemoval () x10000: | 10 ms | 5948 ms

Görebileceğiniz gibi, özel statik (değişmeyen) olasılıklar için, Walker's Alias ​​yöntemi yaklaşık% 50-100 daha hızlıdır. Ancak, daha dinamik durumlarda, ağaç birkaç büyüklük sırası daha hızlıdır !


Ağaç tabanlı çözüm, öğeleri ağırlığa göre ayırırken bize iyi bir çalışma süresi ( nlog(n)) veriyor .
Nathan Merrill

2
Sonuçlarından şüpheliyim, ama bu doğru cevap. Bunun neden en iyi cevap olmadığından emin değiliz , aslında bu sorunun üstesinden gelmek için kanonik yol olduğunu düşünüyoruz.
whn

Hangi dosya ağaç tabanlı çözümü içeriyor? İkincisi, kıyaslama tablonuz: Walker's Alias ​​"masa" sütunu mu?
Yakk

1
@ Yakk: Ağaç tabanlı çözümün kodu burada . Bir AA ağacının açık kaynaklı bir uygulaması üzerine kuruludur . Ve ikinci sorunuza 'evet'.
BlueRaja - Danny Pflughoeft

1
Walker kısmı, sadece bağlantı sadece güzel.
Birikim,

17

Fortune çözümü tekerleği

Bu yöntemi, ürün havuzunuzdaki olasılıklar oldukça büyük ortak bir payda sahipse ve ondan sık sık çekmeniz gerektiğinde kullanabilirsiniz.

Bir dizi seçenek oluşturun. Ancak, her bir öğeyi, defalarca gösterme şansıyla orantılı olarak, her bir öğenin yinelenen sayısı ile birlikte koyun. Yukarıdaki örnekte, tüm öğelerin% 5'in çarpanı olan olasılıkları vardır, bu nedenle aşağıdaki gibi 20 öğeden oluşan bir dizi oluşturabilirsiniz:

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

Daha sonra 0 ile dizinin uzunluğu arasında bir rasgele tamsayı oluşturarak listedeki rastgele bir öğeyi seçin.

Dezavantajları:

  • Bir öğe oluşturmak istediğinizde diziyi oluşturmanız gerekir.
  • Elementlerinizden birinin çok düşük bir olasılık olması gerekiyorsa, çok fazla bellek gerektiren çok büyük bir dizi ile bitirdiniz.

Avantajları:

  • Zaten dizi varsa ve ondan birçok kez çizmek istediğinizde, o zaman çok hızlı. Sadece bir rasgele tamsayı ve bir dizi erişim.

2
İkinci dezavantajdan kaçınmak için karma bir çözüm olarak, son slotu "öteki" olarak atayabilir ve Philipp'in dizi yaklaşımı gibi başka yollarla kullanabilirsiniz. Böylece, bu son slotu% 99,9 iksir şansı ve sadece% 0,1 şansı sunan bir diziyle doldurabilirsiniz Epic Scepter of the Apocalypse. Böyle iki katmanlı bir yaklaşım, her iki yaklaşımın avantajlarından yararlanır.
Cort Ammon

1
Kendi projemde bunun bir çeşitlemesini kullanıyorum. Yaptığım, her bir öğenin ağırlığını hesaplamak ve bunları bir dizide saklamak [('gold', 1),('sword',4),...], tüm ağırlıkları toplamak ve ardından 0'dan toplama rastgele bir sayı döndürmek, daha sonra diziyi yinelemek ve rastgele sayının nereye düştüğünü hesaplamak. a reduce). Sık güncellenen diziler için gayet iyi çalışır ve büyük hafıza domuzları yoktur.

1
@Bluefish Bu çözüm benim diğer cevabım "Yumuşak Kodlu Olasılıklar Çözümü"
Philipp

7

Sabit Kodlanmış Olasılık Çözümü

Ağırlıklı bir koleksiyondan rastgele bir öğe bulmanın en basit yolu, önceki ifadenin vurmadığı gibi, her if-else'in muhtemelen arttığı if-else ifadeler zincirinin altından geçmek.

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!");
}

Şartlıların şansına eşit olmasının sebebi, önceki şartsızlık şanslarının hepsinin eşit olması sebebi, önceki şartlıların zaten bu maddeler olma ihtimalini ortadan kaldırmış olmasıdır. Yani kalkanın şartı için else if(rand <= 70)70, kalkanın% 45 şansına, artı altın için% 5 şansına ve kılıcın% 20 şansına eşittir.

Avantajları:

  • Programlanması kolaydır, çünkü hiçbir veri yapısı gerektirmez.

Dezavantajları:

  • Bakımı zor, çünkü düşürme oranlarınızı kodunuzda tutmanız gerekiyor. Çalışma zamanında bunları belirleyemezsiniz. Bu yüzden daha ileriye dönük bir kanıt istiyorsanız, diğer cevapları da kontrol etmelisiniz.

3
Bu sürdürmek gerçekten sinir bozucu olurdu. Örneğin, altını kaldırmak ve iksir yerini almak istiyorsa, aralarındaki tüm öğelerin olasılıklarını ayarlamanız gerekir.
Alexander - Monica,

1
@Alexander'ın bahsettiği sorunu önlemek için, mevcut hızı her bir koşula eklemek yerine, her adımda çıkarabilirsiniz.
AlexanderJ93

2

C # 'da, biriktiricinizi çalıştırmak için 0 ila 100.0f ve .First () aralığında rastgele bir sayı olup olmadığını kontrol etmek için bir Linq taraması kullanabilirsiniz. Yani bir satır kod gibi.

Yani şöyle bir şey:

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

sumsıfır sıfırlanmış bir tamsayıdır ve aprob / item structs / tuples / örnekleri listesidir. randaralıkta önceden üretilmiş rastgele bir sayıdır.

Bu sadece önceden seçilen rastgele sayıyı geçinceye kadar aralıklar listesindeki toplamı biriktirir ve rasgele sayı aralığı (örneğin 100) toplam ağırlık aralığının yanlışlıkla altındaysa, null değerini döndürecek olan öğeyi veya null değerini döndürür. ve seçilen rasgele sayı toplam ağırlık aralığının dışında.

Bununla birlikte, OP'deki ağırlıkların normal bir dağılıma (Bell Curve) yaklaştığını fark edeceksiniz. Genel olarak, belirli aralıklar istemeyeceğinizi, bir çan eğrisi etrafında ya da sadece azalan bir üstel eğri üzerinde süzülen bir dağılım isteme eğiliminde olacağınızı düşünüyorum (örneğin). Bu durumda, tercih edilen olasılık sırasına göre sıralanmış bir dizi öğeye indeks oluşturmak için yalnızca matematiksel bir formül kullanabilirsiniz. İyi bir örnek normal dağılımdaki CDF'dir.

Ayrıca burada bir örnek .

Başka bir örnek, bir dairenin sağ alt kadranını elde etmek için 90 dereceden 180 dereceye kadar rastgele bir değer alabilir, cos (r) kullanarak x bileşenini alır ve bunu öncelikli bir listeye endekslemek için kullanabilirsiniz.

Farklı formüllerle, herhangi bir uzunluktaki öncelikli bir listeyi girdiğiniz (örneğin N) ve formülün sonucunu (örneğin: cos (x) 0 ile 1 arasında) çarpma (örn: Ncos (x) olan genel bir yaklaşıma sahip olabilirsiniz. ) = 0 - N) dizinini almak için.


3
Sadece bir satır olsaydı, bu kod satırını bize verebilir misiniz? C # ile aşina değilim, o yüzden ne demek istediğini anlamadım.
HEGX64

@ HEGX64 eklendi, ancak mobil ve düzenleyici kullanarak çalışmıyor. Düzenler misiniz
Sentinel,

4
Bu cevabı, belirli bir dilde belirli bir öğretiden ziyade arkasındaki kavramı açıklamak için değiştirebilir misiniz?
Raimund Krämer

@ RaimundKrämer Erm, yapıldı?
Sentinel

Açıklama olmadan aşağı oy = işe yaramaz ve antisosyal.
WGroleau

1

Olasılıkların kodlanmış olması gerekmez. Öğeler ve eşikler bir dizide birlikte olabilir.

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

Eşikleri hala biriktirmeniz gerekir, ancak kodlama yerine bir parametre dosyası oluştururken yapabilirsiniz.


3
Doğru eşiğin nasıl hesaplanacağına dikkat eder misiniz? Örneğin, her biri% 33 şansa sahip üç öğeniz varsa, bu tabloyu nasıl oluşturacaksınız? Her seferinde yeni bir rasgele () oluşturulduğundan, ilki 0.3333'e, ikincisi 0.5'e ve sonuncusu 1.0'a ihtiyaç duyardı. Yoksa algoritmayı yanlış mı okudum?
boru,

Bunu başkalarının cevaplarında yaptığı gibi hesaplıyorsun. X öğelerinin eşit olasılıkları için, ilk eşik 1 / X, ikincisi, 2 / X, vb.
WGroleau

Bunu yapmak için bu algoritmadaki 3 madde için eşikler 1/3, 2/3 ve 3/3, ancak sonuç olasılıkları 1/3, 4/9 ve 2/9 birinci, ikinci ve üçüncü madde için olur. Gerçekten de çağrıyı random()döngüde yapmak mı istiyorsun?
boru

Hayır, bu kesinlikle bir hata. Her kontrol aynı rasgele sayıya ihtiyaç duyar.
WGroleau

0

Bu işlevi yaptım: https://github.com/thewheelmaker/GDscript_Weighted_Random Şimdi! senin durumunda böyle kullanabilirsiniz:

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

Yalnızca 0 ile 4 arasında bir sayı verir, ancak öğeleri aldığınız diziye koyabilirsiniz.

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

Veya fonksiyon:

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

İşte kod. GDscript üzerinde yaptım, yapabilirsin, ama başka bir dili de değiştirebilir, ayrıca mantık hatalarını da kontrol edebilir:

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

Bunun gibi çalışır: on_normal_case ([50,50], 0) Bu 0 veya 1 verir, her ikisi de aynı olasılıktadır.

on_normal_case ([50,50], 1) Bu, 1 veya 2 verir, her ikisi de aynı olasılığı vardır.

on_normal_case ([20,80], 1) Bu 1 veya 2 verir, iki tane almak için daha büyük bir değişikliğe sahiptir.

on_normal_case ([20,80,20,20,30], 1) Bu 1-5 rasgele sayılar verir ve daha büyük sayılar daha küçük sayılardan daha muhtemeldir.

on_normal_case ([20,80,0,0,20,20,30,0,0,0,0,33], 45) Bu atma, gördüğünüz zaman 45,46,49,50,51,56 sayıları arasında değişir. sıfır, asla oluşmaz.

Bu nedenle, işlev, bu dizi dizisinin uzunluğuna ve dönüşüm numarasına bağlı olarak yalnızca bir rasgele sayı döndürür ve dizideki inç sayısı, bir sayının oluşabileceği olasılık ağırlıklarıdır, bu sayının dizideki konumu, artı sayı sayısını artırabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.