Calcola una probabilità esattamente e rapidamente


10

[Questa è una domanda del partner per calcolare esattamente una probabilità ]

Questa attività riguarda la scrittura di codice per calcolare una probabilità esattamente e rapidamente . L'output dovrebbe essere una probabilità precisa scritta come una frazione nella sua forma più ridotta. Cioè non dovrebbe mai essere prodotto 4/8ma piuttosto 1/2.

Per alcuni numeri interi positivi n, considera una stringa uniformemente casuale di 1 e -1 secondi di lunghezza ne chiamala A. Ora concatena al Asuo primo valore. Cioè A[1] = A[n+1]se l'indicizzazione da 1. Aora ha lunghezza n+1. Ora considera anche una seconda stringa casuale di lunghezza i ncui primi nvalori sono -1, 0 o 1 con probabilità 1 / 4,1 / 2, 1/4 ciascuno e chiamalo B.

Consideriamo ora il prodotto interno A[1,...,n]e Bed il prodotto interno di A[2,...,n+1]e B.

Ad esempio, considera n=3. I valori possibili per Ae Bpotrebbero essere A = [-1,1,1,-1]e B=[0,1,-1]. In questo caso i due prodotti interni sono 0e 2.

Il codice deve generare la probabilità che entrambi i prodotti interni siano zero.

Copiando la tabella prodotta da Martin Büttner abbiamo i seguenti risultati di esempio.

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

Lingue e biblioteche

Puoi usare qualsiasi linguaggio e librerie liberamente disponibili che ti piacciono. Devo essere in grado di eseguire il tuo codice, quindi per favore includi una spiegazione completa su come eseguire / compilare il tuo codice in Linux, se possibile.

L'obiettivo

Il codice deve iniziare con n=1e fornire l'output corretto per ogni n crescente su una riga separata. Dovrebbe fermarsi dopo 10 secondi.

Il punteggio

Il punteggio è semplicemente il più alto nraggiunto prima che il codice si interrompa dopo 10 secondi quando viene eseguito sul mio computer. In caso di pareggio, il vincitore sarà quello che otterrà il punteggio più alto più rapidamente.

Tabella delle voci

  • n = 64in Python . Versione 1 di Mitch Schwartz
  • n = 106in Python . Versione dell'11 giugno 2015 di Mitch Schwartz
  • n = 151in C ++ . La risposta di Port of Mitch Schwartz di kirbyfan64sos
  • n = 165in Python . Versione dell'11 giugno 2015 la versione "potatura" di Mitch Schwartz con N_MAX = 165.
  • n = 945in Python di Min_25 usando una formula esatta. Sorprendente!
  • n = 1228in Python di Mitch Schwartz usando un'altra formula esatta (basata sulla risposta precedente di Min_25).
  • n = 2761in Python di Mitch Schwartz usando un'implementazione più veloce della stessa formula esatta.
  • n = 3250in Python usando Pypy di Mitch Schwartz usando la stessa implementazione. Questo punteggio deve pypy MitchSchwartz-faster.py |tailevitare lo scorrimento dall'alto della console.

Mi chiedo se una soluzione numpy funzionerebbe più velocemente di Boost C ++?
qwr

@qwr Penso che numpy, numba e cython sarebbero tutti interessanti mentre si mantengono all'interno della famiglia Python.

2
Mi piacerebbe vedere più di questi problemi con il codice più veloce
qwr l'

@qwr è la mia domanda preferita ... Grazie! La sfida è trovarne uno che non implichi semplicemente la codifica esattamente dello stesso algoritmo nella lingua di livello più basso che puoi trovare.

Stai scrivendo i risultati sulla console o su un file? Usare pypy e scrivere su un file mi sembra il più veloce. La console rallenta notevolmente il processo.
Gnibbler,

Risposte:


24

Pitone

Una formula in forma chiusa di p(n)è

inserisci qui la descrizione dell'immagine

Una funzione generatrice esponenziale di p(n)è

inserisci qui la descrizione dell'immagine

dove I_0(x)è la funzione di Bessel modificata del primo tipo.

Modifica l'11 / 2015/2015:
- aggiornato il codice Python.

