GAP , 416 byte
Không giành chiến thắng về kích thước mã và cách xa thời gian liên tục, nhưng sử dụng toán học để tăng tốc rất nhiều!
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;
Để vắt kiệt khoảng trắng không cần thiết và lấy một dòng có 416 byte, hãy chuyển qua đây:
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
Máy tính xách tay "được thiết kế cho Windows XP" cũ của tôi có thể tính toán f(10)
trong vòng chưa đầy một phút và tiến xa hơn trong vòng một giờ:
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
Làm thế nào nó hoạt động
Giả sử rằng trước tiên chúng ta chỉ muốn biết số lượng biển số xe hoàn hảo phù hợp với mẫu LDDLLDL
, trong đó L
biểu thị một chữ cái và
D
biểu thị một chữ số. Giả sử chúng ta có một danh sách l
các số sao
l[i]
cho số cách mà các chữ cái có thể đưa ra giá trị i
và một danh sách tương tự d
cho các giá trị chúng ta nhận được từ các chữ số. Sau đó, số lượng biển số xe hoàn hảo với giá trị chung i
chỉ là
l[i]*d[i]
, và chúng tôi có được số lượng tất cả các biển số xe hoàn hảo với mẫu của chúng tôi bằng cách tổng hợp tất cả i
. Hãy biểu thị hoạt động của số tiền này bằng cách l@d
.
Bây giờ ngay cả khi cách tốt nhất để có được các danh sách này là thử tất cả các kết hợp và đếm, chúng ta có thể làm điều này một cách độc lập cho các chữ cái và chữ số, xem xét 26^4+10^3
các trường hợp thay vì các 26^4*10^3
trường hợp khi chúng ta chạy qua tất cả các tấm phù hợp với mẫu. Nhưng chúng ta có thể làm tốt hơn nhiều: l
chỉ là danh sách các hệ số
(x+x^2+...+x^26)^k
nơi k
là số chữ cái, ở đây 4
.
Tương tự, chúng ta có được số cách để có được tổng các chữ số trong một k
chữ số là các hệ số của (1+x+...+x^9)^k
. Nếu có nhiều hơn một chữ số, chúng ta cần kết hợp các danh sách tương ứng với một thao tác d1#d2
tại vị trí i
có giá trị bằng tổng của tất cả các d1[i1]*d2[i2]
vị trí . Cùng với thực tế là nó là song tuyến tính, điều này mang lại một cách hay (nhưng không hiệu quả lắm) để tính toán nó.i1*i2=i
. Đây là tích chập Dirichlet, chỉ là sản phẩm nếu chúng ta giải thích các danh sách là hệ số của chuỗi Dirchlet. Nhưng chúng tôi đã sử dụng chúng như các đa thức (chuỗi lũy thừa hữu hạn), và không có cách nào tốt để diễn giải hoạt động cho chúng. Tôi nghĩ rằng sự không phù hợp này là một phần của những gì làm cho nó khó tìm thấy một công thức đơn giản. Chúng ta hãy sử dụng nó trên đa thức và sử dụng cùng một ký hiệu #
. Thật dễ dàng để tính toán khi một toán hạng là một đơn thức: chúng ta cóp(x) # x^k = p(x^k)
Lưu ý rằng k
các chữ cái cho giá trị tối đa 26k
, trong khi các k
chữ số đơn có thể cho giá trị là 9^k
. Vì vậy, chúng ta sẽ thường có được sức mạnh cao không cần thiết trong d
đa thức. Để loại bỏ chúng, chúng ta có thể tính toán modulo x^(maxlettervalue+1)
. Điều này mang lại tốc độ lớn và mặc dù tôi đã không chú ý ngay lập tức, thậm chí còn giúp chơi gôn, bởi vì bây giờ chúng ta biết rằng mức độ d
không lớn hơn mức củal
, điều đó giúp đơn giản hóa giới hạn trên trong trận chung kết Sum
. Chúng tôi thậm chí còn tăng tốc tốt hơn bằng cách thực hiện một mod
phép tính trong đối số đầu tiên của Value
(xem bình luận) và thực hiện toàn bộ #
tính toán ở mức thấp hơn giúp tăng tốc đáng kinh ngạc. Nhưng chúng tôi vẫn đang cố gắng trở thành một câu trả lời chính đáng cho vấn đề chơi gôn.
Vì vậy, chúng tôi đã có của chúng tôi l
và d
và có thể sử dụng chúng để tính toán số lượng tấm giấy phép hoàn hảo với mẫuLDDLLDL
. Đó là con số tương tự như đối với mẫu LDLLDDL
. Nói chung, chúng ta có thể thay đổi thứ tự các chữ số có độ dài khác nhau tùy thích,
NrArrangements
đưa ra số lượng khả năng. Và trong khi phải có một chữ cái giữa các chữ số, các chữ cái khác không được sửa. Tính Binomial
những khả năng này.
Bây giờ nó vẫn còn để chạy qua tất cả các cách có thể có độ dài của chữ số chạy. r
chạy qua tất cả số lần chạy, c
qua tất cả tổng số chữ số vàp
qua tất cả các phân vùng c
có triệu hồi
r
.
Tổng số phân vùng chúng tôi xem xét ít hơn hai phân vùng n+1
và các chức năng phân vùng phát triển như thế nào
exp(sqrt(n))
. Vì vậy, mặc dù vẫn có những cách dễ dàng để cải thiện thời gian chạy bằng cách sử dụng lại kết quả (chạy qua các phân vùng theo thứ tự khác nhau), để cải thiện cơ bản, chúng ta cần tránh nhìn vào từng phân vùng một cách riêng biệt.
Tính toán nhanh
Lưu ý rằng (p+q)@r = p@r + q@r
. Tự nó, điều này chỉ giúp tránh một số phép nhân. Nhưng cùng với (p+q)#r = p#r + q#r
nó có nghĩa là chúng ta có thể kết hợp bằng các đa thức bổ sung đơn giản tương ứng với các phân vùng khác nhau. Chúng ta không thể thêm tất cả chúng, bởi vì chúng ta vẫn cần biết l
chúng ta phải làm gì với@
kết hợp với yếu tố nào, yếu tố nào chúng ta phải sử dụng và yếu #
tố nào vẫn có thể xảy ra.
Chúng ta hãy kết hợp tất cả các đa thức tương ứng với các phân vùng có cùng tổng và độ dài và đã tính đến nhiều cách phân phối độ dài của các chữ số. Khác với những gì tôi suy đoán trong các nhận xét, tôi không cần quan tâm đến giá trị được sử dụng nhỏ nhất hoặc mức độ thường xuyên sử dụng, nếu tôi chắc chắn rằng mình sẽ không gia hạn với giá trị đó.
Đây là mã C ++ của tôi:
#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" ;
}
Cái này sử dụng thư viện GNU MP. Trên debian, cài đặt libgmp-dev
. Biên dịch với g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. Chương trình lấy lý lẽ của nó từ stdin. Để tính thời gian, sử dụng echo 100 | time ./pl
.
Cuối cùng, a[sum][length][i]
đưa ra số cách mà các sum
chữ số trong các length
lần chạy có thể đưa ra số i
. Trong quá trình tính toán, ở đầu m
vòng lặp, nó đưa ra số cách có thể được thực hiện với số lớn hơn m
. Tất cả bắt đầu với
a[0][0][1]=1
. Lưu ý rằng đây là một siêu số của các số chúng ta cần để tính hàm cho các giá trị nhỏ hơn. Vì vậy, gần như cùng một lúc, chúng ta có thể tính toán tất cả các giá trị lên đến n
.
Không có đệ quy, vì vậy chúng tôi có một số vòng lặp lồng nhau cố định. (Mức lồng sâu nhất là 6.) Mỗi vòng lặp đi qua một số giá trị là tuyến tính trong n
trường hợp xấu nhất. Vì vậy, chúng ta chỉ cần thời gian đa thức. Nếu chúng ta nhìn sâu hơn về các lồng nhau i
và j
vòng trong extend
, chúng tôi thấy một giới hạn trên cho j
các hình thức N/i
. Điều đó chỉ nên đưa ra một yếu tố logarit cho j
vòng lặp. Vòng lặp trong cùng trong f
(với sumn
vv) là tương tự. Cũng nên nhớ rằng chúng tôi tính toán với những con số tăng nhanh.
Cũng lưu ý rằng chúng tôi lưu trữ O(n^3)
những con số này.
Thực nghiệm, tôi nhận được các kết quả này trên phần cứng hợp lý (i5-4590S):
f(50)
cần một giây và 23 MB, f(100)
cần 21 giây và 166 MB, f(200)
cần 10 phút và 1,5 GB, và f(300)
cần một giờ và 5,6 GB. Điều này cho thấy một sự phức tạp thời gian tốt hơn O(n^5)
.
N
.