Calcule una probabilidad exacta y rápidamente

10

[Esta es una pregunta de socio para calcular una probabilidad exactamente ]

Esta tarea consiste en escribir código para calcular una probabilidad de manera exacta y rápida . El resultado debe ser una probabilidad precisa escrita como una fracción en su forma más reducida. Es decir, nunca debería salir, 4/8sino más bien 1/2.

Para algún número entero positivo n, considere una cadena uniformemente aleatoria de 1s y -1s de longitud ny llámela A. Ahora concatene a Asu primer valor. Es decir, A[1] = A[n+1]si la indexación desde 1. Aahora tiene longitud n+1. Ahora también considere una segunda cadena aleatoria de longitud ncuyos primeros nvalores son -1, 0 o 1 con probabilidad 1 / 4,1 / 2, 1/4 cada uno y llámelo B.

Consideremos ahora el producto interno de A[1,...,n]y By el producto interno de A[2,...,n+1]y B.

Por ejemplo, considere n=3. Los posibles valores de Ay Bpodrían ser A = [-1,1,1,-1]y B=[0,1,-1]. En este caso, los dos productos internos son 0y 2.

Su código debe generar la probabilidad de que ambos productos internos sean cero.

Copiando la tabla producida por Martin Büttner tenemos los siguientes resultados de muestra.

n   P(n)
1   1/2
2   3/8
3   7/32
4   89/512
5   269/2048
6   903/8192
7   3035/32768
8   169801/2097152

Idiomas y bibliotecas

Puede utilizar cualquier idioma y bibliotecas disponibles gratuitamente que desee. Debo poder ejecutar su código, así que incluya una explicación completa sobre cómo ejecutar / compilar su código en Linux si es posible.

La tarea

Su código debe comenzar con n=1y dar la salida correcta para cada n creciente en una línea separada. Debería detenerse después de 10 segundos.

El marcador

El puntaje es simplemente el más alto nalcanzado antes de que su código se detenga después de 10 segundos cuando se ejecuta en mi computadora. Si hay un empate, el ganador será el que obtenga el puntaje más alto más rápido.

Tabla de entradas

  • n = 64en Python . Versión 1 por Mitch Schwartz
  • n = 106en Python . Versión del 11 de junio de 2015 por Mitch Schwartz
  • n = 151en C ++ . La respuesta del puerto de Mitch Schwartz por kirbyfan64sos
  • n = 165en Python . Versión del 11 de junio de 2015, la versión de "poda" de Mitch Schwartz con N_MAX = 165.
  • n = 945en Python por Min_25 usando una fórmula exacta. ¡Asombroso!
  • n = 1228en Python por Mitch Schwartz usando otra fórmula exacta (basada en la respuesta anterior de Min_25).
  • n = 2761en Python por Mitch Schwartz usando una implementación más rápida de la misma fórmula exacta.
  • n = 3250en Python usando Pypy por Mitch Schwartz usando la misma implementación. Esta puntuación debe pypy MitchSchwartz-faster.py |tailevitar la sobrecarga de desplazamiento de la consola.
Comunidad
fuente
Me pregunto si una solución complicada funcionaría más rápido que Boost C ++.
qwr
@qwr Creo que numpy, numba y cython serían interesantes ya que se mantienen dentro de la familia Python.
2
Me encantaría ver más de estos problemas de código más rápidos
qwr
@qwr es mi tipo de pregunta favorita ... ¡Gracias! El desafío es encontrar uno que no solo implique codificar exactamente el mismo algoritmo en el lenguaje de nivel más bajo que pueda encontrar.
¿Estás escribiendo los resultados en la consola o en un archivo? Usar pypy y escribir en un archivo me parece el más rápido. La consola ralentiza el proceso considerablemente.
gnibbler

Respuestas:

24

Pitón

Una fórmula de forma cerrada de p(n)es

ingrese la descripción de la imagen aquí

Una función generadora exponencial de p(n)es

