Come implementare uno shuffle ponderato


22

Di recente ho scritto del codice che ho ritenuto molto inefficiente, ma poiché includeva solo pochi valori, l'ho accettato. Tuttavia, sono ancora interessato a un algoritmo migliore per quanto segue:

  1. Un elenco di oggetti X, a ciascuno di essi viene assegnato un "peso"
  2. Riassumi i pesi
  3. Genera un numero casuale da 0 alla somma
  4. Scorrere gli oggetti, sottraendo il loro peso dalla somma fino a quando la somma non è positiva
  5. Rimuovere l'oggetto dall'elenco e quindi aggiungerlo alla fine del nuovo elenco

Gli articoli 2,4 e 5 richiedono tutti del ntempo, quindi è un O(n^2)algoritmo.

Questo può essere migliorato?

Come esempio di shuffle ponderato, un elemento ha maggiori possibilità di trovarsi davanti con un peso maggiore.

Esempio (genererò numeri casuali per renderlo reale):

6 oggetti con pesi 6,5,4,3,2,1; La somma è 21

Ho scelto 19 19-6-5-4-3-2 = -1:, quindi 2 va in prima posizione, i pesi ora sono 6,5,4,3,1; La somma è 19

Ho scelto 16 16-6-5-4-3 = -2:, quindi 3 va in seconda posizione, i pesi ora sono 6,5,4,1; La somma è 16

Ho scelto 3 3-6 = -3:, quindi 6 va in terza posizione, i pesi ora sono 5,4,1; La somma è 10

Ho scelto 8:, 8-5-4 = -1quindi 4 va in quarta posizione, i pesi ora sono 5,1; La somma è 6

Ho scelto 5:, 5-5=0quindi 5 va in quinta posizione, i pesi ora sono 1; La somma è 1

Ho scelto 1:, 1-1=0quindi 1 va nell'ultima posizione, non ho più pesi, finisco


6
Che cos'è esattamente uno shuffle ponderato? Significa che maggiore è il peso, maggiore è la probabilità che l'oggetto si trovi in ​​cima al mazzo?
Doval,

Per curiosità, qual è lo scopo del passaggio (5). Ci sono modi per migliorare questo se l'elenco è statico.
Gort il robot il

Sì, Doval. Rimuovo l'elemento dall'elenco in modo che non appaia nell'elenco mescolato più di una volta.
Nathan Merrill,

Il peso di un articolo nell'elenco è costante?

Un articolo avrà un peso maggiore di un altro, ma l'articolo X avrà sempre lo stesso peso. (Ovviamente, se rimuovi gli articoli, il peso maggiore aumenterà in proporzione)
Nathan Merrill

Risposte:


14

Questo può essere implementato O(n log(n))usando un albero.

Innanzitutto, creare l'albero, mantenendo in ciascun nodo la somma cumulativa di tutti i nodi discendenti a destra e a sinistra di ciascun nodo.

Per campionare un elemento, campiona ricorsivamente dal nodo radice, usando le somme cumulative per decidere se restituire il nodo corrente, un nodo da sinistra o un nodo da destra. Ogni volta che si campiona un nodo, impostarne il peso su zero e anche aggiornare i nodi padre.

Questa è la mia implementazione in Python:

import random

def weigthed_shuffle(items, weights):
    if len(items) != len(weights):
        raise ValueError("Unequal lengths")

    n = len(items)
    nodes = [None for _ in range(n)]

    def left_index(i):
        return 2 * i + 1

    def right_index(i):
        return 2 * i + 2

    def total_weight(i=0):
        if i >= n:
            return 0
        this_weigth = weights[i]
        if this_weigth <= 0:
            raise ValueError("Weigth can't be zero or negative")
        left_weigth = total_weight(left_index(i))
        right_weigth = total_weight(right_index(i))
        nodes[i] = [this_weigth, left_weigth, right_weigth]
        return this_weigth + left_weigth + right_weigth

    def sample(i=0):
        this_w, left_w, right_w = nodes[i]
        total = this_w + left_w + right_w
        r = total * random.random()
        if r < this_w:
            nodes[i][0] = 0
            return i
        elif r < this_w + left_w:
            chosen = sample(left_index(i))
            nodes[i][1] -= weights[chosen]
            return chosen
        else:
            chosen = sample(right_index(i))
            nodes[i][2] -= weights[chosen]
            return chosen

    total_weight() # build nodes tree

    return (items[sample()] for _ in range(n - 1))

Uso:

In [2]: items = list(range(10))
   ...: weights = list(range(10, 0, -1))
   ...:

In [3]: for _ in range(10):
   ...:     print(list(weigthed_shuffle(items, weights)))
   ...:
