Calcular uma probabilidade exata e rapidamente

10

[Esta é uma pergunta de parceiro para calcular exatamente uma probabilidade ]

Esta tarefa é sobre escrever código para calcular uma probabilidade exata e rapidamente . A saída deve ser uma probabilidade precisa escrita como uma fração em sua forma mais reduzida. Ou seja, nunca deve produzir, 4/8mas sim 1/2.

Para um número inteiro positivo n, considere uma sequência uniformemente aleatória de 1s e -1s de comprimento ne chame-a de A. Agora concatene Aseu primeiro valor. Ou seja, A[1] = A[n+1]se a indexação de 1. Aagora tiver comprimento n+1. Agora considere também uma segunda sequência aleatória de comprimento ncujos primeiros nvalores sejam -1, 0 ou 1 com probabilidade 1 / 4,1 / 2, 1/4 cada e chame-a de B.

Agora considere o produto interno de A[1,...,n]e Be o produto interno de A[2,...,n+1]e B.

Por exemplo, considere n=3. Valores possíveis para Ae Bpoderiam ser A = [-1,1,1,-1]e B=[0,1,-1]. Nesse caso, os dois produtos internos são 0e 2.

Seu código deve gerar a probabilidade de que ambos os produtos internos sejam zero.

Copiando a tabela produzida por Martin Büttner, temos os seguintes resultados de amostra.

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

Línguas e bibliotecas

Você pode usar qualquer idioma e bibliotecas disponíveis gratuitamente que desejar. Devo ser capaz de executar seu código, portanto, inclua uma explicação completa de como executar / compilar seu código no Linux, se possível.

A tarefa

Seu código deve começar com n=1e fornecer a saída correta para cada n crescente em uma linha separada. Deve parar após 10 segundos.

A pontuação

A pontuação é simplesmente a mais alta natingida antes que seu código pare após 10 segundos quando executado no meu computador. Se houver um empate, o vencedor será o vencedor mais rápido.

Tabela de entradas

  • n = 64em Python . Versão 1 por Mitch Schwartz
  • n = 106em Python . Versão 11 de junho de 2015 por Mitch Schwartz
  • n = 151em C ++ . Resposta de Port of Mitch Schwartz por kirbyfan64sos
  • n = 165em Python . Versão 11 de junho de 2015, a versão "poda" de Mitch Schwartz com N_MAX = 165.
  • n = 945em Python por Min_25 usando uma fórmula exata. Surpreendente!
  • n = 1228em Python, por Mitch Schwartz, usando outra fórmula exata (com base na resposta anterior de Min_25).
  • n = 2761em Python por Mitch Schwartz usando uma implementação mais rápida da mesma fórmula exata.
  • n = 3250em Python usando Pypy por Mitch Schwartz usando a mesma implementação. Essa pontuação precisa pypy MitchSchwartz-faster.py |tailevitar a rolagem do console.
Comunidade
fonte
Gostaria de saber se uma solução numpy iria correr mais rápido que o Boost C ++?
QWR
@qwr Acho que numpy, numba e cython seriam todos interessantes, pois mantêm a família Python.
2
Gostaria muito de ver mais desses problemas de código mais rápido
Qwr
@qwr é o meu tipo favorito de pergunta ... Obrigado! O desafio é encontrar um que não envolva apenas a codificação exatamente do mesmo algoritmo na linguagem de nível mais baixo que você pode encontrar.
Você está gravando os resultados no console ou em um arquivo? Usar pypy e gravar em um arquivo parece o mais rápido para mim. O console retarda o processo consideravelmente.
gnibbler

Respostas:

24

Pitão

Uma fórmula de forma fechada p(n)é

insira a descrição da imagem aqui

Uma função geradora exponencial de p(n)é

insira a descrição da imagem aqui

onde I_0(x)é a função de Bessel modificada do primeiro tipo.

Edite em 11/06/2015:
- atualizou o código Python.

