间隙,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)
在不到一分钟的时间内完成计算,而在一小时之内就可以完成更多工作:
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
其中where k
是字母数4
。
类似地,我们得到了一系列的方法来求和 k
作为的系数的方式(1+x+...+x^9)^k
。如果有多于一个数字,我们需要将相应的列表与一个运算符组合起来,该运算符的d1#d2
位置应i
为所有d1[i1]*d2[i2]
where 的和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
并且可以使用它们来计算具有pattern的完美车牌的数量LDDLLDL
。与图案相同的数字LDLLDDL
。通常,我们可以根据需要更改不同长度的数字游程的顺序,
NrArrangements
并提供多种可能性。虽然数字之间必须有一个字母,但其他字母不是固定的。在Binomial
计算这些可能性。
现在,仍然需要通过所有可能的方式来获得游程数字的长度。 r
遍历所有运行次数,c
所有总数的位数以及带有
summands的p
所有分区。c
r
我们所看到的分区总数比的分区总数少两个n+1
,并且分区函数的增长如图所示
exp(sqrt(n))
。因此,尽管仍然有简单的方法可以通过重用结果(以不同的顺序遍历分区)来缩短运行时间,但从根本上来说,我们需要避免分别查看每个分区。
快速计算
注意(p+q)@r = p@r + q@r
。就其本身而言,这仅有助于避免某些乘法。但是,(p+q)#r = p#r + q#r
这意味着我们可以通过对应于不同分区的简单加法多项式进行组合。我们不能仅仅将它们全部添加,因为我们仍然需要知道与哪个对象有关。l
进行@
组合,必须使用哪些因子以及#
仍然可以进行哪些扩展。
让我们用相同的总和和长度组合与分区相对应的所有多项式,并已经考虑了多种分配数字游程长度的方法。与我在评论中推测的不同,如果我确定不会使用该最小值,则无需关心最小的使用值或使用频率。
这是我的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库。在debian上,安装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
(与sumn
etc)中是相似的。另外请记住,我们使用快速增长的数字进行计算。
另请注意,我们存储 O(n^3)
这些数字。
通过实验,我在合理的硬件(i5-4590S)上获得了以下结果:
f(50)
需要1秒和23 MB,f(100)
需要21秒和166 MB,f(200)
需要10分钟和1.5 GB,f(300)
需要1小时和5.6 GB。这表明时间复杂度要好于O(n^5)
。
N
。