간격 , 416 바이트
코드 크기에서 이길 수는 없으며 일정한 시간과는 거리가 멀지 만 수학을 사용하여 속도를 높이십시오!
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;
불필요한 공백을 짜내고 416 바이트로 한 줄을 얻으려면 다음을 파이프하십시오.
sed -e 's/^ *//' -e 's/in \[/in[/' -e 's/ do/do /' | tr -d \\n
예전의 "Windows XP 용으로 설계된"랩탑은 f(10)
1 분 안에 계산할 수 있고 1 시간 안에 훨씬 더 나아갈 수 있습니다 .
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
작동 원리
먼저 패턴에 맞는 완벽한 번호판의 수만 알고 싶다고 가정합니다 LDDLLDL
. 여기서 L
문자와
D
숫자를 나타냅니다. 글자가 값을 줄 수있는 방법의 수 와 숫자에서 얻은 값에 대한 유사한 목록 을 제공하는 l
숫자 목록 이
있다고 가정합니다 . 그런 다음 일반적인 값으로 완벽한 번호판의 숫자는 그냥
, 우리 모두 이상이를 합산하여 우리의 패턴 모두 완벽한 번호판의 번호를 . 로이 합계를 가져 오는 작업을 나타냅니다 .l[i]
i
d
i
l[i]*d[i]
i
l@d
이 목록을 얻는 가장 좋은 방법은 모든 조합과 계산을 시도하는 것이었지만 문자와 숫자에 대해 독립적 으로이 작업을 수행 할 수 있습니다
. 패턴에 맞는 모든 판을 통과하는 26^4+10^3
경우 대신 26^4*10^3
케이스를 봅니다. 그러나 우리는 훨씬 더 잘 할 수 있습니다 : 문자 수가 어디에 있는지 에 대한 l
계수 목록 일뿐
입니다.(x+x^2+...+x^26)^k
k
4
마찬가지로, 우리 k
는 계수로 자릿수 의 자릿수를 얻는 방법의 수를 얻습니다 (1+x+...+x^9)^k
. 하나 이상의 자릿수가있는 경우 해당 목록 d1#d2
을 위치 i
에있는 모든 d1[i1]*d2[i2]
위치 의 합이 값인 연산과 결합해야합니다 i1*i2=i
. 이것은 Dirichlet 컨볼 루션입니다. 목록을 Dirchlet 시리즈의 계수로 해석하면 제품입니다. 그러나 우리는 이미 그것들을 다항식 (유한 한 파워 시리즈)으로 사용했으며, 그것들을위한 연산을 해석하는 좋은 방법은 없습니다. 이 불일치가 간단한 수식을 찾기 어려운 부분이라고 생각합니다. 어쨌든 다항식에 사용하고 같은 표기법을 사용합시다 #
. 하나의 피연산자가 단일 일 때 쉽게 계산할 수 있습니다.p(x) # x^k = p(x^k)
. 그것이 쌍 선형이라는 사실과 함께, 이것은 그것을 계산하는 좋은 (매우 효율적이지 않은) 방법을 제공합니다.
참고 k
편지를 최대에서의 값을 제공 26k
하면서, k
한 자리의 값을 제공 할 수 있습니다 9^k
. 따라서 우리는 종종 d
다항식 에서 불필요한 높은 힘을 얻습니다 . 그것들을 제거하기 위해 모듈로를 계산할 수 있습니다 x^(maxlettervalue+1)
. 이것은 빠른 속도를 제공하지만, 즉시 눈치 채지 못했지만 골프를 돕습니다. 우리는 이제 학위가 학위 d
보다 크지 않다는 것을 알기 때문에 l
결승전의 상한을 단순화합니다 Sum
. 우리 mod
는 첫 번째 논점에서 계산을 함으로써 더 빠른 속도를 얻습니다 Value
(주석 참조). 그리고 #
더 낮은 수준에서 전체 계산을 수행 하면 놀라운 속도를 얻 습니다. 그러나 우리는 여전히 골프 문제에 대한 합법적 인 답변을 찾고 있습니다.
그래서 우리는 우리를 가지고 l
와 d
패턴으로 완벽한 번호판의 수를 계산하는 데 사용할 수 있습니다 LDDLLDL
. 패턴과 같은 숫자입니다 LDLLDDL
. 일반적으로 원하는 길이가 다른 숫자의 런 순서를 변경할
NrArrangements
수 있으며 가능한 수를 제공합니다. 숫자 사이에 하나의 문자가 있어야하지만 다른 문자는 고정되어 있지 않습니다. 는 Binomial
이러한 가능성을 계산합니다.
이제 길이가 런 자릿수를 갖는 가능한 모든 방법을 통해 실행됩니다. r
, 실행의 모든 번호를 통해 실행 c
모든 숫자 총 숫자를 통해, 그리고 p
모든 파티션을 통해 c
와
r
피가수.
우리가 보는 총 파티션 수는 파티션 수보다 두 개가 적으며 n+1
파티션 기능은 다음과 같이 커
exp(sqrt(n))
집니다. 따라서 파티션을 통해 실행되는 결과를 다른 순서로 다시 실행하여 실행 시간을 개선하는 쉬운 방법이 여전히 있지만 기본 개선을 위해서는 각 파티션을 따로 보지 않아야합니다.
빠른 컴퓨팅
참고하십시오 (p+q)@r = p@r + q@r
. 그 자체만으로도 곱셈을 피할 수 있습니다. 그러나 (p+q)#r = p#r + q#r
그것 과 함께 우리는 다른 파티션에 해당하는 간단한 추가 다항식으로 결합 할 수 있습니다. 우리가 아직있는 알 필요가 있기 때문에 우리는 그들 모두를 추가 할 수 없습니다 l
우리가해야 @
우리가 사용해야하고, 어떤 어떤 요인, -combine #
-Extensions은 여전히 가능합니다.
파티션에 해당하는 모든 다항식을 동일한 합과 길이로 결합하고 이미 자릿수 길이를 분산시키는 여러 가지 방법을 설명합시다. 내가 의견에서 추측 한 것과는 달리, 내가 그 값으로 확장되지 않도록하기 위해 사용 된 가장 작은 값이나 사용 빈도에 대해 신경 쓸 필요가 없습니다.
내 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" ;
}
이것은 GNU MP 라이브러리를 사용합니다. 데비안에서는을 설치하십시오 libgmp-dev
. 로 컴파일하십시오 g++ -std=c++11 -O3 -o pl pl.cpp -lgmp -lgmpxx
. 이 프로그램은 stdin에서 인수를 취합니다. 타이밍은을 사용하십시오 echo 100 | time ./pl
.
마지막에는 런
숫자 a[sum][length][i]
가 sum
숫자를 제공 할 수있는 방법 length
의 수를 나타 i
냅니다. 계산 중에 m
루프 시작시 보다 큰 숫자로 수행 할 수있는 여러 가지 방법을 제공합니다 m
. 모두로 시작합니다
a[0][0][1]=1
. 이것은 더 작은 값에 대한 함수를 계산하는 데 필요한 숫자의 상위 집합입니다. 따라서 거의 동시에 모든 값을까지 계산할 수 n
있습니다.
재귀가 없으므로 고정 된 수의 중첩 루프가 있습니다. (가장 깊은 중첩 수준은 6입니다.) 각 루프는 n
최악의 경우 선형 인 여러 값을 통과합니다 . 따라서 다항식 시간 만 필요합니다. 우리가 가까이 중첩에서 보면 i
와 j
의 루프 extend
, 우리에 대한 상한 찾을 j
양식을 N/i
. j
루프에 대한 로그 팩터 만 제공해야합니다 . 가장 안쪽의 루프 f
(with sumn
등)는 비슷합니다. 또한 빠르게 증가하는 숫자로 계산합니다.
우리는 또한 O(n^3)
이 숫자 들을 저장 합니다.
실험적으로 합리적인 하드웨어 (i5-4590S)에서이 결과를 얻습니다. f(50)
1 초 및 23MB, f(100)
21 초 및 166MB , f(200)
10 분 및 1.5GB, f(300)
1 시간 5.6GB
가 필요합니다. 이는보다 복잡한 시간을 제안합니다 O(n^5)
.
N
.