[5, 0, 8, 6, 7, 2, 3, 1, 4]
[1, 2, 5, 7, 3, 6, 9, 0, 4]
[1, 0, 2, 6, 8, 3, 7, 5, 4]
[4, 6, 8, 1, 2, 0, 3, 9, 7]
[3, 5, 1, 0, 4, 7, 2, 6, 8]
[3, 7, 1, 2, 0, 5, 6, 4, 8]
[1, 4, 8, 2, 6, 3, 0, 9, 5]
[3, 5, 0, 4, 2, 6, 1, 8, 9]
[6, 3, 5, 0, 1, 2, 4, 8, 7]
[4, 1, 2, 0, 3, 8, 6, 5, 7]

weigthed_shuffleè un generatore, quindi puoi campionare gli kelementi migliori in modo efficiente. Se si desidera lo shuffle dell'intero array, basta scorrere il generatore fino all'esaurimento (usando la listfunzione).

AGGIORNARE:

Il campionamento casuale ponderato (2005; Efraimidis, Spirakis) fornisce un algoritmo molto elegante per questo. L'implementazione è super semplice e funziona anche in O(n log(n)):

def weigthed_shuffle(items, weights):
    order = sorted(range(len(items)), key=lambda i: -random.random() ** (1.0 / weights[i]))
    return [items[i] for i in order]

L'ultimo aggiornamento sembra stranamente simile a una soluzione one-liner sbagliata . Sei sicuro che sia corretto?
Giacomo Alzetta,

19

EDIT: questa risposta non interpreta i pesi nel modo previsto. Vale a dire un articolo con peso 2 non è il doppio delle probabilità di essere il primo di uno con peso 1.

Un modo per mescolare un elenco è quello di assegnare numeri casuali a ciascun elemento nell'elenco e ordinarli in base a tali numeri. Possiamo estendere quell'idea, dobbiamo solo selezionare numeri casuali ponderati. Ad esempio, potresti usare random() * weight. Scelte diverse produrranno distribuzioni diverse.

In qualcosa come Python, questo dovrebbe essere semplice come:

items.sort(key = lambda item: random.random() * item.weight)

Fai attenzione a non valutare le chiavi più di una volta, poiché finiranno con valori diversi.


2
Questo è onestamente geniale per la sua semplicità. Supponendo che tu stia utilizzando un algoritmo di ordinamento nlogn, questo dovrebbe funzionare bene.
Nathan Merrill,

Qual è il peso dei pesi? Se sono alti, gli oggetti vengono semplicemente ordinati in base al peso. Se sono bassi, gli oggetti sono quasi casuali con solo lievi perturbazioni in base al peso. Ad ogni modo, questo metodo l'ho sempre usato, ma probabilmente il calcolo della posizione di ordinamento richiederà qualche modifica.
david.pfx,

@ david.pfx L'intervallo dei pesi dovrebbe essere l'intervallo dei numeri casuali. In questo modo max*min = min*max, e quindi qualsiasi permutazione è possibile, ma alcuni sono molto più probabili (specialmente se i pesi non sono distribuiti uniformemente)
Nathan Merrill

2
In realtà, questo approccio è sbagliato! Immagina pesi 75 e 25. Per il caso 75, 2/3 del tempo sceglierà un numero> 25. Per il restante 1/3 del tempo, "batterà" il 25 50% del tempo. 75 saranno i primi 2/3 + (1/3 * 1/2) del tempo: 83%. Non ho ancora risolto la correzione.
Adam Rabung,

1
Questa soluzione dovrebbe funzionare sostituendo la distribuzione uniforme del campionamento casuale con una distribuzione esponenziale.
P-Gn,

5

Innanzitutto, consente di lavorare in base al quale il peso di un determinato elemento nell'elenco da ordinare è costante. Non cambierà tra le iterazioni. Se lo fa, allora ... beh, questo è un problema più grande.

Per esempio, usiamo un mazzo di carte in cui vogliamo pesare le carte frontali. weight(card) = card.rank. Riassumendo questi, se non sappiamo che la distribuzione dei pesi è effettivamente O (n) una volta.

Questi elementi sono memorizzati in una struttura ordinata come una modifica in un elenco di salto indicizzabile in modo tale che tutti gli indici dei livelli siano accessibili da un determinato nodo:

   1 10
 o ---> o -------------------------------------------- -------------> o Livello superiore
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o Livello 3
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o Livello 2
   1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o Livello inferiore

Testa 1 ° 2 ° 3 ° 4 ° 5 ° 6 ° 7 ° 8 ° 9 ° 10 ° NIL
      Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo

Tuttavia, in questo caso, ogni nodo "occupa" tanto spazio quanto il suo peso.