ingrese la descripción de la imagen aquí

donde I_0(x)está la función Bessel modificada del primer tipo.

Editar en 2015-06-11:
- actualizado el código de Python.

Editar en 2015-06-13:
- se agregó una prueba de la fórmula anterior.
- arreglado el time_limit.
- Se agregó un código PARI / GP.

Pitón

def solve():
  # straightforward implementation

  from time import time
  from itertools import count

  def binom(n, r):
    return facts[n] // (facts[r] * facts[n - r])

  def p(N):
    ans = 0
    for i in range(1 + N // 2):
      t = binom(2 * (N - 2 * i), N - 2 * i)
      t *= binom(N, 2 * i)
      t *= binom(4 * i, 2 * i)
      ans += t
    e = (ans & -ans).bit_length() - 1
    numer = ans >> e
    denom = 1 << (3 * N - 1 - e)
    return numer, denom

  facts = [1]
  time_limit = 10.0 + time()

  for i in count(1):
    facts.append(facts[-1] * (2 * i - 1))
    facts.append(facts[-1] * (2 * i))

    n, d = p(i)

    if time() > time_limit:
      break

    print("%d %d/%d" % (i, n, d))

solve()

PARI / GP

p(n) = polcoeff( (exp(x/2) + 1) * besseli(0, x/4) ^ 2, n) * n!;

Prueba:
este problema es similar a un problema de caminata aleatoria bidimensional (restringida).

Si A[i] = A[i+1], podemos pasar de (x, y)a (x+1, y+1)[1 manera], (x, y)[2] o formas (x-1, y-1)[1 manera].

Si A[i] != A[i+1], podemos pasar de (x, y)a (x-1, y+1)[1 manera], (x, y)[2] o formas (x+1, y-1)[1 manera].

Deje a(n, m) = [x^m]((x+1)^n + (x-1)^n), b(n) = [x^n](1+x)^{2n}y c(n)sea ​​la cantidad de formas de moverse de (0, 0)a (0, 0)con npasos.

Entonces, c(n) = \sum_{i=0}^n a(n, i) * b(i) * b(n-i).

Desde entonces p(n) = c(n) / 8^n, podemos obtener la fórmula de forma cerrada anterior.

Min_25
fuente
1
Esto es ... bueno ... ¡increíble! ¿Cómo demonios calculaste la fórmula exacta?
1
¡Guauu! ¡La forma cerrada siempre está ordenada!
qwr
1
@Lembik: agregué una prueba (aproximada).
Min_25
1
@qwr: Gracias. ¡Yo también lo creo!
Min_25
1
@ mbomb007: Sí. Pero, es una tarea de implementación en lugar de una tarea informática. Por lo tanto, no lo codificaría en C ++.
Min_25
9

Pitón

Nota: ¡ Felicitaciones a Min_25 por encontrar una solución de forma cerrada!

Gracias por el interesante problema! Se puede resolver con DP, aunque actualmente no me siento muy motivado para optimizar la velocidad para obtener una puntuación más alta. Podría ser bueno para el golf.

El código llegó N=39en 10 segundos en esta vieja computadora portátil con Python 2.7.5.

from time import*
from fractions import*
from collections import*

X={(1,0,0,0):1,(-1,0,0,0):1}

T=time()
N=0

while 1:
    Y=defaultdict(lambda:0)
    n=d=0
    for a,b,s,t in X:
        c=X[(a,b,s,t)]
        for A in ( (1,-1) if N else [a] ):
            for B in 1,0,0,-1:
                n+=c*(s+A*B==0==t+A*b+a*B)
                d+=c
                Y[(a,B,s+A*B,t+A*b)]+=c
    if time()>T+10: break
    N+=1
    print N,Fraction(n,d)
    X=Y

Para tupla (a,b,s,t): aes el primer elemento de A, bes el último elemento de B, ses el producto interno de A[:-1]y B, y tes el producto interno de A[1:-1]y B[:-1], utilizando la notación de corte de Python. Mi código no almacena las matrices Ani en Bninguna parte, por lo que uso esas letras para referirme a los siguientes elementos que se agregarán Ay B, respectivamente. Esta elección de nombres variables hace que la explicación sea un poco incómoda, pero permite una apariencia agradable A*b+a*Ben el código mismo. Tenga en cuenta que el elemento que se agrega Aes el penúltimo, ya que el último elemento siempre es el mismo que el primero. He usado el truco de Martin Büttner de incluir 0dos veces enBcandidatos para obtener la distribución de probabilidad adecuada. El diccionario X(que lleva el nombre Yde N+1) realiza un seguimiento del recuento de todos los arreglos posibles de acuerdo con el valor de la tupla. Las variables ny drepresentan numerador y denominador, es por eso que cambié el nombre nde la declaración del problema como N.

La parte clave de la lógica es que se puede actualizar a partir Nde N+1utilizar sólo los valores de la tupla. Los dos productos internos especificados en la pregunta están dados por s+A*By t+A*b+a*B. Esto está claro si examina un poco las definiciones; tenga en cuenta que [A,a]y [b,B]son los dos últimos elementos de las matrices Ay Brespectivamente.

Tenga en cuenta que sy tson pequeños y acotados de acuerdo con N, y para una implementación rápida en un lenguaje rápido, podríamos evitar los diccionarios a favor de las matrices.

Es posible aprovechar la simetría considerando valores que difieren solo en el signo; No he investigado eso.

Observación 1 : El tamaño del diccionario crece cuadráticamente N, donde tamaño significa número de pares clave-valor.

Observación 2 : Si establecemos un límite superior N, podemos podar tuplas para las cuales N_MAX - N <= |s|y de manera similar para t. Esto podría hacerse especificando un estado absorbente, o implícitamente con una variable para mantener el recuento de estados podados (que tendrían que multiplicarse por 8 en cada iteración).

Actualización : esta versión es más rápida:

from time import*
from fractions import*
from collections import*

N_MAX=115

def main():
    T=time()

    N=1
    Y={(1,0,0,0):1,(1,1,1,0):1}
    n=1
    thresh=N_MAX

    while time() <= T+10:
        print('%d %s'%(N,Fraction(n,8**N/4)))

        N+=1
        X=Y
        Y=defaultdict(lambda:0)
        n=0

        if thresh<2:
            print('reached MAX_N with %.2f seconds remaining'%(T+10-time()))
            return

        for a,b,s,t in X:
            if not abs(s)<thresh>=abs(t):
                continue

            c=X[(a,b,s,t)]

            # 1,1

            if not s+1 and not t+b+a: n+=c
            Y[(a,1,s+1,t+b)]+=c

            # -1,1

            if not s-1 and not t-b+a: n+=c
            Y[(a,1,s-1,t-b)]+=c

            # 1,-1

            if not s-1 and not t+b-a: n+=c
            Y[(a,-1,s-1,t+b)]+=c

            # -1,-1

            if not s+1 and not t-b-a: n+=c
            Y[(a,-1,s+1,t-b)]+=c

            # 1,0

            c+=c

            if not s and not t+b: n+=c
            Y[(a,0,s,t+b)]+=c

            # -1,0

            if not s and not t-b: n+=c
            Y[(a,0,s,t-b)]+=c

        thresh-=1

main()

Optimizaciones implementadas:

  • poner todo en main()- el acceso variable local es más rápido que el global
  • manejar N=1explícitamente para evitar la verificación (1,-1) if N else [a](lo que obliga a que el primer elemento de la tupla sea consistente al agregar elementos al Acomenzar desde la lista vacía)
  • desenrollar los bucles internos, lo que también elimina la multiplicación
  • duplicar el conteo cpara agregar un 0a en Blugar de hacer esas operaciones dos veces
  • el denominador es siempre, 8^Npor lo que no necesitamos hacer un seguimiento
  • ahora teniendo en cuenta la simetría: podemos fijar el primer elemento de Aas 1y dividir el denominador entre 2, ya que los pares válidos (A,B)con A[1]=1y aquellos con A[1]=-1pueden ser puestos en correspondencia uno a uno mediante la negación A. Del mismo modo, podemos arreglar el primer elemento de Bcomo no negativo.
  • ahora con poda. Tendría que jugar N_MAXpara ver qué puntaje puede obtener en su máquina. Podría reescribirse para encontrar un apropiado N_MAXautomáticamente mediante búsqueda binaria, pero parece innecesario? Nota: No es necesario que verifiquemos la condición de poda hasta alcanzar el límite N_MAX / 2, por lo que podríamos acelerar un poco iterando en dos fases, pero decidí no hacerlo por simplicidad y limpieza del código.
Mitch Schwartz
fuente
1
¡Esta es una gran respuesta! ¿Podrías explicar lo que hiciste en tu velocidad?
@Lembik Gracias :) Agregó una explicación, además de otra pequeña optimización, y lo hizo compatible con Python3.
Mitch Schwartz
En mi computadora, obtuve N=57la primera versión y N=75la segunda.
kirbyfan64sos
Tus respuestas han sido asombrosas. Es sólo que la respuesta de Min_25 era aún más :)
5

