ağırlıklı rastgele öğe almak


51

Örneğin, bu tablo var

+ ----------------- +
| meyve | ağırlık |
+ ----------------- +
| elma | 4 |
| turuncu | 2 |
| limon | 1 |
+ ----------------- +

Rasgele bir meyve vermem gerekiyor. Ama elma kadar sık olarak 4 kez aldı edilmelidir Lemon kadar sık ve 2 kez turuncu .

Daha genel bir durumda f(weight)sık sık olması gerekir .

Bu davranışı uygulamak için iyi bir genel algoritma nedir?

Ya da Ruby'de bazı hazır taşlar var? :)

PS
Ruby'de güncel algoritma kullandım https://github.com/fl00r/pickup


11
Diablo :-) 'te rastgele ganimet elde etmek için aynı formül olmalı
Jalayn

1
@ Jalayn: Aslında, benim cevabımdaki aralık çözümü fikri, World of Warcraft'taki savaş tabloları hakkında hatırladığımdan geliyor. :-D
Benjamin Kloster



Ben uyguladık birkaç basit ağırlıklı rastgele algoritmalar . Sorunuz olursa lütfen bana söyleyin.
Leonid Ganeline,

Yanıtlar:


50

Kavramsal olarak en basit çözüm, her bir elemanın ağırlığının olduğu kadar meydana geldiği bir liste oluşturmak olacaktır.

fruits = [apple, apple, apple, apple, orange, orange, lemon]

Ardından listenizdeki rastgele bir öğeyi seçmek için elinizde olan fonksiyonları kullanın (örn. Uygun aralıkta rastgele bir dizin oluşturun). Bu elbette çok hafıza açısından verimli değildir ve tamsayılar gerektirir.


Başka, biraz daha karmaşık bir yaklaşım şöyle görünür:

  1. Ağırlıkların toplamı hesaplayın:

    intervals = [4, 6, 7]

    4'ün altındaki bir endeksin bir elmayı , 4 ila 6'nın altında bir portakalı ve 6 ila 7'nin altında bir limonu temsil ettiği durumlarda .

  2. Rastgele bir sayı oluşturmak naralığında 0için sum(weights).

  3. Kümülatif toplamı yukarıda olan son öğeyi bulun n. Karşılık gelen meyve senin sonucun.

Bu yaklaşım, ilkinden daha karmaşık kod gerektirir, ancak daha az bellek ve hesaplama gerektirir ve kayan nokta ağırlıklarını destekler.

Her iki algoritmada da, kurulum adımı rastgele seçilmiş rastgele seçimler için bir kez yapılabilir.


2
aralık çözümü iyi görünüyor
Jalayn

1
Bu benim ilk düşüncemdi :). Peki ya 100 meyve içeren bir tablom varsa ve ağırlığı 10 bin civarında olabilirse? Çok geniş bir dizi olacak ve bu istediğim kadar verimli olmayacak. Bu ilk çözümle ilgili. İkinci çözüm iyi görünüyor
fl00r

1
Bu algoritmayı Ruby'de kullandım github.com/fl00r/pickup
fl00r

1
alias yöntemi, bunun üstesinden gelmenin en etkili yolu , takma yöntemi yok sayarak , aynı kodu tekrar tekrar tekrar eden gönderilerin sayısına dürüstçe şaşırdım . tanrı aşkına sürekli zaman performansı olsun!
opa

30