Ora, quando si cerca una carta in questo elenco, è possibile accedere alla sua posizione nell'elenco in O (log n) time e rimuoverlo dagli elenchi associati in O (1) time. Ok, potrebbe non essere O (1), potrebbe essere O (log log n) volta (dovrei pensarci molto di più). La rimozione del sesto nodo nell'esempio sopra implicherebbe l'aggiornamento di tutti e quattro i livelli - e quei quattro livelli sono indipendenti dal numero di elementi presenti nell'elenco (a seconda della modalità di implementazione dei livelli).

Poiché il peso di un elemento è costante, si può semplicemente fare a sum -= weight(removed)meno di dover attraversare di nuovo la struttura.

E quindi, hai un costo una tantum di O (n) e un valore di ricerca di O (log n) e un costo di rimozione dalla lista di O (1). Questo diventa O (n) + n * O (log n) + n * O (1) che ti dà una performance complessiva di O (n log n).


Vediamo questo con le carte, perché è quello che ho usato sopra.

      10
top 3 -----------------------> 4d
                                .
       3 7.
    2 ---------> 2d ---------> 4d
                  . .
       1 2. 3 4.
bot 1 -> Annuncio -> 2d -> 3d -> 4d

Questo è un mazzo davvero piccolo con solo 4 carte. Dovrebbe essere facile vedere come può essere esteso. Con 52 carte una struttura ideale avrebbe 6 livelli (log 2 (52) ~ = 6), anche se se si scava negli skip list anche quello potrebbe essere ridotto a un numero più piccolo.

La somma di tutti i pesi è 10. Quindi ottieni un numero casuale da [1 .. 10) e il suo 4 Cammina l'elenco salta per trovare l'oggetto che si trova al soffitto (4). Poiché 4 è inferiore a 10, si passa dal livello superiore al secondo livello. Quattro è maggiore di 3, quindi ora siamo al 2 di diamanti. 4 è inferiore a 3 + 7, quindi passiamo al livello inferiore e 4 è inferiore a 3 + 3, quindi abbiamo 3 diamanti.

Dopo aver rimosso il 3 di diamanti dalla struttura, la struttura ora appare come:

       7
top 3 ----------------> 4d
                         .
       3 4.
    2 ---------> 2d -> 4d
                  . .
       1 2. 4
bot 1 -> Annuncio -> 2d -> 4d

Noterai che i nodi occupano una quantità di "spazio" proporzionale al loro peso nella struttura. Ciò consente la selezione ponderata.

Poiché questo si avvicina a un albero binario bilanciato, la ricerca in questo non ha bisogno di camminare sul livello inferiore (che sarebbe O (n)) e invece andare dall'alto ti consente di saltare rapidamente la struttura per trovare ciò che stai cercando per.

Gran parte di questo potrebbe invece essere fatto con una sorta di albero equilibrato. Il problema è che il riequilibrio della struttura quando un nodo viene rimosso diventa confuso poiché questa non è una struttura ad albero classica e le pulizie per ricordare che il 4 di diamanti viene ora spostato da posizioni [6 7 8 9] a [3 4 5 6] può costare di più rispetto ai vantaggi della struttura ad albero.

Tuttavia, mentre l'elenco skip si avvicina ad un albero binario nella sua capacità di saltare l'elenco in tempo O (log n), ha invece la semplicità di lavorare con un elenco collegato.

Questo non vuol dire che è facile fare tutto questo (è comunque necessario tenere sotto controllo tutti i collegamenti che è necessario modificare quando si rimuove un elemento), ma significa aggiornare solo i livelli che si hanno e i loro collegamenti piuttosto di tutto a destra sulla corretta struttura ad albero.


Non sono sicuro di come ciò che si sta descrivendo le partite una lista Skip (ma poi, mi ha fatto basta guardare le liste fino salto). Da quello che ho capito su Wikipedia, il peso maggiore sarebbe più a destra rispetto ai pesi inferiori. Tuttavia, stai descrivendo che la larghezza dei salti dovrebbe essere il peso. Un'altra domanda ... usando questa struttura, come scegli un elemento casuale?
Nathan Merrill,

1
@MrTi quindi la modifica sull'idea di un elenco di salto indicizzabile. La chiave è poter accedere all'elemento nel punto in cui il peso degli elementi precedenti viene sommato a <23 nel tempo O (log n) anziché nel tempo O (n). Scegli ancora l'elemento casuale nel modo in cui descrivi, seleziona un numero casuale da [0, somma (pesi)] e quindi ottieni l'elemento corrispondente dall'elenco. Non importa in quale ordine si trovino i nodi / le carte nella skip list - perché la chiave è lo "spazio" più grande occupato dagli oggetti più pesanti.

Ah capisco. Mi piace.
Nathan Merrill,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.