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.