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 Loznacza literę, a
Dcyfrę. Załóżmy, że mamy taką listę lliczb, która
l[i]podaje liczbę sposobów, w jakie litery mogą podawać wartość i, oraz podobną listę dwartości, które otrzymujemy z cyfr. Zatem liczba idealnych tablic rejestracyjnych o wspólnej wartości ijest 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^3przypadki 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 ljest tylko lista współczynników,
(x+x^2+...+x^26)^kgdzie kjest liczba liter4 .
Podobnie otrzymujemy liczbę sposobów uzyskania sumy cyfr w szeregu kcyfr 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#d2która na pozycji ima 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 klitery 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 dwielomianu. 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 modobliczenia 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 lid 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,
NrArrangementsdaje to szereg możliwości. I chociaż między ciągami cyfr musi znajdować się jedna litera, pozostałe litery nie są ustalone. BinomialLiczy te możliwości.
Teraz pozostaje przejść przez wszystkie możliwe sposoby uzyskania długości cyfr przebiegów. rprzebiega przez wszystkie liczby przebiegów,c przez całkowitą liczbę cyfr i pprzez wszystkie partycje cz
rsumami.
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#rtym 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 lmusimy @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 lengthseriach mogą podawać liczbę i. Podczas obliczeń, na początku mpę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 nw najgorszym przypadku są liniowe . Potrzebujemy tylko czasu wielomianowego. Jeśli przyjrzymy się bliżej zagnieżdżeniu ii jpętli extend, znajdziemy górną granicę jformy N/i. To powinno dać jedynie współczynnik logarytmiczny dla jpę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.