Modifica il 13/06/2015:
- aggiunta una prova della formula sopra.
- risolto il problema time_limit.
- aggiunto un codice PARI / GP.

Pitone

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:
questo problema è simile a un problema di camminata casuale bidimensionale (limitato).

Se A[i] = A[i+1]possiamo spostarci da (x, y)a (x+1, y+1)[1 via], (x, y)[2 vie] o (x-1, y-1)[1 via].

Se A[i] != A[i+1]possiamo spostarci da (x, y)a (x-1, y+1)[1 via], (x, y)[2 vie] o (x+1, y-1)[1 via].

Lasciate a(n, m) = [x^m]((x+1)^n + (x-1)^n), b(n) = [x^n](1+x)^{2n}e c(n)il numero di modi per passare da (0, 0)a (0, 0)con ngradini.

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

Da allora p(n) = c(n) / 8^n, possiamo ottenere la formula in forma chiusa sopra.


1
Questo è ... beh .. fantastico! Come mai hai calcolato la formula esatta?

1
Wow! La forma chiusa è sempre pulita!
qwr

1
@Lembik: ho aggiunto una (approssimativa) prova.
Min_25,

1
@qwr: grazie. Lo penso anch'io !
Min_25

1
@ mbomb007: Sì. Ma è un'attività di implementazione piuttosto che un'attività di elaborazione. Quindi, non lo scriverei in C ++.
Min_25

9

Pitone

Nota: Congratulazioni a Min_25 per aver trovato una soluzione a forma chiusa!

Grazie per l'interessante problema! Può essere risolto usando DP, anche se al momento non mi sento molto motivato a ottimizzare la velocità per ottenere un punteggio più alto. Potrebbe essere bello per il golf.

Il codice ha raggiunto N=39entro 10 secondi su questo vecchio laptop 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

Per tupla (a,b,s,t): aè il primo elemento di A, bè l'ultimo elemento di B, sè il prodotto interno di A[:-1]e B, ed tè il prodotto interno di A[1:-1]e B[:-1], usando la notazione di slice Python. Il mio codice non memorizza gli array Ao Baltrove, quindi uso quelle lettere per fare riferimento agli elementi successivi da aggiungere Ae B, rispettivamente. Questa scelta di denominazione variabile rende la spiegazione un po 'imbarazzante, ma consente un aspetto gradevole A*b+a*Bnel codice stesso. Si noti che l'elemento a cui si aggiunge Aè il penultimo, poiché l'ultimo elemento è sempre lo stesso del primo. Ho usato il trucco di Martin Büttner di includere 0due volteBcandidati al fine di ottenere la corretta distribuzione delle probabilità. Il dizionario X(che prende il nome Ydi N+1) tiene traccia del conteggio di tutte le possibili matrici in base al valore della tupla. Le variabili ne dsupporto per numeratore e denominatore, che è il motivo per cui ho rinominato la ndella dichiarazione problema N.

La parte fondamentale della logica è che è possibile aggiornare da Na N+1utilizzare solo i valori nella tupla. I due prodotti interni indicati nella domanda sono indicati da s+A*Be t+A*b+a*B. Questo è chiaro se si esaminano un po 'le definizioni; notare che [A,a]e [b,B]sono gli ultimi due elementi delle matrici Ae Brispettivamente.

Si noti che se tsono piccoli e limitati in base a N, e per una rapida implementazione in un linguaggio veloce potremmo evitare dizionari a favore di array.

Potrebbe essere possibile sfruttare la simmetria considerando valori che differiscono solo per segno; Non l'ho esaminato.

Nota 1 : la dimensione del dizionario aumenta in modo quadratico N, dove dimensione indica il numero di coppie chiave-valore.

Nota 2 : se impostiamo un limite superiore N, possiamo potare le tuple per le quali N_MAX - N <= |s|e allo stesso modo per t. Questo potrebbe essere fatto specificando uno stato di assorbimento, o implicitamente con una variabile per contenere il conteggio degli stati di potatura (che dovrebbe essere moltiplicato per 8 ad ogni iterazione).

Aggiornamento : questa versione è più veloce:

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()

