GAP , 416 byte
Non vincerà sulla dimensione del codice, e lungi dall'essere un tempo costante, ma usa la matematica per accelerare molto!
x:=X(Integers);
z:=CoefficientsOfUnivariatePolynomial;
s:=Size;
f:=function(n)
local r,c,p,d,l,u,t;
t:=0;
for r in [1..Int((n+1)/2)] do
for c in [r..n-r+1] do
l:=z(Sum([1..26],i->x^i)^(n-c));
for p in Partitions(c,r) do
d:=x;
for u in List(p,k->z(Sum([0..9],i->x^i)^k)) do
d:=Sum([2..s(u)],i->u[i]*Value(d,x^(i-1))mod x^s(l));
od;
d:=z(d);
t:=t+Binomial(n-c+1,r)*NrArrangements(p,r)*
Sum([2..s(d)],i->d[i]*l[i]);
od;
od;
od;
return t;
end;
Per eliminare lo spazio bianco non necessario e ottenere una riga con 416 byte, passare attraverso questo:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Il mio vecchio laptop "progettato per Windows XP" può calcolare f(10)
in meno di un minuto e andare molto più avanti in meno di un'ora:
gap> for i in [2..15] do Print(i,": ",f(i),"\n");od;
2: 18
3: 355
4: 8012
5: 218153
6: 6580075
7: 203255386
8: 6264526999
9: 194290723825
10: 6116413503390
11: 194934846864269
12: 6243848646446924
13: 199935073535438637
14: 6388304296115023687
15: 203727592114009839797
Come funziona
Supponiamo di voler prima conoscere solo il numero di targhe perfette che si adattano al modello LDDLLDL
, dove L
indica una lettera e
D
indica una cifra. Supponiamo di avere un elenco l
di numeri tale
l[i]
da fornire il numero di modi in cui le lettere possono dare il valore i
e un elenco simile d
per i valori che otteniamo dalle cifre. Quindi il numero di targhe perfette con un valore comune i
è giusto
l[i]*d[i]
e otteniamo il numero di tutte le targhe perfette con il nostro modello sommando questo su tutto i
. Indichiamo l'operazione per ottenere questa somma l@d
.
Ora, anche se il modo migliore per ottenere questi elenchi fosse provare tutte le combinazioni e contare, possiamo farlo in modo indipendente per le lettere e le cifre, osservando i 26^4+10^3
casi anziché i 26^4*10^3
casi quando passiamo attraverso tutte le piastre che si adattano al modello. Ma possiamo fare molto meglio: l
è solo l'elenco dei coefficienti di
(x+x^2+...+x^26)^k
dove k
è il numero di lettere, qui 4
.
Allo stesso modo, otteniamo il numero di modi per ottenere una somma di cifre in una serie di k
cifre come coefficienti di (1+x+...+x^9)^k
. Se v'è più di una corsa di cifre, è necessario combinare gli elenchi corrispondenti con un'operazione d1#d2
che nella posizione i
ha come valore la somma di tutti d1[i1]*d2[i2]
dove i1*i2=i
. Questa è la convoluzione di Dirichlet, che è solo il prodotto se interpretiamo le liste come coefficienti delle serie di Dirchlet. Ma li abbiamo già usati come polinomi (serie di potenze finite) e non esiste un buon modo per interpretare l'operazione per loro. Penso che questa mancata corrispondenza faccia parte di ciò che rende difficile trovare una formula semplice. Usiamolo comunque sui polinomi e usiamo la stessa notazione #
. È facile calcolare quando un operando è un monomio: abbiamop(x) # x^k = p(x^k)
. Insieme al fatto che è bilineare, questo fornisce un modo piacevole (ma non molto efficiente) per calcolarlo.
Nota che le k
lettere danno un valore al massimo 26k
, mentre le k
singole cifre possono dare un valore di 9^k
. Quindi spesso otterremo alti poteri non necessari nel d
polinomio. Per sbarazzarci di loro, possiamo calcolare il modulo x^(maxlettervalue+1)
. Questo dà una grande velocità e, anche se non l'ho notato immediatamente, aiuta anche a giocare a golf, perché ora sappiamo che il grado di d
non è maggiore di quello dil
, il che semplifica il limite superiore in finale Sum
. Otteniamo un'accelerazione ancora migliore facendo un mod
calcolo nel primo argomento di Value
(vedi commenti) e facendo l'intero #
calcolo a un livello inferiore si ottiene un'incredibile velocità. Ma stiamo ancora cercando di essere una risposta legittima a un problema di golf.
Quindi abbiamo il nostro l
e d
possiamo usarli per calcolare il numero di targhe perfette con motivoLDDLLDL
. Questo è lo stesso numero del modello LDLLDDL
. In generale, possiamo cambiare l'ordine delle serie di cifre di diversa lunghezza a nostro piacimento,
NrArrangements
dando il numero di possibilità. E mentre ci deve essere una lettera tra le serie di cifre, le altre lettere non sono fisse. Il Binomial
conta queste possibilità.
Ora resta da percorrere tutti i modi possibili per avere lunghezze di cifre di esecuzione. r
corre attraverso tutti i numeri di corse, c
attraverso tutti i numeri totali di cifre ep
attraverso tutte le partizioni di c
con
r
somme.
Il numero totale di partizioni che osserviamo è due in meno rispetto al numero di partizioni di n+1
, e le funzioni di partizione crescono come
exp(sqrt(n))
. Quindi, mentre ci sono ancora modi semplici per migliorare il tempo di esecuzione riutilizzando i risultati (scorrendo le partizioni in un ordine diverso), per un miglioramento fondamentale dobbiamo evitare di guardare ciascuna partizione separatamente.
Calcolo veloce
Si noti che (p+q)@r = p@r + q@r
. Da solo, questo aiuta solo ad evitare alcune moltiplicazioni. Ma insieme ad (p+q)#r = p#r + q#r
esso significa che possiamo combinare con semplici polinomi di aggiunta corrispondenti a diverse partizioni. Non possiamo semplicemente aggiungerli tutti, perché dobbiamo ancora sapere con chi l
dobbiamo@
combinare, quale fattore dobbiamo usare e quali #
estensioni sono ancora possibili.
Combiniamo tutti i polinomi corrispondenti alle partizioni con la stessa somma e lunghezza e già spieghiamo i molteplici modi di distribuire le lunghezze delle serie di cifre. Diversamente da quanto ho ipotizzato nei commenti, non ho bisogno di preoccuparmi del valore usato più piccolo o della frequenza con cui viene utilizzato, se mi assicuro di non estenderlo con quel valore.
Ecco il mio codice C ++:
#include<vector>
#include<algorithm>
#include<iostream>
#include<gmpxx.h>
using bignum = mpz_class;
using poly = std::vector<bignum>;
poly mult(const poly &a, const poly &b){
poly res ( a.size()+b.size()-1 );
for(int i=0; i<a.size(); ++i)
for(int j=0; j<b.size(); ++j)
res[i+j]+=a[i]*b[j];
return res;
}
poly extend(const poly &d, const poly &e, int ml, poly &a, int l, int m){
poly res ( 26*ml+1 );
for(int i=1; i<std::min<int>(1+26*ml,e.size()); ++i)
for(int j=1; j<std::min<int>(1+26*ml/i,d.size()); ++j)
res[i*j] += e[i]*d[j];
for(int i=1; i<res.size(); ++i)
res[i]=res[i]*l/m;
if(a.empty())
a = poly { res };
else
for(int i=1; i<a.size(); ++i)
a[i]+=res[i];
return res;
}
bignum f(int n){
std::vector<poly> dp;
poly digits (10,1);
poly dd { 1 };
dp.push_back( dd );
for(int i=1; i<n; ++i){
dd=mult(dd,digits);
int l=1+26*(n-i);
if(dd.size()>l)
dd.resize(l);
dp.push_back(dd);
}
std::vector<std::vector<poly>> a;
a.reserve(n);
a.push_back( std::vector<poly> { poly { 0, 1 } } );
for(int i=1; i<n; ++i)
a.push_back( std::vector<poly> (1+std::min(i,n+i-i)));
for(int m=n-1; m>0; --m){
// std::cout << "m=" << m << "\n";
for(int sum=n-m; sum>=0; --sum)
for(int len=0; len<=std::min(sum,n+1-sum); ++len){
poly d {a[sum][len]} ;
if(!d.empty())
for(int sumn=sum+m, lenn=len+1, e=1;
sumn+lenn-1<=n;
sumn+=m, ++lenn, ++e)
d=extend(d,dp[m],n-sumn,a[sumn][lenn],lenn,e);
}
}
poly let (27,1);
let[0]=0;
poly lp { 1 };
bignum t { 0 };
for(int sum=n-1; sum>0; --sum){
lp=mult(lp,let);
for(int len=1; len<=std::min(sum,n+1-sum); ++len){
poly &a0 = a[sum][len];
bignum s {0};
for(int i=1; i<std::min(a0.size(),lp.size()); ++i)
s+=a0[i]*lp[i];
bignum bin;
mpz_bin_uiui( bin.get_mpz_t(), n-sum+1, len );
t+=bin*s;
}
}
return t;
}
int main(){
int n;
std::cin >> n;
std::cout << f(n) << "\n" ;
}
Questo utilizza la libreria GNU MP. Su debian, installa libgmp-dev
. Compila con g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. Il programma prende le sue argomentazioni da stdin. Per i tempi, utilizzare echo 100 | time ./pl
.
Alla fine a[sum][length][i]
indica il numero di modi in cui le sum
cifre nelle length
corse possono indicare il numero i
. Durante il calcolo, all'inizio del m
ciclo, fornisce il numero di modi che è possibile eseguire con numeri maggiori di m
. Tutto inizia con
a[0][0][1]=1
. Si noti che questo è un superset dei numeri di cui abbiamo bisogno per calcolare la funzione per valori più piccoli. Quindi quasi allo stesso tempo, potremmo calcolare tutti i valori fino a n
.
Non c'è ricorsione, quindi abbiamo un numero fisso di loop nidificati. (Il livello di annidamento più profondo è 6.) Ogni ciclo passa attraverso un numero di valori che è lineare nel n
caso peggiore. Quindi abbiamo bisogno solo del tempo polinomiale. Se osserviamo più da vicino i nidificati i
e gli j
anelli extend
, troviamo un limite superiore per j
il modulo N/i
. Ciò dovrebbe solo dare un fattore logaritmico per il j
ciclo. Il loop più interno in f
(con sumn
etc) è simile. Tieni anche presente che calcoliamo con numeri che crescono rapidamente.
Si noti inoltre che memorizziamo O(n^3)
questi numeri.
Sperimentalmente, ottengo questi risultati su hardware ragionevole (i5-4590S): ha
f(50)
bisogno di un secondo e 23 MB, f(100)
richiede 21 secondi e 166 MB, ha f(200)
bisogno di 10 minuti e 1,5 GB e f(300)
richiede un'ora e 5,6 GB. Ciò suggerisce una complessità temporale migliore di O(n^5)
.
N
.