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 Lindica uma letra e
Dindica um dígito. Suponha que temos uma lista lde números que
l[i]fornece o número de maneiras pelas quais as letras podem fornecer o valor ie uma lista semelhante dpara 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^3casos 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 lestá apenas a lista de coeficientes de
(x+x^2+...+x^26)^konde kestá 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 kdí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#d2que na posição itenha 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 kletras 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 dpolinô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 dnã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 modcá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 le dpodemos 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,
NrArrangementsfornece 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 Binomialconta essas possibilidades.
Agora, resta analisar todas as formas possíveis de ter comprimentos de dígitos de execuções. ratravessa todos os números de pistas, catravés de todos os números totais de dígitos, e patravés de todas as partições de ccom
rsummands.
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#risso 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 ldevemos @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 lengthexecuções podem fornecer o número i. Durante a computação, no início do mloop, 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 npior dos casos. Então, precisamos apenas de tempo polinomial. Se observarmos mais de perto o aninhado ie o jloop extend, encontraremos um limite superior para jo formulário N/i. Isso deve fornecer apenas um fator logarítmico para o jloop. O loop mais interno f
(com sumnetc) é 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.