Supponiamo che tutte le parti d=6 abbiano pari possibilità. Generalize e di Let trovare il numero atteso di rotoli necessari fino lato 1 è apparso n1 volte, parte 2 è apparso n2 volte, ..., e sul lato d è apparso nd volte. Poiché le identità dei lati non contano (hanno tutte le stesse possibilità), la descrizione di questo obiettivo può essere condensata: supponiamo che io0 lati non debbano apparire affatto, i1 dei lati devono apparire solo una volta, ... e indei lati devono apparire n=max(n1,n2,…,nd) volte. Sia
i=(i0,i1,…,in)
designare questa situazione e scrivere
e(i)
per il numero previsto di tiri. La domanda richiede
e(0,0,0,6) :
i3=6 indica che tutti e sei i lati devono essere visti tre volte ciascuno.
È disponibile una ricorrenza facile. Al tiro successivo, la parte che appare corrisponde a uno dei ij : che è, o non abbiamo bisogno di vederlo, o abbiamo bisogno di vedere una volta, ..., o abbiamo bisogno di vederlo n più volte. j è il numero di volte in cui abbiamo avuto bisogno di vederlo.
Quando j=0 , non abbiamo bisogno di vederlo e non cambia nulla. Questo accade con probabilità i0/d .
Quando allora abbiamo avuto bisogno di vedere questo lato. Ora c'è un lato in meno che deve essere visto j volte e un altro lato che deve essere visto j - 1 volte. Pertanto, i j diventa i j - 1 e i j - 1 diventa i j + 1 . Lascia che questa operazione sui componenti di i sia designata i ⋅ j , in modo chej>0jj−1ijij−1ij−1ij+1ii⋅j
i⋅j=(i0,…,ij−2,ij−1+1,ij−1,ij+1,…,in).
Questo accade con probabilità .ij/d
Dobbiamo semplicemente contare questo tiro di dado e usare la ricorsione per dirci quanti altri tiri sono previsti. Secondo le leggi di aspettativa e probabilità totale,
e(i)=1+i0de(i)+∑j=1nijde(i⋅j)
(Comprendiamo che ogni volta che , il termine corrispondente nella somma è zero.)ij=0
Se , abbiamo finito ed e ( i ) = 0 . Altrimenti possiamo risolvere per e ( i ) , dando la formula ricorsiva desideratai0=de(i)=0e(i)
e(i)=d+i1e(i⋅1)+⋯+ine(i⋅n)d−i0.(1)
Si noti che è il numero totale di eventi che desideriamo vedere. L'operazione ⋅ j riduce quella quantità di una per ogni j > 0 fornito i j > 0 , che è sempre il caso. Pertanto questa ricorsione termina a una profondità precisamente | io | (uguale a 3 ( 6 ) =
|i|=0(i0)+1(i1)+⋯+n(in)
⋅jj>0ij>0|i| nella domanda). Inoltre (come non è difficile verificare) il numero di possibilità per ciascuna profondità di ricorsione in questa domanda è piccolo (mai superiore a
8 ). Di conseguenza, questo è un metodo efficiente, almeno quando le possibilità combinatorie non sono troppo numerose e memorizziamo i risultati intermedi (in modo che nessun valore di
e sia calcolato più di una volta).
3(6)=188e
Calcolo che
e(0,0,0,6)=228687860450888369984000000000≈32.677.
Mi è sembrato terribilmente piccolo, quindi ho eseguito una simulazione (usando R
). Dopo oltre tre milioni di lanci di dadi, questo gioco è stato giocato fino al suo completamento oltre 100.000 volte, con una lunghezza media di . L'errore standard di tale stima è 0,027 : la differenza tra questa media e il valore teorico è insignificante, confermando l'accuratezza del valore teorico.32.6690.027
La distribuzione delle lunghezze può essere di interesse. (Ovviamente deve iniziare alle , il numero minimo di tiri necessari per raccogliere tutti e sei i lati tre volte ciascuno.)18
# Specify the problem
d <- 6 # Number of faces
k <- 3 # Number of times to see each
N <- 3.26772e6 # Number of rolls
# Simulate many rolls
set.seed(17)
x <- sample(1:d, N, replace=TRUE)
# Use these rolls to play the game repeatedly.
totals <- sapply(1:d, function(i) cumsum(x==i))
n <- 0
base <- rep(0, d)
i.last <- 0
n.list <- list()
for (i in 1:N) {
if (min(totals[i, ] - base) >= k) {
base <- totals[i, ]
n <- n+1
n.list[[n]] <- i - i.last
i.last <- i
}
}
# Summarize the results
sim <- unlist(n.list)
mean(sim)
sd(sim) / sqrt(length(sim))
length(sim)
hist(sim, main="Simulation results", xlab="Number of rolls", freq=FALSE, breaks=0:max(sim))
Implementazione
Sebbene il calcolo ricorsivo di sia semplice, presenta alcune sfide in alcuni ambienti informatici. Il principale tra questi è la memorizzazione dei valori di e ( i ) mentre vengono calcolati. Questo è essenziale, altrimenti ogni valore verrà (ridondante) calcolato un numero molto grande di volte. Tuttavia, l'archiviazione potenzialmente necessaria per un array indicizzato da iee(i)i potrebbe essere enorme. Idealmente, dovrebbero essere memorizzati solo i valori di effettivamente rilevati durante il calcolo. Ciò richiede una sorta di array associativo.i
Per illustrare, ecco il R
codice funzionante . I commenti descrivono la creazione di una semplice classe "AA" (array associativo) per la memorizzazione di risultati intermedi. I vettori vengono convertiti in stringhe e quelli vengono utilizzati per indicizzare in un elenco che conterrà tutti i valori. IliE
operazione i ⋅ j è implementata come.i⋅j%.%
Questi preliminari abilitano la funzione ricorsiva di definire e in modo piuttosto semplice in modo da mettere in parallelo la notazione matematica. In particolare, la lineae
x <- (d + sum(sapply(1:n, function(i) j[i+1]*e.(j %.% i))))/(d - j[1])
è direttamente paragonabile alla formula sopra. Si noti che tutti gli indici sono stati aumentati di 1 perché inizia a indicizzare le sue matrici su 1 anziché su 0 .(1)1R
10
Il tempismo mostra che occorrono secondi per il calcolo ; il suo valore è0.01e(c(0,0,0,6))
32,6771634160506
L'errore di arrotondamento in virgola mobile accumulato ha distrutto le ultime due cifre (che dovrebbero essere 68
anziché 06
).
e <- function(i) {
#
# Create a data structure to "memoize" the values.
#
`[[<-.AA` <- function(x, i, value) {
class(x) <- NULL
x[[paste(i, collapse=",")]] <- value
class(x) <- "AA"
x
}
`[[.AA` <- function(x, i) {
class(x) <- NULL
x[[paste(i, collapse=",")]]
}
E <- list()
class(E) <- "AA"
#
# Define the "." operation.
#
`%.%` <- function(i, j) {
i[j+1] <- i[j+1]-1
i[j] <- i[j] + 1
return(i)
}
#
# Define a recursive version of this function.
#
e. <- function(j) {
#
# Detect initial conditions and return initial values.
#
if (min(j) < 0 || sum(j[-1])==0) return(0)
#
# Look up the value (if it has already been computed).
#
x <- E[[j]]
if (!is.null(x)) return(x)
#
# Compute the value (for the first and only time).
#
d <- sum(j)
n <- length(j) - 1
x <- (d + sum(sapply(1:n, function(i) j[i+1]*e.(j %.% i))))/(d - j[1])
#
# Store the value for later re-use.
#
E[[j]] <<- x
return(x)
}
#
# Do the calculation.
#
e.(i)
}
e(c(0,0,0,6))
Infine, ecco l' implementazione originale di Mathematica che ha prodotto la risposta esatta. La memorizzazione viene effettuata tramite l' e[i_] := e[i] = ...
espressione idiomatica , eliminando quasi tutti i R
preliminari. Internamente, però, i due programmi stanno facendo le stesse cose allo stesso modo.
shift[j_, x_List] /; Length[x] >= j >= 2 := Module[{i = x},
i[[j - 1]] = i[[j - 1]] + 1;
i[[j]] = i[[j]] - 1;
i];
e[i_] := e[i] = With[{i0 = First@i, d = Plus @@ i},
(d + Sum[If[i[[k]] > 0, i[[k]] e[shift[k, i]], 0], {k, 2, Length[i]}])/(d - i0)];
e[{x_, y__}] /; Plus[y] == 0 := e[{x, y}] = 0
e[{0, 0, 0, 6}]
228687860450888369984000000000