Ottimizzazioni implementate:

  • metti tutto in main(): l'accesso alle variabili locali è più veloce di quello globale
  • gestire N=1esplicitamente per evitare il controllo (1,-1) if N else [a](che impone che il primo elemento nella tupla sia coerente, quando si aggiungono elementi a Apartire dall'elenco vuoto)
  • srotolare i circuiti interni, eliminando anche la moltiplicazione
  • raddoppiare il conteggio cper l'aggiunta di 0a Binvece di eseguire quelle operazioni due volte
  • il denominatore è sempre 8^Nquindi non è necessario tenerne traccia
  • ora tenendo conto della simmetria: possiamo fissare il primo elemento di Aas 1e dividere il denominatore per 2, poiché le coppie valide (A,B)con A[1]=1e quelle con A[1]=-1possono essere messe in corrispondenza uno a uno negando A. Allo stesso modo, possiamo correggere il primo elemento Bcome non negativo.
  • ora con potatura. Dovresti giocherellare N_MAXper vedere quale punteggio può ottenere sulla tua macchina. Potrebbe essere riscritto per trovare N_MAXautomaticamente un'appropriata ricerca binaria, ma sembra superfluo? Nota: non abbiamo bisogno di controllare le condizioni di potatura fino a quando non raggiungiamo N_MAX / 2, quindi potremmo ottenere un po 'di velocità ripetendo in due fasi, ma ho deciso di non farlo per semplicità e pulizia del codice.

1
Questa è davvero un'ottima risposta! Potresti spiegare cosa hai fatto nella tua velocità?

@Lembik Grazie :) Aggiunta una spiegazione, oltre a un'altra piccola ottimizzazione, e resa conforme a Python3.
Mitch Schwartz,

Sul mio computer, ho ottenuto N=57per la prima versione e N=75per la seconda.
Kirbyfan64sos,

Le tue risposte sono state fantastiche. È solo che la risposta di Min_25 è stata ancora di più :)

5

Pitone

Usando l'idea di Min_25 di camminata casuale, sono stato in grado di arrivare a una formula diversa:

