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.