间隙,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所有分区。cr
我们所看到的分区总数比的分区总数少两个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。