Pitón

Usando la idea de Min_25 de caminata aleatoria, pude llegar a una fórmula diferente:

p (n) = \ begin {cases} \ frac {\ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {impar} \ \ frac {\ binom {n} {n / 2} ^ 2 + \ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {even} \ \ end {cases}

Aquí hay una implementación de Python basada en Min_25:

from time import*
from itertools import*

def main():
    def binom(n, k):
        return facts[n]/(facts[k]*facts[n-k])

    def p(n):
        numer=0
        for i in range(n/2+1):
            t=binom(2*i,i)
            t*=t
            t*=binom(n,2*i)
            t<<=2*(n-2*i)
            numer+=t
        if not n&1:
            numer+=t
        e=(numer&-numer).bit_length()-1
        numer>>=e
        denom=1<<(3*n-e)
        return numer, denom

    facts=[1]
    time_limit=time()+10

    for i in count(1):
        facts.append(facts[-1]*i)

        n,d=p(i)

        if time()>time_limit:
            break

        print("%d %d/%d"%(i,n,d))

main()

Explicación / prueba:

Primero resolvemos un problema de conteo relacionado, donde permitimos A[n+1] = -A[1]; es decir, el elemento adicional concatenado Apuede ser 1o -1independientemente del primer elemento. Por lo tanto, no necesitamos hacer un seguimiento de cuántas veces A[i] = A[i+1]ocurre. Tenemos la siguiente caminata aleatoria:

Desde (x,y)podemos pasar a (x+1,y+1)[1 vía], (x+1,y-1)[1 vía], (x-1,y+1)[1 vía], (x-1,y-1)[1 vía], (x,y)[4 vías]

donde xy ysoporte para los dos productos escalares, y estamos contando el número de maneras de pasar de (0,0)que (0,0)en nlos pasos. Ese recuento se multiplicaría por 2para tener en cuenta el hecho de que Apuede comenzar con 1o -1.

Nos referimos a permanecer en (x,y)un movimiento cero .

Repetimos el número de movimientos ique no son cero , lo que tiene que ser uniforme para volver a hacerlo (0,0). Los movimientos horizontales y verticales forman dos caminatas aleatorias unidimensionales independientes, que pueden contarse C(i,i/2)^2, donde C(n,k)es el coeficiente binomial. (Para una caminata con kpasos hacia la izquierda y hacia la kderecha, hay C(2k,k)formas de elegir el orden de los pasos). Además, hay C(n,i)formas de colocar los movimientos y 4^(n-i)formas de elegir los movimientos cero. Entonces obtenemos:

a(n) = 2 * sum_{i in (0,2,4,...,n)} C(i/2,i)^2 * C(n,i) * 4^(n-i)

Ahora, tenemos que volver al problema original. Defina un par permitido (A,B)para que sea convertible si Bcontiene un cero. Defina un par (A,B)para que sea casi permisible si A[n+1] = -A[1]los dos productos de punto son cero.

Lema: Para un hecho n, los pares casi permitidos están en correspondencia uno a uno con los pares convertibles.

Podemos (reversiblemente) convertir un par convertible (A,B)en un par casi permisible (A',B')al negar A[m+1:]y B[m+1:]dónde mestá el índice del último cero B. La comprobación de esto es sencilla: si el último elemento de Bes cero, no necesitamos hacer nada. De lo contrario, cuando negamos el último elemento de A, podemos negar el último elemento de Bpara preservar el último término del producto de puntos desplazados. Pero esto niega el último valor del producto de punto no desplazado, por lo que solucionamos esto al negar el penúltimo elemento de A. Pero luego esto arroja el penúltimo valor del producto desplazado, por lo que negamos el penúltimo elemento de B. Y así sucesivamente, hasta llegar a un elemento cero en B.

Ahora, solo tenemos que mostrar que no hay pares casi permitidos para los que Bno contenga un cero. Para un producto de punto a igual a cero, tenemos que tener un número igual de 1y -1términos a cancelarse. Cada -1término se compone de (1,-1)o (-1,1). Por lo tanto, la paridad del número de -1que se produce se fija de acuerdo con n. Si el primer y el último elemento de Atienen signos diferentes, cambiamos la paridad, por lo que esto es imposible.

Entonces obtenemos

c(n) = a(n)/2 if n is odd, else a(n)/2 + C(n,n/2)^2

p(n) = c(n) / 8^n

que da la fórmula anterior (reindexando con i' = i/2).

Actualización: Aquí hay una versión más rápida que usa la misma fórmula:

from time import*
from itertools import*

def main():
    time_limit=time()+10

    binoms=[1]
    cb2s=[1]
    cb=1

    for n in count(1):
        if n&1:
            binoms=[a+b for a,b in zip([0]+binoms,binoms)]
        else:
            binoms=[a+b for a,b in zip([0]+binoms,binoms+[binoms[-1]])]
            cb=(cb<<2)-(cb+cb)/(n/2)
            cb2s.append(cb*cb)

        numer=0
        for i in xrange(n/2+1):
            t=cb2s[i]*binoms[min(2*i,n-2*i)]
            t<<=2*(n-2*i)
            numer+=t
        if not n&1:
            numer+=t
        e=(numer&-numer).bit_length()-1
        numer>>=e
        denom=1<<(3*n-e)

        if time()>time_limit:
            break

        print("%d %d/%d"%(n,numer,denom))

main()

Optimizaciones implementadas:

  • función en línea p(n)
  • usar recurrencia para coeficientes binomiales C(n,k)conk <= n/2
  • usar recurrencia para coeficientes binomiales centrales
Mitch Schwartz
fuente
Para que lo sepas, p(n)no es necesario que sea una función por partes. En general, si f(n) == {g(n) : n is odd; h(n) : n is even}puede escribir f(n) == (n-2*floor(n/2))*g(n) + ((n+1)-2*(floor((n+1)/2)))*h(n)o usar en n mod 2lugar de (n-2*floor(n/2)). Ver aquí
mbomb007
1
@ mbomb007 Eso es obvio y poco interesante.
Mitch Schwartz
3

Explicación de la fórmula de Min_25

Min_25 publicó una gran prueba, pero tardó un tiempo en seguirla. Esta es una pequeña explicación para completar entre las líneas.

a (n, m) representa el número de formas de elegir A de manera que A [i] = A [i + 1] m veces. La fórmula para a (n, m) es equivalente a a (n, m) = {2 * (n elija m) para nm par; 0 para nm impar.} Solo se permite una paridad porque A [i]! = A [i + 1] debe ocurrir un número par de veces para que A [0] = A [n]. El factor 2 se debe a la elección inicial A [0] = 1 o A [0] = -1.

Una vez que el número de (A [i]! = A [i + 1]) se fija en q (denominado i en la fórmula c (n)), se separa en dos recorridos aleatorios 1D de longitud q y nq. b (m) es la cantidad de formas de realizar una caminata aleatoria unidimensional de m pasos que termina en el mismo lugar donde comenzó, y tiene un 25% de posibilidades de moverse hacia la izquierda, un 50% de posibilidades de quedarse quieto y un 25% de posibilidades de moviéndose a la derecha Una forma más obvia de establecer la función generadora es [x ^ m] (1 + 2x + x ^ 2) ^ n, donde 1, 2x y x ^ 2 representan izquierda, sin movimiento y derecha, respectivamente. Pero entonces 1 + 2x + x ^ 2 = (x + 1) ^ 2.

Feersum
fuente
¡Otra razón más para amar PPCG! Gracias.
2

C ++

Solo un puerto de la (excelente) respuesta de Python de Mitch Schwartz. La principal diferencia es que solía 2representar -1para la avariable e hice algo similar b, lo que me permitió usar una matriz. Usando Intel C ++ con -O3, lo tengo N=141! Mi primera versión tiene N=140.

Esto usa Boost. Probé una versión paralela pero me encontré con algunos problemas.

#include <boost/multiprecision/gmp.hpp>
#include <boost/typeof/typeof.hpp>
#include <boost/rational.hpp>
#include <boost/chrono.hpp>
#include <boost/array.hpp>
#include <iostream>
#include <utility>
#include <map>

typedef boost::multiprecision::mpz_int integer;
typedef boost::array<boost::array<std::map<int, std::map<int, integer> >, 3>, 2> array;
typedef boost::rational<integer> rational;

int main() {
    BOOST_AUTO(T, boost::chrono::high_resolution_clock::now());

    int N = 1;
    integer n = 1;
    array* Y = new array, *X = NULL;
    (*Y)[1][0][0][0] = 1;
    (*Y)[1][1][1][0] = 1;

    while (boost::chrono::high_resolution_clock::now() < T+boost::chrono::seconds(10)) {
        std::cout << N << " " << rational(n, boost::multiprecision::pow(integer(8), N)/4) << std::endl;
        ++N;
        delete X;
        X = Y;
        Y = new array;
        n = 0;

        for (int a=0; a<2; ++a)
            for (int b=0; b<3; ++b)
                for (BOOST_AUTO(s, (*X)[a][b].begin()); s != (*X)[a][b].end(); ++s)
                    for (BOOST_AUTO(t, s->second.begin()); t != s->second.end(); ++t) {
                        integer c = t->second;
                        int d = b&2 ? -1 : b, e = a == 0 ? -1 : a;

                        if (s->first == -1 && t->first+d+e == 0) n += c;
                        (*Y)[a][1][s->first+1][t->first+d] += c;

                        if (s->first == 1 && t->first-d+e == 0) n += c;
                        (*Y)[a][1][s->first-1][t->first-d] += c;

                        if (s->first == 1 && t->first+d-e == 0) n += c;
                        (*Y)[a][2][s->first-1][t->first+d] += c;

                        if (s->first == -1 && t->first-d-e == 0) n += c;
                        (*Y)[a][2][s->first+1][t->first-d] += c;

                        c *= 2;

                        if (s->first == 0 && t->first+d == 0) n += c;
                        (*Y)[a][0][s->first][t->first+d] += c;

                        if (s->first == 0 && t->first-d == 0) n += c;
                        (*Y)[a][0][s->first][t->first-d] += c;
                    }
    }

    delete X;
    delete Y;
}
kirbyfan64sos
fuente
Esto necesita g++ -O3 kirbyfan64sos.cpp -o kirbyfan64sos -lboost_system -lboost_timer -lboost_chrono -lrt -lgmpcompilarse. (Gracias a aditsu.)