Cómo implementar una baraja ponderada

22

Recientemente escribí un código que pensé que era muy ineficiente, pero como solo incluía unos pocos valores, lo acepté. Sin embargo, todavía estoy interesado en un mejor algoritmo para lo siguiente:

  1. Una lista de objetos X, a cada uno de ellos se le asigna un "peso"
  2. Resume los pesos
  3. Genera un número aleatorio de 0 a la suma
  4. Iterar a través de los objetos, restando su peso de la suma hasta que la suma no sea positiva
  5. Elimine el objeto de la lista y luego agréguelo al final de la nueva lista

Los ítems 2,4 y 5 toman ntiempo, por lo que es un O(n^2)algoritmo.

¿Se puede mejorar esto?

Como ejemplo de una combinación aleatoria ponderada, un elemento tiene una mayor probabilidad de estar al frente con un peso mayor.

Ejemplo (generaré números aleatorios para hacerlo real):

6 objetos con pesas 6,5,4,3,2,1; Suma es 21

Elegí 19: por lo 19-6-5-4-3-2 = -1tanto, 2 va en la primera posición, los pesos ahora son 6,5,4,3,1; Suma es 19

Elegí 16: por lo 16-6-5-4-3 = -2tanto, 3 va en la segunda posición, los pesos ahora son 6,5,4,1; Suma es 16

Elegí 3: por lo 3-6 = -3tanto, 6 va en la tercera posición, los pesos ahora son 5,4,1; Suma es 10

Escogí 8: 8-5-4 = -1entonces 4 va en la cuarta posición, los pesos ahora son 5,1; Suma es 6

Escogí 5: 5-5=0entonces 5 va en la quinta posición, los pesos ahora son 1; Suma es 1

Elegí 1: por lo 1-1=0tanto, 1 va en la última posición, no tengo más pesas, termino

Nathan Merrill
fuente
66
¿Qué es exactamente una barajadura ponderada? ¿Significa que cuanto mayor sea el peso, más probable es que el objeto esté en la parte superior de la cubierta?
Doval
Por curiosidad, cuál es el propósito del paso (5). Hay formas de mejorar esto si la lista es estática.
Gort the Robot
Sí Doval Elimino el elemento de la lista para que no aparezca en la lista aleatoria más de una vez.
Nathan Merrill
¿Es constante el peso de un artículo en la lista?
Un artículo tendrá un peso mayor que otro, pero el artículo X siempre tendrá el mismo peso. (Obviamente, si quita elementos, el mayor peso aumentará en proporción)
Nathan Merrill

Respuestas:

14

Esto se puede implementar al O(n log(n))usar un árbol.

Primero, cree el árbol, manteniendo en cada nodo la suma acumulativa de todos los nodos descendientes a la derecha y a la izquierda de cada nodo.

Para muestrear un elemento, muestre recursivamente desde el nodo raíz, utilizando las sumas acumulativas para decidir si devuelve el nodo actual, un nodo desde la izquierda o un nodo desde la derecha. Cada vez que muestree un nodo, establezca su peso en cero y también actualice los nodos principales.

Esta es mi implementación en 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_shufflees un generador, por lo que puede probar los kelementos principales de manera eficiente. Si desea mezclar todo el conjunto, simplemente repita el proceso hasta el agotamiento (utilizando la listfunción).

ACTUALIZAR:

El muestreo aleatorio ponderado (2005; Efraimidis, Spirakis) proporciona un algoritmo muy elegante para esto. La implementación es súper simple y también se ejecuta en 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]
jbochi
fuente
La última actualización parece inquietantemente similar a una solución incorrecta de una sola línea . ¿Estás seguro de que es correcto?
Giacomo Alzetta
19

EDITAR: Esta respuesta no interpreta los pesos de la manera que se esperaría. Es decir, un artículo con peso 2 no tiene el doble de probabilidades de ser el primero que uno con peso 1.

Una forma de barajar una lista es asignar números aleatorios a cada elemento de la lista y ordenarlos por esos números. Podemos extender esa idea, solo tenemos que elegir números aleatorios ponderados. Por ejemplo, podrías usar random() * weight. Diferentes opciones producirán diferentes distribuciones.

En algo como Python, esto debería ser tan simple como:

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

Tenga cuidado de no evaluar las claves más de una vez, ya que terminarán con valores diferentes.

Winston Ewert
fuente
2
Esto es honestamente genial debido a su simplicidad. Suponiendo que está utilizando un algoritmo de clasificación nlogn, esto debería funcionar bien.
Nathan Merrill
¿Cuál es el peso de las pesas? Si son altos, los objetos simplemente se ordenan por peso. Si son bajos, los objetos son casi aleatorios con solo pequeñas perturbaciones según el peso. De cualquier manera, este método siempre lo he usado, pero el cálculo de la posición de clasificación probablemente necesitará algunos ajustes.
david.pfx
@ david.pfx El rango de los pesos debe ser el rango de los números aleatorios. De esa manera max*min = min*max, y por lo tanto, es posible cualquier permutación, pero algunas son mucho más probables (especialmente si los pesos no están distribuidos uniformemente)
Nathan Merrill
2
En realidad, este enfoque está mal! Imagine que pesa 75 y 25. Para el caso 75, 2/3 de las veces elegirá un número> 25. Durante el 1/3 restante del tiempo, "vencerá" al 25 50% de las veces. 75 serán los primeros 2/3 + (1/3 * 1/2) del tiempo: 83%. Aún no he resuelto la solución.
Adam Rabung
1
Esta solución debería funcionar reemplazando la distribución uniforme del muestreo aleatorio por una distribución exponencial.
P-Gn
5