Editar em 13/06/2015:
- adicionou uma prova da fórmula acima.
- consertou o time_limit.
- adicionado um código PARI / GP.

Pitão

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!;

Prova:
Esse problema é semelhante a um problema de caminhada aleatória bidimensional (restrita).

Se A[i] = A[i+1], podemos passar de (x, y)para (x+1, y+1)[1 via], (x, y)[2 vias] ou (x-1, y-1)[1 via].

Se A[i] != A[i+1], podemos passar de (x, y)para (x-1, y+1)[1 via], (x, y)[2 vias] ou (x+1, y-1)[1 via].

Vamos a(n, m) = [x^m]((x+1)^n + (x-1)^n), b(n) = [x^n](1+x)^{2n}e c(n)ser o número de maneiras de se mover a partir (0, 0)de (0, 0)com npassos.

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

Desde então p(n) = c(n) / 8^n, podemos obter a fórmula de formulário fechado acima.

Min_25
fonte
11
Isso é ... bem ... incrível! Como diabos você calculou a fórmula exata?
11
Uau! O formulário fechado é sempre limpo!
QWR
11
@Embembik: eu adicionei uma prova (áspera).
Min_25
11
@qwr: Obrigado. Eu também acho !
Min_25
11
@ mbomb007: Sim. Mas, é uma tarefa de implementação e não uma tarefa de computação. Portanto, eu não o codificaria em C ++.
Min_25
9

Pitão

Nota: Parabéns a Min_25 por encontrar uma solução em formato fechado!

Obrigado pelo problema interessante! Ele pode ser resolvido usando o DP, embora atualmente não esteja me sentindo muito motivado para otimizar a velocidade para obter uma pontuação mais alta. Poderia ser bom para o golfe.

O código chegou N=39em 10 segundos neste laptop antigo executando o 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): aé o primeiro elemento de A, bé o último elemento de B, sé o produto interno de A[:-1]e B, e té o produto interno de A[1:-1]e B[:-1], usando a notação de fatia do Python. Meu código não armazena as matrizes Aou em Bqualquer lugar, então eu uso essas letras para me referir aos próximos elementos a serem adicionados Ae B, respectivamente. Essa opção de nomeação de variáveis ​​torna a explicação um pouco estranha, mas permite uma boa aparência A*b+a*Bno próprio código. Observe que o elemento que está sendo adicionado Aé o penúltimo, já que o último elemento é sempre o mesmo que o primeiro. Eu usei o truque de Martin Büttner de incluir 0duas vezesBcandidatos para obter a distribuição de probabilidade adequada. O dicionário X(que é nomeado Ypara N+1) mantém o controle da contagem de todas as matrizes possíveis de acordo com o valor da tupla. As variáveis ne drepresentam numerador e denominador, e foi por isso que renomeei a ndeclaração do problema como N.

A parte principal da lógica é que você pode atualizar de Npara N+1usando apenas os valores na tupla. Os dois produtos internos especificados na pergunta são dados por s+A*Be t+A*b+a*B. Isso fica claro se você examinar um pouco as definições; observe que [A,a]e [b,B]são os dois últimos elementos das matrizes Ae Brespectivamente.

Observe que se tsão pequenos e delimitados de acordo com N, e para uma rápida implementação em uma linguagem rápida, poderíamos evitar dicionários em favor de matrizes.

Pode ser possível tirar proveito da simetria considerando valores que diferem apenas no sinal; Eu não olhei para isso.

Observação 1 : o tamanho do dicionário cresce quadraticamente N, em que tamanho significa número de pares de valores-chave.

Observação 2 : se definirmos um limite superior N, podemos remover tuplas para as quais N_MAX - N <= |s|e da mesma forma para t. Isso pode ser feito especificando um estado de absorção ou implicitamente com uma variável para conter a contagem de estados removidos (que precisariam ser multiplicados por 8 a cada iteração).

Atualização : Esta versão é mais 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()

