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" ;
}
Для цього використовується бібліотека MP GNU. На 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
тощо) схожа. Також майте на увазі, що ми обчислюємо числа, які швидко ростуть.
Зауважте також, що ми зберігаємо 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
.