GAP , 416 octets
Ne gagnera pas sur la taille du code, et loin du temps constant, mais utilise les mathématiques pour accélérer beaucoup!
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;
Pour réduire les espaces inutiles et obtenir une ligne de 416 octets, dirigez-le:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Mon ancien ordinateur portable "conçu pour Windows XP" peut calculer f(10)
en moins d'une minute et aller beaucoup plus loin en moins d'une heure:
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
Comment ça marche
Supposons que nous ne voulions d'abord connaître que le nombre de plaques d'immatriculation parfaites correspondant au motif LDDLLDL
, où correspond à L
une lettre et à
D
un chiffre. Supposons que nous ayons une liste l
de nombres
l[i]
donnant le nombre de façons dont les lettres peuvent donner la valeur i
et une liste similaire d
pour les valeurs que nous obtenons des chiffres. Ensuite, le nombre de plaques d'immatriculation parfaites ayant une valeur commune i
est juste
l[i]*d[i]
, et nous obtenons le nombre de toutes les plaques d'immatriculation parfaites avec notre modèle en additionnant l'ensemble i
. Notons l'opération consistant à obtenir cette somme par l@d
.
Maintenant, même si le meilleur moyen d’obtenir ces listes était d’essayer toutes les combinaisons et de compter, nous pouvons le faire indépendamment pour les lettres et les chiffres, en regardant les 26^4+10^3
cas au lieu des 26^4*10^3
cas où nous passons simplement en revue toutes les plaques correspondant au modèle. Mais nous pouvons faire beaucoup mieux: l
est juste la liste des coefficients de
(x+x^2+...+x^26)^k
où k
est le nombre de lettres, ici 4
.
De même, nous obtenons le nombre de façons d'obtenir une somme de chiffres dans une suite de k
chiffres en tant que coefficients de (1+x+...+x^9)^k
. S'il y a plus d'une suite de chiffres, nous devons combiner les listes correspondantes avec une opération d1#d2
dont position i
a la valeur la somme de tous d1[i1]*d2[i2]
où i1*i2=i
. Il s'agit de la convolution de Dirichlet, qui est simplement le produit si nous interprétons les listes comme des coefficients de la série de Dirchlet. Mais nous les avons déjà utilisés en tant que polynômes (séries de puissance finie) et il n’existe aucun moyen d’interpréter l’opération pour eux. Je pense que cette inadéquation fait partie de ce qui rend difficile de trouver une formule simple. Utilisons-le quand même sur les polynômes et utilisons la même notation #
. Il est facile de calculer quand un opérande est un monôme: on ap(x) # x^k = p(x^k)
. Avec le fait qu'il soit bilinéaire, cela donne un moyen agréable (mais pas très efficace) de le calculer.
Notez que les k
lettres donnent une valeur d'au plus 26k
, alors que k
les chiffres simples peuvent donner une valeur de 9^k
. Nous aurons donc souvent des pouvoirs élevés inutiles dans le d
polynôme. Pour nous en débarrasser, nous pouvons calculer modulo x^(maxlettervalue+1)
. Cela donne une grande vitesse et, bien que je ne l'aie pas remarqué tout de suite, aide même au golf, car nous savons maintenant que le degré de d
n'est pas plus grand que celui de l
, ce qui simplifie la limite supérieure en finale Sum
. Nous obtenons une vitesse encore meilleure en faisant un mod
calcul dans le premier argument de Value
(voir les commentaires), et en effectuant tout le #
calcul à un niveau inférieur, cela donne une vitesse incroyable. Mais nous essayons toujours d'être une réponse légitime à un problème de golf.
Nous avons donc nos l
et nous d
pouvons les utiliser pour calculer le nombre de plaques d'immatriculation parfaites avec un motif LDDLLDL
. C'est le même nombre que pour le motif LDLLDDL
. En général, nous pouvons modifier l'ordre des séquences de chiffres de longueur différente selon
NrArrangements
le nombre de possibilités. Et s'il doit y avoir une lettre entre les séries de chiffres, les autres lettres ne sont pas fixes. Le Binomial
compte ces possibilités.
Il reste maintenant à parcourir tous les moyens possibles pour avoir des longueurs de chiffres de courses. r
passe à travers tous les numéros de pistes, à c
travers tous le nombre total de chiffres, et à p
travers toutes les partitions de c
avec
r
summands.
Le nombre total de partitions que nous examinons est deux de moins que le nombre de partitions n+1
, et les fonctions de partition se développent comme suit
exp(sqrt(n))
. Il est donc toujours facile d'améliorer le temps d'exécution en réutilisant les résultats (en parcourant les partitions dans un ordre différent), mais pour une amélioration fondamentale, nous devons éviter de regarder chaque partition séparément.
Le calcul rapide
Notez que (p+q)@r = p@r + q@r
. En soi, cela permet d'éviter certaines multiplications. Mais (p+q)#r = p#r + q#r
cela signifie que nous pouvons combiner par simple addition des polynômes correspondant à différentes partitions. Nous ne pouvons pas simplement les ajouter tous, car nous avons encore besoin de savoir avec lequel l
nous devons @
combiner, quel facteur nous devons utiliser et quelles #
extensions sont encore possibles.
Associons tous les polynômes correspondant aux partitions ayant la même somme et la même longueur, et prenons déjà en compte les multiples façons de répartir les longueurs des séries de chiffres. Contrairement à ce que j'ai spéculé dans les commentaires, je n'ai pas besoin de me soucier de la plus petite valeur utilisée ou de la fréquence d'utilisation, si je m'assure que je ne vais pas prolonger cette valeur.
Voici mon code 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" ;
}
Ceci utilise la bibliothèque GNU MP. Sur Debian, installez libgmp-dev
. Compiler avec g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. Le programme tire son argument de stdin. Pour le timing, utilisez echo 100 | time ./pl
.
À la fin, a[sum][length][i]
donne le nombre de façons dont les sum
chiffres dans les length
exécutions peuvent donner le nombre i
. Lors du calcul, au début de la m
boucle, il indique le nombre de façons de procéder avec des nombres supérieurs à m
. Tout commence par
a[0][0][1]=1
. Notez que ceci est un sur-ensemble des nombres dont nous avons besoin pour calculer la fonction pour des valeurs plus petites. Donc, presque au même moment, nous pourrions calculer toutes les valeurs jusqu’à n
.
Il n'y a pas de récursivité, nous avons donc un nombre fixe de boucles imbriquées. (Le niveau d'imbrication le plus profond est 6.) Chaque boucle passe par un nombre de valeurs qui est linéaire n
dans le pire des cas. Nous avons donc besoin que de temps polynomial. Si nous regardons de plus près l'imbrication i
et la j
boucle extend
, nous trouvons une limite supérieure pour j
la forme N/i
. Cela ne devrait donner qu'un facteur logarithmique pour la j
boucle. La boucle la plus interne f
(avec sumn
etc.) est similaire. Gardez également à l'esprit que nous calculons avec des nombres qui croissent rapidement.
Notez également que nous stockons O(n^3)
ces nombres.
Expérimentalement, j'obtiens ces résultats sur un matériel raisonnable (i5-4590S):
f(50)
nécessite une seconde et 23 Mo, f(100)
21 secondes et 166 Mo, f(200)
10 minutes et 1,5 Go, et f(300)
une heure et 5,6 Go. Cela suggère une complexité temporelle meilleure que O(n^5)
.
N
.