LUKA , 416 bajtów
Nie wygra rozmiaru kodu, i to daleko od stałego czasu, ale używa matematyki, aby znacznie przyspieszyć!
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;
Aby wycisnąć niepotrzebne białe znaki i uzyskać jedną linię z 416 bajtami, przeciągnij przez to:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Mój stary laptop „zaprojektowany dla systemu Windows XP” może obliczyć f(10)
w mniej niż minutę i pójść znacznie dalej w ciągu godziny:
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
Jak to działa
Załóżmy, że najpierw chcemy znać tylko liczbę idealnych tablic rejestracyjnych pasujących do wzoru LDDLLDL
, gdzie L
oznacza literę, a
D
cyfrę. Załóżmy, że mamy taką listę l
liczb, która
l[i]
podaje liczbę sposobów, w jakie litery mogą podawać wartość i
, oraz podobną listę d
wartości, które otrzymujemy z cyfr. Zatem liczba idealnych tablic rejestracyjnych o wspólnej wartości i
jest po prostu
l[i]*d[i]
i otrzymujemy liczbę wszystkich idealnych tablic rejestracyjnych według naszego wzoru, sumując to wszystko i
. Oznaczmy operację uzyskania tej sumy l@d
.
Teraz nawet jeśli najlepszym sposobem na uzyskanie tych list było wypróbowanie wszystkich kombinacji i policzenie, możemy to zrobić niezależnie dla liter i cyfr, patrząc na 26^4+10^3
przypadki zamiast 26^4*10^3
przypadków, gdy po prostu przeglądamy wszystkie tablice pasujące do wzoru. Ale możemy zrobić znacznie lepiej: tutaj l
jest tylko lista współczynników,
(x+x^2+...+x^26)^k
gdzie k
jest liczba liter4
.
Podobnie otrzymujemy liczbę sposobów uzyskania sumy cyfr w szeregu k
cyfr jako współczynników (1+x+...+x^9)^k
. Jeśli jest więcej niż jeden ciąg cyfr, musimy połączyć odpowiednie listy z operacją, d1#d2
która na pozycji i
ma jako wartość sumę wszystkich d1[i1]*d2[i2]
gdzie . Wraz z faktem, że jest dwuliniowy, daje to miły (ale niezbyt wydajny) sposób na jego obliczenie.i1*i2=i
. Jest to splot Dirichleta, który jest produktem, jeśli interpretujemy listy jako współczynniki serii Dirchleta. Ale używaliśmy ich już jako wielomianów (skończone szeregi mocy) i nie ma dobrego sposobu na interpretację ich działania. Myślę, że to niedopasowanie jest częścią tego, co utrudnia znalezienie prostej formuły. W każdym razie użyjmy go na wielomianach i zastosujmy tę samą notację #
. Łatwo jest obliczyć, gdy jeden operand jest monomialny: mamyp(x) # x^k = p(x^k)
Zauważ, że k
litery dają co najwyżej wartość 26k
, podczas gdy k
pojedyncze cyfry mogą dawać wartość 9^k
. Tak więc często otrzymamy niepotrzebne wysokie moce w d
wielomianu. Aby się ich pozbyć, możemy obliczyć modulo x^(maxlettervalue+1)
. Daje to duże przyspieszenie i chociaż nie zauważyłem od razu, nawet pomaga w grze w golfa, ponieważ teraz wiemy, że stopieńd
nie jest większy niż ten l
, co upraszcza górną granicę w finale Sum
. Jeszcze lepsze przyspieszenie uzyskujemy, wykonując mod
obliczenia w pierwszym argumencie Value
(patrz komentarze), a wykonanie całego #
obliczenia na niższym poziomie daje niesamowite przyspieszenie. Ale wciąż staramy się być uzasadnioną odpowiedzią na problem golfowy.
Więc mamy nasze l
id
i można z nich korzystać, aby obliczyć liczbę doskonałych tablic rejestracyjnych z wzorem LDDLLDL
. To ta sama liczba, co dla wzoru LDLLDDL
. Ogólnie rzecz biorąc, możemy zmienić kolejność ciągów cyfr o różnej długości, jak nam się podoba,
NrArrangements
daje to szereg możliwości. I chociaż między ciągami cyfr musi znajdować się jedna litera, pozostałe litery nie są ustalone. Binomial
Liczy te możliwości.
Teraz pozostaje przejść przez wszystkie możliwe sposoby uzyskania długości cyfr przebiegów. r
przebiega przez wszystkie liczby przebiegów,c
przez całkowitą liczbę cyfr i p
przez wszystkie partycje c
z
r
sumami.
Całkowita liczba partycji, które przeglądamy, jest o dwa razy mniejsza niż liczba partycji n+1
, a funkcje partycji rosną podobnie
exp(sqrt(n))
. Tak więc, chociaż wciąż istnieją proste sposoby na poprawę czasu działania poprzez ponowne wykorzystanie wyników (przeglądanie partycji w innej kolejności), dla fundamentalnej poprawy musimy unikać oddzielnego patrzenia na każdą partycję.
Obliczam to szybko
Zauważ, że (p+q)@r = p@r + q@r
. Samo to pomaga po prostu uniknąć mnożenia. Ale razem z (p+q)#r = p#r + q#r
tym oznacza, że możemy łączyć poprzez proste dodawanie wielomianów odpowiadających różnym partycjom. Nie możemy po prostu dodać ich wszystkich, ponieważ wciąż musimy wiedzieć, z czym l
musimy @
połączyć, jaki czynnik musimy zastosować i które #
rozszerzenia są nadal możliwe.
Połączmy wszystkie wielomiany odpowiadające partycjom z tą samą sumą i długością, i już uwzględniliśmy wiele sposobów dystrybucji długości ciągów cyfr. W odróżnieniu od tego, co spekulowałem w komentarzach, nie muszę się martwić o najmniejszą używaną wartość lub częstotliwość jej używania, jeśli upewnię się, że nie będę rozszerzać o tę wartość.
Oto mój kod 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" ;
}
Wykorzystuje bibliotekę GNU MP. W Debianie zainstaluj libgmp-dev
. Kompiluj z g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. Program bierze swój argument ze standardowego wejścia. Do pomiaru czasu użyj echo 100 | time ./pl
.
Na końcu a[sum][length][i]
podaje liczbę sposobów, w jakie sum
cyfry w length
seriach mogą podawać liczbę i
. Podczas obliczeń, na początku m
pętli, podaje liczbę sposobów, które można zrobić z liczbami większymi niż m
. Wszystko zaczyna się od
a[0][0][1]=1
. Zauważ, że jest to nadzbiór liczb potrzebnych do obliczenia funkcji dla mniejszych wartości. Więc prawie w tym samym czasie moglibyśmy obliczyć wszystkie wartości do n
.
Nie ma rekurencji, więc mamy stałą liczbę zagnieżdżonych pętli. (Najgłębszy poziom zagnieżdżenia to 6.) Każda pętla przechodzi przez szereg wartości, które n
w najgorszym przypadku są liniowe . Potrzebujemy tylko czasu wielomianowego. Jeśli przyjrzymy się bliżej zagnieżdżeniu i
i j
pętli extend
, znajdziemy górną granicę j
formy N/i
. To powinno dać jedynie współczynnik logarytmiczny dla j
pętli. Najbardziej wewnętrzna pętla w f
(zsumn
etc) jest podobna. Należy również pamiętać, że obliczamy z szybko rosnącymi liczbami.
Pamiętaj również, że przechowujemy O(n^3)
te liczby.
Eksperymentalnie otrzymuję te wyniki na rozsądnym sprzęcie (i5-4590S):
f(50)
potrzebuje jednej sekundy i 23 MB, f(100)
potrzebuje 21 sekund i 166 MB, f(200)
potrzebuje 10 minut i 1,5 GB oraz f(300)
potrzebuje godziny i 5,6 GB. To sugeruje złożoność czasu lepszą niż O(n^5)
.
N
.