p (n) = \ begin {casi} \ frac {\ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {dispari} \ \ 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 {casi}

Ecco un'implementazione di Python basata su 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()

Spiegazione / prova:

Innanzitutto risolviamo un problema di conteggio correlato, dove consentiamo A[n+1] = -A[1]; vale a dire, l'elemento aggiuntivo concatenato Apuò essere 1o -1indipendentemente dal primo elemento. Quindi non è necessario tenere traccia di quante volte si A[i] = A[i+1]verificano. Abbiamo la seguente passeggiata casuale:

Da (x,y)possiamo passare a (x+1,y+1)[1 via], (x+1,y-1)[1 via], (x-1,y+1)[1 via], (x-1,y-1)[1 via], (x,y)[4 vie]

dove xe yrappresentiamo i due prodotti a punti, e contiamo il numero di modi per spostarsi da (0,0)a (0,0)a npassi. Tale conteggio verrebbe quindi moltiplicato per 2tenere conto del fatto che Apuò iniziare con 1o -1.

Ci riferiamo a rimanere (x,y)come una mossa zero .

Esaminiamo il numero di mosse diverse da zero i, che deve essere pari per tornare a (0,0). I movimenti orizzontali e verticali compongono due camminate casuali unidimensionali indipendenti, che possono essere contate C(i,i/2)^2, dove si C(n,k)trova il coefficiente binomiale. (Per una camminata con kpassi a sinistra e kpassi a destra, ci sono C(2k,k)modi per scegliere l'ordine dei passi.) Inoltre ci sono C(n,i)modi per posizionare le mosse e 4^(n-i)modi per scegliere le mosse zero. Quindi otteniamo:

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

Ora, dobbiamo tornare al problema originale. Definire una coppia consentita (A,B)per essere convertibile se Bcontiene uno zero. Definire una coppia (A,B)di essere quasi ammissibile se A[n+1] = -A[1]ei due prodotti dot siano entrambi nulli.

Lemma: Per un dato momento n, le coppie quasi consentite sono in corrispondenza uno a uno con le coppie convertibili.

Possiamo (reversibilmente) convertire una coppia convertibile (A,B)in una coppia quasi ammissibile (A',B')negando A[m+1:]e B[m+1:], dove si mtrova l'indice dell'ultimo zero in B. Il controllo per questo è semplice: se l'ultimo elemento di Bè zero, non abbiamo bisogno di fare nulla. Altrimenti, quando neghiamo l'ultimo elemento di A, possiamo negare l'ultimo elemento Bal fine di preservare l'ultimo termine del prodotto con punto spostato. Ma questo annulla l'ultimo valore del prodotto punto non spostato, quindi risolviamo negando il penultimo elemento di A. Ma poi questo getta via il penultimo valore del prodotto spostato, quindi neghiamo il penultimo elemento di B. E così via, fino a raggiungere un elemento zero in B.

Ora, dobbiamo solo mostrare che non ci sono coppie quasi consentite per le quali Bnon contiene uno zero. Perché un prodotto punto sia uguale a zero, dobbiamo avere un numero uguale di 1e -1termini per annullare. Ogni -1termine è composto da (1,-1)o (-1,1). Quindi la parità del numero -1che si verifica è fissata in base a n. Se il primo e l'ultimo elemento di Ahanno segni diversi, cambiamo la parità, quindi questo è impossibile.

Quindi otteniamo

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

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

che fornisce la formula sopra (reindicizzazione con i' = i/2).

Aggiornamento: ecco una versione più veloce usando la stessa formula:

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()

Ottimizzazioni implementate:

  • funzione in linea p(n)
  • usa la ricorrenza per coefficienti binomiali C(n,k)conk <= n/2
  • usa la ricorrenza per i coefficienti binomiali centrali

Solo così sai, p(n)non deve essere una funzione a tratti. In generale, se f(n) == {g(n) : n is odd; h(n) : n is even}è possibile scrivere f(n) == (n-2*floor(n/2))*g(n) + ((n+1)-2*(floor((n+1)/2)))*h(n)o utilizzare n mod 2invece di (n-2*floor(n/2)). Vedi qui
mbomb007,

1
@ mbomb007 È ovvio e poco interessante.
Mitch Schwartz,

3

Spiegazione della formula di Min_25

Min_25 ha pubblicato un'ottima prova ma ci è voluto un po 'di tempo per seguirlo. Questa è una spiegazione da compilare tra le righe.

a (n, m) rappresenta il numero di modi per scegliere A in modo tale che A [i] = A [i + 1] m volte. La formula per a (n, m) è equivalente a a (n, m) = {2 * (n sceglie m) per nm pari; 0 per nm dispari.} È consentita una sola parità perché A [i]! = A [i + 1] deve verificarsi un numero pari di volte in modo che A [0] = A [n]. Il fattore 2 è dovuto alla scelta iniziale A [0] = 1 o A [0] = -1.

Una volta che il numero di (A [i]! = A [i + 1]) è fissato per essere q (chiamato i nella formula c (n)), si separa in due passi casuali 1D di lunghezza q e nq. b (m) è il numero di modi per eseguire una camminata casuale unidimensionale di m passi che termina nello stesso punto in cui è iniziata e ha il 25% di probabilità di spostarsi a sinistra, il 50% di probabilità di rimanere fermi e il 25% di possibilità di muovendosi a destra. Un modo più ovvio per dichiarare la funzione di generazione è [x ^ m] (1 + 2x + x ^ 2) ^ n, dove 1, 2x e x ^ 2 rappresentano rispettivamente sinistra, nessuna mossa e destra. Ma poi 1 + 2x + x ^ 2 = (x + 1) ^ 2.


Ancora un altro motivo per amare PPCG! Grazie.

2

C ++

Solo una porta della (eccellente) risposta Python di Mitch Schwartz. La differenza principale è che 2rappresentavo -1per la avariabile e facevo qualcosa di simile b, che mi ha permesso di usare un array. Utilizzando Intel C ++ con -O3, ho ottenuto N=141! Ho ottenuto la mia prima versione N=140.

Questo utilizza Boost. Ho provato una versione parallela ma ho riscontrato qualche problema.

#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;
}

Questo deve g++ -O3 kirbyfan64sos.cpp -o kirbyfan64sos -lboost_system -lboost_timer -lboost_chrono -lrt -lgmpcompilare. (Grazie ad aditsu.)
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.