GAP , 416 bytes
Não vencerá no tamanho do código e está longe do tempo constante, mas usa a matemática para acelerar muito!
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 espremer o espaço em branco desnecessário e obter uma linha com 416 bytes, passe por isso:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Meu antigo laptop "projetado para Windows XP" pode calcular f(10)
em menos de um minuto e ir além em menos de uma 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
Como funciona
Suponha que primeiro queremos saber apenas o número de placas perfeitas que se encaixam no padrão LDDLLDL
, onde L
indica uma letra e
D
indica um dígito. Suponha que temos uma lista l
de números que
l[i]
fornece o número de maneiras pelas quais as letras podem fornecer o valor i
e uma lista semelhante d
para os valores que obtemos dos dígitos. Então, o número de placas perfeitas com valor comum i
é justo
l[i]*d[i]
, e obtemos o número de todas as placas perfeitas com nosso padrão, somando isso em geral i
. Vamos denotar a operação de obter essa soma por l@d
.
Agora, mesmo que a melhor maneira de obter essas listas seja tentar todas as combinações e contar, podemos fazer isso de forma independente para as letras e os dígitos, observando 26^4+10^3
casos em vez de 26^4*10^3
casos quando apenas percorremos todas as placas que se encaixam no padrão. Mas podemos fazer muito melhor: aqui l
está apenas a lista de coeficientes de
(x+x^2+...+x^26)^k
onde k
está o número de letras 4
.
Da mesma forma, obtemos o número de maneiras de obter uma soma de dígitos em uma sequência de k
dígitos como coeficientes de (1+x+...+x^9)^k
. Se houver mais de uma sequência de dígitos, precisamos combinar as listas correspondentes com uma operação d1#d2
que na posição i
tenha como valor a soma de tudo em d1[i1]*d2[i2]
que i1*i2=i
. Essa é a convolução de Dirichlet, que é apenas o produto se interpretarmos as listas como coeficientes da série Dirchlet. Mas nós já os usamos como polinômios (séries finitas de potência) e não há uma boa maneira de interpretar a operação para eles. Eu acho que essa incompatibilidade faz parte do que torna difícil encontrar uma fórmula simples. Vamos usá-lo em polinômios de qualquer maneira e usar a mesma notação #
. É fácil calcular quando um operando é um monômio: temosp(x) # x^k = p(x^k)
. Juntamente com o fato de ser bilinear, isso fornece uma maneira agradável (mas não muito eficiente) de computá-lo.
Observe que as k
letras dão um valor de no máximo 26k
, enquanto k
os dígitos únicos podem dar um valor de 9^k
. Portanto, frequentemente obteremos altas potências desnecessárias no d
polinômio. Para se livrar deles, podemos calcular o módulo x^(maxlettervalue+1)
. Isso dá uma grande velocidade e, embora eu não tenha notado imediatamente, até ajuda no golfe, porque agora sabemos que o grau de d
não é maior que o de l
, o que simplifica o limite superior na final Sum
. Nós obtemos uma velocidade ainda melhor ao fazer um mod
cálculo no primeiro argumento de Value
(ver comentários), e fazer todo o #
cálculo em um nível mais baixo fornece uma velocidade incrível. Mas ainda estamos tentando ser uma resposta legítima para um problema no golfe.
Portanto, temos o nosso l
e d
podemos usá-lo para calcular o número de placas perfeitas com padrão LDDLLDL
. Esse é o mesmo número do padrão LDLLDDL
. Em geral, podemos alterar a ordem das séries de dígitos de diferentes comprimentos, conforme desejamos,
NrArrangements
fornece o número de possibilidades. E enquanto deve haver uma letra entre as execuções de dígitos, as outras letras não são fixas. O Binomial
conta essas possibilidades.
Agora, resta analisar todas as formas possíveis de ter comprimentos de dígitos de execuções. r
atravessa todos os números de pistas, c
através de todos os números totais de dígitos, e p
através de todas as partições de c
com
r
summands.
O número total de partições que observamos é dois a menos do que o número de partições n+1
, e as funções da partição aumentam como
exp(sqrt(n))
. Portanto, embora ainda haja maneiras fáceis de melhorar o tempo de execução reutilizando resultados (executando as partições em uma ordem diferente), para uma melhoria fundamental, precisamos evitar olhar cada partição separadamente.
Computando rápido
Note isso (p+q)@r = p@r + q@r
. Por si só, isso apenas ajuda a evitar algumas multiplicações. Mas junto com (p+q)#r = p#r + q#r
isso significa que podemos combinar por polinômios de adição simples correspondentes a diferentes partições. Não podemos simplesmente adicioná-los todos, porque ainda precisamos saber com os quais l
devemos @
combinar, qual fator devemos usar e quais #
extensões ainda são possíveis.
Vamos combinar todos os polinômios correspondentes às partições com a mesma soma e comprimento, e já consideramos as várias maneiras de distribuir os comprimentos das execuções de dígitos. Diferente do que especulei nos comentários, não preciso me preocupar com o menor valor usado ou com que frequência ele é usado, se tiver certeza de que não estenderei esse valor.
Aqui está o meu 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" ;
}
Isso usa a biblioteca GNU MP. No debian, instale libgmp-dev
. Compile com g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. O programa leva seu argumento de stdin. Para cronometrar, use echo 100 | time ./pl
.
No final, a[sum][length][i]
fornece o número de maneiras pelas quais os sum
dígitos nas length
execuções podem fornecer o número i
. Durante a computação, no início do m
loop, fornece o número de maneiras que podem ser feitas com números maiores que m
. Tudo começa com
a[0][0][1]=1
. Observe que este é um superconjunto dos números que precisamos para calcular a função para valores menores. Assim, quase ao mesmo tempo, poderíamos calcular todos os valores até n
.
Como não há recursão, temos um número fixo de loops aninhados. (O nível de aninhamento mais profundo é 6.) Cada loop passa por vários valores lineares no n
pior dos casos. Então, precisamos apenas de tempo polinomial. Se observarmos mais de perto o aninhado i
e o j
loop extend
, encontraremos um limite superior para j
o formulário N/i
. Isso deve fornecer apenas um fator logarítmico para o j
loop. O loop mais interno f
(com sumn
etc) é semelhante. Lembre-se também de que computamos com números que crescem rapidamente.
Observe também que armazenamos O(n^3)
esses números.
Experimentalmente, obtenho esses resultados em hardware razoável (i5-4590S):
f(50)
precisa de um segundo e 23 MB, f(100)
precisa de 21 segundos e 166 MB, f(200)
precisa de 10 minutos e 1,5 GB e f(300)
precisa de uma hora e 5,6 GB. Isso sugere uma complexidade de tempo melhor que O(n^5)
.
N
.