Otimizações implementadas:

  • colocar tudo main()- o acesso variável local é mais rápido que o global
  • manipular N=1explicitamente para evitar a verificação (1,-1) if N else [a](que impõe que o primeiro elemento da tupla seja consistente ao adicionar elementos a Apartir da lista vazia)
  • desenrole os circuitos internos, o que também elimina a multiplicação
  • dobrar a contagem cpara adicionar um 0a em Bvez de fazer essas operações duas vezes
  • o denominador é sempre, 8^Nentão não precisamos acompanhá-lo
  • Agora, considerando a simetria: podemos fixar o primeiro elemento de Aas 1e dividir o denominador por 2, uma vez que os pares válidos (A,B)com A[1]=1e os com A[1]=-1podem ser colocados em uma correspondência individual negando A. Da mesma forma, podemos corrigir o primeiro elemento Bcomo não negativo.
  • agora com poda. Você precisaria mexer N_MAXpara ver qual pontuação ele pode obter na sua máquina. Pode ser reescrito para encontrar um apropriado N_MAXautomaticamente pela pesquisa binária, mas parece desnecessário? Nota: Não precisamos verificar a condição de poda até chegarmos ao redor N_MAX / 2, para que possamos acelerar um pouco iterando em duas fases, mas decidi não simplificar e limpar o código.
Mitch Schwartz
fonte
11
Esta é realmente uma ótima resposta! Você poderia explicar o que fez na sua velocidade?
@Lembik Obrigado :) Adicionada uma explicação, além de outra pequena otimização, e a compatibilidade com o Python3.
Mitch Schwartz
No meu computador, peguei N=57a primeira versão e N=75a segunda.
kirbyfan64sos
Suas respostas foram incríveis. É apenas que a resposta de Min_25 foi ainda mais :)
5

Pitão