İşte herhangi bir diziden rastgele ağırlıklı öğeyi seçebilen, yalnızca bir kez yineleyen bir algoritma (C #):

public static T Random<T>(this IEnumerable<T> enumerable, Func<T, int> weightFunc)
{
    int totalWeight = 0; // this stores sum of weights of all elements before current
    T selected = default(T); // currently selected element
    foreach (var data in enumerable)
    {
        int weight = weightFunc(data); // weight of current element
        int r = Random.Next(totalWeight + weight); // random value
        if (r >= totalWeight) // probability of this is weight/(totalWeight+weight)
            selected = data; // it is the probability of discarding last selected element and selecting current one instead
        totalWeight += weight; // increase weight sum
    }

    return selected; // when iterations end, selected is some element of sequence. 
}

Bu, aşağıdaki akıl yürütmeye dayanır: Dizilimimizin ilk öğesini "geçerli sonuç" olarak seçelim; daha sonra, her yinelemede, saklayın ya da atın ve geçerli olarak yeni bir öğe seçin. Biz herhangi bir elementin olasılık bir bütün olasılıkların ürünü olarak sonunda seçilecek hesaplayabilir olmaz sonraki adımlarda atılmalıdır, kere ilk etapta belli olacağını olasılık. Matematiği yaparsanız, bu ürünün basitçe (element ağırlığının) / (tüm ağırlıkların toplamını) basitleştiğini göreceksiniz, ki bu tam olarak ihtiyacımız olan şey!

Bu yöntem yalnızca bir kez giriş sırası üzerinde yinelendiğinden, ağırlıkların toplamının bir int(veya bu sayaç için daha büyük bir tür seçebilirsiniz) olması koşuluyla, müstehcen büyük sekanslarla bile çalışır.


2
Bir kez daha yinelendiği için daha iyi olduğunu varsaymadan önce bunu kıyaslarım. Aynı sayıda rastgele değer üretmek de tam olarak hızlı değildir.
Jean-Bernard Pellerin

1
@ Jean-Bernard Pellerin Yaptım ve aslında büyük listelerde daha hızlı. Kriptografik olarak rasgele bir jeneratör kullanmazsanız (-8
Nevermind

Kabul edilen cevap olmalı. Bunu "aralık" ve "tekrarlanan giriş" yaklaşımından daha çok seviyorum.
Vivin Paliath

2
Bu yöntemi kullanmak için son birkaç yılda bu konuya 3 ya da 4 kez geldiğimi söylemek istedim. Bu yöntem, amaçlarım için yeterince hızlı bir şekilde ihtiyacım olan cevapları sağlamayı başardı. Keşke tekrar kullanmak için geri geldiğimde bu cevabı oylayabilmeyi diliyorum.
Jim Yarbro

1
Güzel bir çözüm gerçekten sadece bir kez seçmek zorunda kalırsanız. Aksi halde, çözüm için hazırlık çalışmalarını ilk cevapta bir kez yapmak çok daha etkilidir.
Deduplicator

22

Zaten mevcut cevaplar iyidir ve ben onları biraz daha genişleteceğim.

Benjamin'in önerdiği gibi, kümülatif toplamlar bu tür problemlerde tipik olarak kullanılır:

+------------------------+
| fruit  | weight | csum |
+------------------------+
| apple  |   4    |   4  |
| orange |   2    |   6  |
| lemon  |   1    |   7  |
+------------------------+

Bu yapıdaki bir öğeyi bulmak için Nevermind'in kod parçası gibi bir şey kullanabilirsiniz. Bu genellikle kullandığım C # kodu parçası:

double r = Random.Next() * totalSum;
for(int i = 0; i < fruit.Count; i++)
{
    if (csum[i] > r)
        return fruit[i];
}

Şimdi ilginç kısma. Bu yaklaşım ne kadar verimli ve en verimli çözüm nedir? Kod parçam O (n) bellek gerektiriyor ve O (n) zamanda çalışıyor. Aslında, O (n) alanından daha azıyla yapılabileceğini sanmıyorum ama zaman karmaşıklığı çok daha düşük olabilir, aslında O (log n) . Hile döngü için normal yerine ikili arama kullanmaktır.

double r = Random.Next() * totalSum;
int lowGuess = 0;
int highGuess = fruit.Count - 1;

while (highGuess >= lowGuess)
{
    int guess = (lowGuess + highGuess) / 2;
    if ( csum[guess] < r)
        lowGuess = guess + 1;
    else if ( csum[guess] - weight[guess] > r)
        highGuess = guess - 1;
    else
        return fruit[guess];
}

Ayrıca ağırlık güncellemeyle ilgili bir hikaye var. En kötü durumda, bir elemanın güncelleme ağırlığı, güncelleme karmaşıklığını O (n) ' ye yükselten tüm elemanlar için kümülatif toplamların güncellenmesine neden olur . Bu da ikili indekslenmiş ağaç kullanılarak O (log n) ' ye kadar azaltılabilir .


İkili arama hakkında iyi bir nokta
fl00r

Nevermind'in cevabının fazladan alana ihtiyacı yok, bu yüzden O (1), fakat art arda rasgele sayılar üreterek ve ağırlık işlevini (temel probleme bağlı olarak pahalı olabilir) değerlendirerek çalışma zamanı karmaşıklığı ekler.
Benjamin Kloster

1
Kodumun "daha okunabilir bir sürümü" olduğunu iddia ettiğiniz aslında değil. Kodunuzun, toplam ağırlık ve kümülatif toplamları önceden bilmesi gerekir; benim değil.
Nevermind

@Benjamin Kloster Kodum, element başına yalnızca bir kez ağırlık işlevini çağırıyor - bundan daha iyisini yapamazsınız. Yine de rastgele sayılar konusunda haklısın.
Nevermind

@Severmind: Seçim işlevine yapılan çağrı başına yalnızca bir kez çağırırsınız; böylece kullanıcı iki kez ararsa, ağırlık işlevi her öğe için tekrar çağrılır. Elbette önbelleğe alabilirsiniz, ancak o zaman artık alan karmaşıklığı için O (1) değilsiniz.
Benjamin Kloster

8

Bu basit bir Python uygulamasıdır:

from random import random

def select(container, weights):
    total_weight = float(sum(weights))
    rel_weight = [w / total_weight for w in weights]

    # Probability for each element
    probs = [sum(rel_weight[:i + 1]) for i in range(len(rel_weight))]

    slot = random()
    for (i, element) in enumerate(container):
        if slot <= probs[i]:
            break

    return element

ve

population = ['apple','orange','lemon']
weights = [4, 2, 1]

print select(population, weights)

Genetik algoritmalarda, bu seçme prosedürüne Spor orantılı seçim veya Rulet Çark Seçimi denir :

  • tekerleğin bir kısmı, ağırlık değerlerine bağlı olarak olası seçimlerin her birine atanır. Bu, bir seçimin ağırlığının, tüm seçimlerin toplam ağırlığına bölünmesi ve böylece bunların 1'e normalleştirilmesi ile sağlanabilir.
  • daha sonra rulet çarkının döndürülme şekline benzer bir rastgele seçim yapılır.

Rulet tekerleği seçimi

Tipik algoritmalar O (N) veya O (log N) karmaşıklığına sahiptir ancak aynı zamanda O (1) de yapabilirsiniz (örn . Stokastik kabul yoluyla rulet tekerleği seçimi ).


Bu görüntünün asıl kaynağının ne olduğunu biliyor musunuz? Bir makale için kullanmak istiyorum, ancak atıftan emin olmak gerekiyor.
Malcolm MacLeod

@MalcolmMacLeod Üzgünüz, birçok GA gazetesinde / sitesinde kullanılıyor ancak yazarın kim olduğunu bilmiyorum.
manlio

0

Bu özlem tam olarak ne istiyorsan onu yapıyor.

public static Random random = new Random(DateTime.Now.Millisecond);
public int chooseWithChance(params int[] args)
    {
        /*
         * This method takes number of chances and randomly chooses
         * one of them considering their chance to be choosen.    
         * e.g. 
         *   chooseWithChance(0,99) will most probably (%99) return 1
         *   chooseWithChance(99,1) will most probably (%99) return 0
         *   chooseWithChance(0,100) will always return 1.
         *   chooseWithChance(100,0) will always return 0.
         *   chooseWithChance(67,0) will always return 0.
         */
        int argCount = args.Length;
        int sumOfChances = 0;

        for (int i = 0; i < argCount; i++) {
            sumOfChances += args[i];
        }

        double randomDouble = random.NextDouble() * sumOfChances;

        while (sumOfChances > randomDouble)
        {
            sumOfChances -= args[argCount -1];
            argCount--;
        }

        return argCount-1;
    }

böyle kullanabilirsiniz:

string[] fruits = new string[] { "apple", "orange", "lemon" };
int choosenOne = chooseWithChance(98,1,1);
Console.WriteLine(fruits[choosenOne]);

Yukarıdaki kod büyük olasılıkla (% 98) verilen dizi için 'elma' için dizin olan 0 değerini döndürecektir.

Ayrıca, bu kod yukarıda verilen yöntemi test eder:

Console.WriteLine("Start...");
int flipCount = 100;
int headCount = 0;
int tailsCount = 0;

for (int i=0; i< flipCount; i++) {
    if (chooseWithChance(50,50) == 0)
        headCount++;
    else
        tailsCount++;
}

Console.WriteLine("Head count:"+ headCount);
Console.WriteLine("Tails count:"+ tailsCount);

Bir çıktıya şöyle bir şey verir:

Start...
Head count:52
Tails count:48

2
Programcılar olduğu konusunda kavramsal soruları ve cevapları şeyleri açıklamak bekleniyor. Açıklama yerine kod dökümlerini atmak, kodu IDE'den beyaz tahtaya kopyalamak gibidir: tanıdık gelebilir ve hatta bazen anlaşılabilir görünebilir, ancak garip ... sadece garip geliyor. Beyaz Tahta derleyici yok
gnat

Haklısın, koda odaklandım, bu yüzden nasıl çalıştığını söylemeyi unuttum. Nasıl çalıştığı hakkında bir açıklama ekleyeceğim.
Ramazan Polat
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.