GAP , 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
где k
число букв, здесь4
.
Точно так же мы получаем количество способов получить сумму цифр в серии k
цифр как коэффициенты (1+x+...+x^9)^k
. Если имеется более одного набора цифр, нам нужно объединить соответствующие списки с операцией d1#d2
, в i
которой в качестве значения в качестве суммы принимается сумма всех d1[i1]*d2[i2]
где . Вместе с тем, что он является билинейным, это дает хороший (но не очень эффективный) способ его вычисления.i1*i2=i
. Это свертка Дирихле, которая является просто произведением, если мы интерпретируем списки как коэффициенты рядов Дирхлета. Но мы уже использовали их как полиномы (конечные степенные ряды), и нет хорошего способа интерпретировать операцию для них. Я думаю, что это несоответствие является частью того, что затрудняет поиск простой формулы. Давайте все равно будем использовать его на полиномах и использовать те же обозначения #
. Легко вычислить, когда один операнд является мономом: мы имеем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
нам нужно @
объединить, какой фактор мы должны использовать, и какие #
расширения все еще возможны.
Давайте объединим все полиномы, соответствующие разделам с одинаковой суммой и длиной, и уже учтем несколько способов распределения длин серий цифр. В отличие от того, что я размышлял в комментариях, мне не нужно заботиться о наименьшем используемом значении или о том, как часто оно используется, если я буду уверен, что не буду расширять это значение.
Вот мой код 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
. Программа берет свой аргумент из стандартного ввода. Для выбора времени используйте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
т. Д.) Похож. Также имейте в виду, что мы вычисляем с быстро растущими числами.
Обратите внимание, что мы храним O(n^3)
эти числа.
Экспериментально я получаю эти результаты на разумном оборудовании (i5-4590S):
f(50)
требуется одна секунда и 23 МБ, f(100)
требуется 21 секунда и 166 МБ, f(200)
требуется 10 минут и 1,5 ГБ и f(300)
один час и 5,6 ГБ. Это предполагает сложность времени лучше, чем O(n^5)
.
N
.