Primero, trabajemos desde que el peso de un elemento dado en la lista que se va a ordenar es constante. No va a cambiar entre iteraciones. Si lo hace, entonces ... bueno, ese es un problema mayor.

Por ejemplo, usemos una baraja de cartas donde queremos pesar las cartas de la cara al frente. weight(card) = card.rank. Resumiendo estos, si no sabemos la distribución de los pesos es de hecho O (n) una vez.

Estos elementos se almacenan en una estructura ordenada, como una modificación en una lista de omisión indexable, de modo que se pueda acceder a todos los índices de los niveles desde un nodo dado:

   1 10
 o ---> o -------------------------------------------- -------------> o Nivel superior
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o Nivel 3
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o Nivel 2
   1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o Nivel inferior

Cabeza 1º 2º 3º 4º 5º 6º 7º 8º 9º 10º NIL
      Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo

Sin embargo, en este caso, cada nodo también 'ocupa' tanto espacio como su peso.

Ahora, al buscar una tarjeta en esta lista, se puede acceder a su posición en la lista en el tiempo O (log n) y eliminarla de las listas asociadas en el tiempo O (1). Ok, podría no ser O (1), podría ser O (log log n) tiempo (tendría que pensar en esto mucho más). Eliminar el sexto nodo en el ejemplo anterior implicaría actualizar los cuatro niveles, y esos cuatro niveles son independientes de la cantidad de elementos que hay en la lista (dependiendo de cómo implemente los niveles).

Como el peso de un elemento es constante, uno puede hacerlo sum -= weight(removed)sin tener que atravesar la estructura nuevamente.

Y por lo tanto, tiene un costo único de O (n) y un valor de búsqueda de O (log n) y un costo de eliminación de la lista de O (1). Esto se convierte en O (n) + n * O (log n) + n * O (1) que le proporciona un rendimiento general de O (n log n).


Veamos esto con tarjetas, porque eso es lo que usé anteriormente.

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

Este es un mazo realmente pequeño con solo 4 cartas. Debería ser fácil ver cómo se puede extender esto. Con 52 cartas, una estructura ideal tendría 6 niveles (log 2 (52) ~ = 6), aunque si profundiza en las listas de omisión, incluso eso podría reducirse a un número menor.

La suma de todos los pesos es 10. Entonces obtienes un número aleatorio de [1 .. 10) y es 4 Recorres la lista de saltos para encontrar el elemento que está en el techo (4). Como 4 es menor que 10, pasas del nivel superior al segundo nivel. Cuatro es mayor que 3, así que ahora estamos en el 2 de los diamantes. 4 es menos de 3 + 7, por lo que nos movemos hacia el nivel inferior y 4 es menos de 3 + 3, por lo que tenemos un 3 de diamantes.

Después de eliminar los 3 diamantes de la estructura, la estructura ahora se ve así:

       7 7
top 3 ----------------> 4d
                         .
       3 4.
    2 ---------> 2d -> 4d
                  . .
       1 2. 4)
bot 1 -> Anuncio -> 2d -> 4d

Notará que los nodos ocupan una cantidad de 'espacio' proporcional a su peso en la estructura. Esto permite la selección ponderada.

Como esto se aproxima a un árbol binario equilibrado, la búsqueda en este no necesita caminar por la capa inferior (que sería O (n)) y, en cambio, ir desde la parte superior le permite saltar rápidamente la estructura para encontrar lo que está buscando para.

Gran parte de esto podría hacerse con algún tipo de árbol equilibrado. El problema es que el reequilibrio de la estructura cuando se elimina un nodo se vuelve confuso, ya que esta no es una estructura de árbol clásica y la limpieza para recordar que el 4 de los diamantes ahora se mueve de las posiciones [6 7 8 9] a [3 4 5 6] puede costar más que los beneficios de la estructura del árbol.

Sin embargo, aunque la lista de omisión se aproxima a un árbol binario en su capacidad de omitir la lista en tiempo O (log n), tiene la simplicidad de trabajar con una lista vinculada.

Esto no quiere decir que sea fácil hacer todo esto (aún necesita mantener pestañas en todos los enlaces que necesita modificar cuando elimina un elemento), pero significa solo actualizar todos los niveles que tenga y sus enlaces. que todo a la derecha en la estructura de árbol adecuada.


fuente
No estoy seguro de cómo lo que usted está describiendo coincide con una lista de salto (pero entonces, qué acaba de ver las listas de arriba Salto). Por lo que entiendo en Wikipedia, los de mayor peso estarían más a la derecha que los de menor peso. Sin embargo, está describiendo que el ancho de los saltos debe ser el peso. Otra pregunta ... usando esta estructura, ¿cómo se elige un elemento aleatorio?
Nathan Merrill
1
@MrTi, por lo tanto, la modificación de la idea de una lista de omisión indexable. La clave es poder acceder al elemento en el que el peso de los elementos anteriores se suma a <23 en tiempo O (log n) en lugar de tiempo O (n). Todavía elige el elemento aleatorio de la manera que estaba describiendo, selecciona un número aleatorio de [0, suma (pesos)] y luego obtiene el elemento correspondiente de la lista. No importa en qué orden se encuentren los nodos / tarjetas en la lista de salto, porque la clave es el "espacio" más grande que ocupan los elementos pesados ​​más pesados.
Ah, ya entiendo. Me gusta.
Nathan Merrill