Estructura de datos para dados cargados?

130

Supongamos que tengo un dado cargado de n lados donde cada lado k tiene alguna probabilidad p k de aparecer cuando lo saco. Tengo curiosidad por saber si hay un buen algoritmo para almacenar esta información estáticamente (es decir, para un conjunto fijo de probabilidades) para poder simular de manera eficiente una tirada aleatoria del dado.

Actualmente, tengo una solución O (lg n) para este problema. La idea es almacenar una tabla de la probabilidad acumulativa de los primeros k lados para todos los k, generar un número real aleatorio en el rango [0, 1) y realizar una búsqueda binaria sobre la tabla para obtener el índice más grande cuyo acumulado el valor no es mayor que el valor elegido. Prefiero esta solución, pero parece extraño que el tiempo de ejecución no tenga en cuenta las probabilidades. En particular, en los casos extremos de un lado que siempre aparece o que los valores se distribuyen uniformemente, es posible generar el resultado del rollo en O (1) usando un enfoque ingenuo, aunque mi solución aún tomará muchos pasos logarítmica.

¿Alguien tiene alguna sugerencia sobre cómo resolver este problema de una manera que sea de alguna manera "adaptativa" en su tiempo de ejecución?

EDITAR : Basado en las respuestas a esta pregunta, he escrito un artículo que describe muchos enfoques a este problema , junto con sus análisis. Parece que la implementación de Vose del método de alias da time (n) tiempo de preprocesamiento y O (1) tiempo por tirada, lo cual es realmente impresionante. ¡Ojalá sea una adición útil a la información contenida en las respuestas!

templatetypedef
fuente
2
Es razonable que exista una solución O (1) para cada caso específico .
Tim

Respuestas:

117

Está buscando el método de alias que proporciona un método O (1) para generar una distribución de probabilidad discreta fija (suponiendo que puede acceder a las entradas en una matriz de longitud n en tiempo constante) con una configuración única de O (n) . Puede encontrarlo documentado en el capítulo 3 (PDF) de "Generación de varianza aleatoria no uniforme" de Luc Devroye.

La idea es tomar su conjunto de probabilidades p k y producir tres nuevos conjuntos de elementos n, q k , a k y b k . Cada q k es una probabilidad entre 0 y 1, y cada a k y b k es un número entero entre 1 y n.

Generamos números aleatorios entre 1 yn generando dos números aleatorios, rys, entre 0 y 1. Sea i = floor (r * N) +1. Si q i <s, entonces devuelve a i else return b i . El trabajo en el método de alias es descubrir cómo producir q k , a k y b k .

mhum
fuente
Para un algoritmo tan útil, el Método Alias ​​es sorprendentemente poco conocido.
mhum
Para el registro: publiqué una pequeña biblioteca C para muestreo aleatorio usando el método de alias apps.jcns.fz-juelich.de/ransampl .
Joachim W
1
Una implementación específica del método de alias puede ser más lenta que un método con una complejidad de tiempo peor, como la ruleta para un determinado nnúmero de números aleatorios a generar debido a factores constantes involucrados en la implementación de algoritmos.
jfs
4

Utilice un árbol de búsqueda binario equilibrado (o búsqueda binaria en una matriz) y obtenga la complejidad O (log n). Tenga un nodo para cada resultado de dado y haga que las claves sean el intervalo que activará ese resultado.

function get_result(node, seed):
    if seed < node.interval.start:
        return get_result(node.left_child, seed)
    else if seed < node.interval.end:
        // start <= seed < end
        return node.result
    else:
        return get_result(node.right_child, seed)

Lo bueno de esta solución es que es muy simple de implementar pero aún tiene buena complejidad.

hugomg
fuente
El árbol binario hecho a mano como el anterior es fácil de implementar, pero no se garantiza un equilibrio
yusong
Puede garantizar que esté equilibrado si lo construye en el orden correcto.
hugomg
3

Estoy pensando en granular tu mesa.

En lugar de tener una tabla con el valor acumulado para cada valor de dado, podría crear una matriz entera de longitud xN, donde x es idealmente un número alto para aumentar la precisión de la probabilidad.

Rellene esta matriz utilizando el índice (normalizado por xN) como el valor acumulativo y, en cada 'ranura' en la matriz, almacene la posible tirada de dados si aparece este índice.

Tal vez podría explicar más fácilmente con un ejemplo:

Usando tres dados: P (1) = 0.2, P (2) = 0.5, P (3) = 0.3

Cree una matriz, en este caso elegiré una longitud simple, digamos 10. (es decir, x = 3.33333)

arr[0] = 1,
arr[1] = 1,
arr[2] = 2,
arr[3] = 2,
arr[4] = 2,
arr[5] = 2,
arr[6] = 2,
arr[7] = 3,
arr[8] = 3,
arr[9] = 3

Luego, para obtener la probabilidad, simplemente aleatorice un número entre 0 y 10 y simplemente acceda a ese índice.

Este método puede perder precisión, pero aumentar xy la precisión será suficiente.

andrewjs
fuente
1
Para obtener una precisión total, puede hacer la búsqueda de matriz como primer paso, y para intervalos de matriz que corresponden a varios lados, haga una búsqueda allí.
aaz
1

Hay muchas formas de generar un número entero aleatorio con una distribución personalizada (también conocida como distribución discreta ). La elección depende de muchas cosas, incluido el número de números enteros para elegir, la forma de la distribución y si la distribución cambiará con el tiempo.

Una de las formas más simples de elegir un número entero con una función de peso personalizada f(x)es el método de muestreo de rechazo . Lo siguiente supone que el valor más alto posible de fes max. La complejidad del tiempo para el muestreo de rechazo es constante en promedio, pero depende en gran medida de la forma de la distribución y tiene el peor de los casos de ejecución para siempre. Para elegir un número entero en [1, k] usando el muestreo de rechazo:

  1. Elija un entero aleatorio uniforme ien [1,k ].
  2. Con probabilidad f(i)/max, regreso i. De lo contrario, vaya al paso 1.

Otros algoritmos tienen un tiempo de muestreo promedio que no depende en gran medida de la distribución (generalmente constante o logarítmica), pero a menudo requieren que precalcule los pesos en un paso de configuración y los almacene en una estructura de datos. Algunos de ellos también son económicos en términos de la cantidad de bits aleatorios que usan en promedio. Muchos de estos algoritmos se introdujeron después de 2011, e incluyen:

  • la estructura de datos sucinta de Bringmann – Larsen ("Muestreo sucinto de distribuciones discretas", 2012),
  • La búsqueda multinivel de Yunpeng Tang ("Un estudio empírico de métodos de muestreo aleatorio para cambiar distribuciones discretas", 2019), y
  • el Dice Roller rápida Cargado (2020).

Otros algoritmos incluyen el método de alias (ya mencionado en su artículo), el algoritmo Knuth-Yao, la estructura de datos MVN y más. Consulte mi sección " Una nota sobre algoritmos de elección ponderada " para una encuesta.

Peter O.
fuente