GAP , 416 bytes
No ganará en tamaño de código, y lejos del tiempo constante, ¡pero usa las matemáticas para acelerar mucho!
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;
Para exprimir el espacio en blanco innecesario y obtener una línea con 416 bytes, canalice esto:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Mi vieja computadora portátil "diseñada para Windows XP" puede calcular f(10)
en menos de un minuto e ir mucho más lejos en menos de una hora:
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
Cómo funciona
Supongamos que primero solo queremos saber el número de placas perfectas que se ajustan al patrón LDDLLDL
, donde L
denota una letra y
D
denota un dígito. Supongamos que tenemos una lista l
de números que
l[i]
da la cantidad de formas en que las letras pueden dar el valor i
, y una lista similar d
para los valores que obtenemos de los dígitos. Entonces, el número de placas perfectas con valor común i
es justo
l[i]*d[i]
, y obtenemos el número de todas las placas perfectas con nuestro patrón sumando esto sobre todo i
. Denotemos la operación de obtener esta suma l@d
.
Ahora, incluso si la mejor manera de obtener estas listas era probar todas las combinaciones y contar, podemos hacer esto independientemente para las letras y los dígitos, mirando los 26^4+10^3
casos en lugar de los 26^4*10^3
casos cuando simplemente pasamos por todas las placas que se ajustan al patrón. Pero podemos hacerlo mucho mejor: aquí l
es solo la lista de coeficientes de
(x+x^2+...+x^26)^k
dónde k
está el número de letras 4
.
Del mismo modo, obtenemos el número de formas de obtener una suma de dígitos en una serie de k
dígitos como los coeficientes de (1+x+...+x^9)^k
. Si hay más de una serie de dígitos, necesitamos combinar las listas correspondientes con una operación d1#d2
que en la posición i
tenga como valor la suma de todos los d1[i1]*d2[i2]
lugares i1*i2=i
. Esta es la convolución de Dirichlet, que es solo el producto si interpretamos las listas como coeficientes de las series de Dirchlet. Pero ya los hemos usado como polinomios (series de potencia finita), y no hay una buena manera de interpretar la operación para ellos. Creo que este desajuste es parte de lo que hace que sea difícil encontrar una fórmula simple. Usémoslo en polinomios de todos modos y usemos la misma notación #
. Es fácil de calcular cuando un operando es un monomio: tenemosp(x) # x^k = p(x^k)
. Junto con el hecho de que es bilineal, esto proporciona una forma agradable (pero no muy eficiente) de calcularlo.
Tenga en cuenta que las k
letras dan un valor de como máximo 26k
, mientras quek
dígitos individuales pueden dar un valor de 9^k
. Por lo tanto, a menudo obtendremos altos poderes innecesarios en el d
polinomio. Para deshacernos de ellos, podemos calcular el módulo x^(maxlettervalue+1)
. Esto da una gran velocidad y, aunque no me di cuenta de inmediato, incluso ayuda al golf, porque ahora sabemos que el grado ded
no es mayor que el de l
, lo que simplifica el límite superior en la final Sum
. Obtenemos una velocidad aún mejor al hacer un mod
cálculo en el primer argumento de Value
(ver comentarios), y hacer todo el #
cálculo a un nivel inferior da una velocidad increíble. Pero todavía estamos tratando de ser una respuesta legítima a un problema de golf.
Así que tenemos nuestro l
yd
podemos usarlos para calcular el número de matrículas perfectas con patrón LDDLLDL
. Ese es el mismo número que para el patrón LDLLDDL
. En general, podemos cambiar el orden de las corridas de dígitos de diferente longitud como queramos,
NrArrangements
da el número de posibilidades. Y aunque debe haber una letra entre las series de dígitos, las otras letras no son fijas. El Binomial
cuenta estas posibilidades.
Ahora queda por recorrer todas las formas posibles de tener longitudes de dígitos de ejecución. r
recorre todos los números de carreras,c
todos los números totales de dígitos y p
todas las particiones de c
con
r
sumandos.
El número total de particiones que vemos es dos menos que el número de particiones n+1
, y las funciones de partición crecen como
exp(sqrt(n))
. Por lo tanto, aunque todavía hay formas fáciles de mejorar el tiempo de ejecución reutilizando los resultados (ejecutando las particiones en un orden diferente), para una mejora fundamental, debemos evitar mirar cada partición por separado.
Calcularlo rápido
Tenga en cuenta que (p+q)@r = p@r + q@r
. Por sí solo, esto solo ayuda a evitar algunas multiplicaciones. Pero junto con (p+q)#r = p#r + q#r
esto significa que podemos combinar mediante polinomios de suma simple correspondientes a diferentes particiones. No podemos simplemente agregarlos a todos, porque aún necesitamos saber con quél
tenemos que @
combinar, qué factor tenemos que usar y qué #
extensiones aún son posibles.
Combinemos todos los polinomios correspondientes a particiones con la misma suma y longitud, y ya tenga en cuenta las múltiples formas de distribuir las longitudes de las series de dígitos. A diferencia de lo que especulé en los comentarios, no necesito preocuparme por el valor usado más pequeño o con qué frecuencia se usa, si me aseguro de no extenderme con ese valor.
Aquí está mi código 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" ;
}
Esto usa la biblioteca GNU MP. En debian, instale libgmp-dev
. Compilar con g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. El programa toma su argumento de stdin. Para el tiempo, useecho 100 | time ./pl
.
Al final a[sum][length][i]
da el número de formas en que los sum
dígitos en las length
corridas pueden dar el número i
. Durante el cálculo, al comienzo del m
ciclo, proporciona la cantidad de formas que se pueden hacer con números mayores que m
. Todo comienza con
a[0][0][1]=1
. Tenga en cuenta que este es un superconjunto de los números que necesitamos para calcular la función para valores más pequeños. Entonces, casi al mismo tiempo, podríamos calcular todos los valores hasta n
.
No hay recursión, por lo que tenemos un número fijo de bucles anidados. (El nivel de anidamiento más profundo es 6.) Cada ciclo pasa por una serie de valores que son lineales n
en el peor de los casos. Entonces solo necesitamos tiempo polinomial. Si miramos más de cerca los anidados i
y los j
bucles extend
, encontramos un límite superior para j
el formulario N/i
. Eso solo debería dar un factor logarítmico para el j
bucle. El bucle más interno en f
(consumn
etc.) es similar. También tenga en cuenta que calculamos con números que crecen rápidamente.
Tenga en cuenta también que almacenamos O(n^3)
estos números.
Experimentalmente, obtengo estos resultados en hardware razonable (i5-4590S):
f(50)
necesita un segundo y 23 MB, f(100)
necesita 21 segundos y 166 MB, f(200)
necesita 10 minutos y 1.5 GB, y f(300)
necesita una hora y 5.6 GB. Esto sugiere una complejidad temporal mejor que O(n^5)
.
N
.