Usando a ideia de caminhada aleatória de Min_25, consegui chegar a uma 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 \ texto {ímpar} \ \ frac {\ binom {n} {n / 2} ^ 2 + \ sum _ {i = 0} ^ {\ piso n / 2 \ andar} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {even} \ \ end {cases}

Aqui está uma implementação Python baseada em 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()

Explicação / prova:

Primeiro, resolvemos um problema de contagem relacionado, onde permitimos A[n+1] = -A[1]; isto é, o elemento adicional concatenado para Apode ser 1ou -1independentemente do primeiro elemento. Portanto, não precisamos acompanhar quantas vezes A[i] = A[i+1]ocorre. Temos o seguinte passeio aleatório:

De (x,y)podemos mudar para (x+1,y+1)[1 caminho], (x+1,y-1)[1 caminho], (x-1,y+1)[1 caminho], (x-1,y-1)[1 caminho], (x,y)[4 caminhos]

onde xe yrepresentam os dois produtos do ponto, e contamos o número de maneiras para se deslocar de (0,0)que (0,0)em netapas. Essa contagem seria então multiplicada 2para levar em conta o fato de que Apode começar com 1ou -1.

Nós nos referimos a permanecer (x,y)como um movimento zero .

Nós iteramos sobre o número de movimentos diferentes de zero i, que precisam ser uniformes para voltarmos (0,0). Os movimentos horizontais e verticais compõem dois passeios aleatórios unidimensionais independentes, que podem ser contados C(i,i/2)^2, onde C(n,k)é o coeficiente binomial. (Para uma caminhada com os kpassos à esquerda e os kpassos à direita, existem C(2k,k)maneiras de escolher a ordem dos passos.) Além disso, existem C(n,i)maneiras de colocar os movimentos e 4^(n-i)maneiras de escolher os movimentos zero. Então temos:

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

Agora, precisamos voltar ao problema original. Defina um par permitido (A,B)para ser conversível se Bcontiver um zero. Defina um par (A,B)como quase permitido se A[n+1] = -A[1]e os dois produtos de ponto forem zero.

Lema: Para um dado n, os pares quase permitidos estão em correspondência individual com os pares conversíveis.

Podemos (reversivelmente) converter um par conversível (A,B)em um par quase permitido (A',B')negando A[m+1:]e B[m+1:], onde mestá o índice do último zero em B. A verificação para isso é simples: se o último elemento de Bfor zero, não precisamos fazer nada. Caso contrário, quando negamos o último elemento de A, podemos negar o último elemento de Bpara preservar o último termo do produto de ponto alterado. Mas isso nega o último valor do produto de ponto sem deslocamento, portanto, corrigimos isso negando o penúltimo elemento de A. Mas isso gera o penúltimo valor do produto deslocado, de modo que negamos o penúltimo elemento de B. E assim por diante, até atingir um elemento zero B.

Agora, apenas precisamos mostrar que não há pares quase permitidos para os quais Bnão contém zero. Para um produto escalar ser igual a zero, precisamos ter um número igual 1e -1termos para cancelar. Cada -1termo é composto por (1,-1)ou (-1,1). Portanto, a paridade do número -1que ocorre é fixada de acordo com n. Se o primeiro e o último elementos de Atêm sinais diferentes, mudamos a paridade, então isso é impossível.

Então nós temos

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 fornece a fórmula acima (reindexação com i' = i/2).

Atualização: Aqui está uma versão mais rápida usando a mesma 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()

Otimizações implementadas:

  • função embutida p(n)
  • recorrência para coeficientes binomiais C(n,k)comk <= n/2
  • use recorrência para coeficientes binomiais centrais
Mitch Schwartz
fonte
Só para você saber, p(n)não precisa ser uma função por partes. Em geral, se f(n) == {g(n) : n is odd; h(n) : n is even}você pode escrever f(n) == (n-2*floor(n/2))*g(n) + ((n+1)-2*(floor((n+1)/2)))*h(n)ou usar em n mod 2vez de (n-2*floor(n/2)). Veja aqui
mbomb007
11
@ mbomb007 Isso é óbvio e desinteressante.
Mitch Schwartz
3

Explicação da fórmula de Min_25

Min_25 postou uma ótima prova, mas demorou um pouco para seguir. Esta é uma explicação para preencher as entrelinhas.

a (n, m) representa o número de maneiras de escolher A, de modo que A [i] = A [i + 1] m vezes. A fórmula para a (n, m) é equivalente a a (n, m) = {2 * (n escolha m) para nm par; 0 para nm ímpar.} Somente uma paridade é permitida porque A [i]! = A [i + 1] deve ocorrer um número par de vezes para que A [0] = A [n]. O fator 2 é devido à escolha inicial A [0] = 1 ou A [0] = -1.

Uma vez que o número de (A [i]! = A [i + 1]) é fixado para ser q (denominado i na fórmula c (n)), ele se separa em duas caminhadas aleatórias 1D de comprimento q e nq. b (m) é o número de maneiras de fazer uma caminhada aleatória unidimensional de m etapas que termina no mesmo local em que começou e tem 25% de chance de se mover para a esquerda, 50% de chance de ficar parado e 25% de chance de movendo para a direita. Uma maneira mais óbvia de indicar a função geradora é [x ^ m] (1 + 2x + x ^ 2) ^ n, onde 1, 2x e x ^ 2 representam esquerda, sem movimento e direita, respectivamente. Mas então 1 + 2x + x ^ 2 = (x + 1) ^ 2.

feersum
fonte
Mais uma razão para amar o PPCG! Obrigado.
2

C ++

Apenas uma porta da (excelente) resposta em Python de Mitch Schwartz. A principal diferença é que eu costumava 2representar -1para a avariável e fazia algo semelhante b, o que me permitia usar uma matriz. Usando o Intel C ++ com -O3, eu consegui N=141! Minha primeira versão chegou N=140.

Isso usa o Boost. Eu tentei uma versão paralela, mas tive alguns 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
fonte
Isso precisa g++ -O3 kirbyfan64sos.cpp -o kirbyfan64sos -lboost_system -lboost_timer -lboost_chrono -lrt -lgmpcompilar. (Graças a aditsu.)