В 1960-м году Андрей Колмогоров и несколько других советских пионеров информатики собрались на научном семинаре и выдвинули «гипотезу
Через неделю 23-летний аспирант Анатолий Карацуба предложил метод умножения с оценкой времени работы
Историческое примечание: эта задача сейчас решается за
Любое число можно можно представить в виде многочлена, если вместо
Наивная формула для умножения многочленов, если раскрыть все скобки, выглядит так:
Единственный нюанс: в случае чисел, когда мы хотим получить их реальное представление, нужно предварительно провести «каррирование»:
const int base = 10;
void carry(int *a, int n) {
int d = 0;
for (int i = 0; i < n; i++) {
a[i] += d;
d = a[i] % base;
a[i] /= base;
}
}
Основание системы счисления может быть выбрано произвольно, но из соображений производительности его имеет смысл выбрать его как можно большим, избегая при этом переполнений. Например, при выборе основания long long
размера
Основная схема алгоритма очень простая: в нём умножение двух чисел длины
То, почему он работает за такую странную асимптотику, весьма неочевидно, и для этого мы сначала докажем более мощную теорему, которая говорит об асимптотике большого класса алгоритмов вида «разделяй-и-властвуй», заменяющих исходную задачу на
Мастер-теорема. Пусть имеется рекуррента:
Тогда:
-
A. Если
$c > \log_b a$ , то$T(n) = \Theta(n^c)$ . -
B. Если
$c = \log_b a$ , то$T(n) = \Theta(n^c \log n)$ . -
C. Если
$c < \log_b a$ , то$T(n) = \Theta(n^{\log_b a})$ .
Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет
A. Если
B. Если
C. Если
Примечание. Для более точных оценок асимптотики «мерджа» теорема ничего не говорит. Например, если мердж занимает
В то же время эта рекуррента под условия теоремы не попадает. Можно лишь получить неточные границы
Пусть у нас есть два многочлена
Теперь рекурсивно вычислим многочлены-произведения
А также многочлен
Результат умножения исходных многочленов теперь можно посчитать по следующей формуле — внимание, алгебра:
Корректность формулы можно проверить, просто выполнив нужные подстановки.
Обратим внимание, что результат умножения — многочлен размера
Если посчитать необходимые операции, то выясняется, что для перемножения двух многочленов размера
Пролистав пол-экрана выше, можно убедиться, что асимптотика всего алгоритма будет
Для простоты будем предполагать, что
-
Можно дополнить коэффициенты многочлена нулями до ближайшей степени двойки — в худшем случае это будет работать в
$2^{1.58} \approx 3$ раза дольше. -
Можно «отщепить» последний коэффициент от многочленов и свести задачу размера
$2k + 1$ к задаче размера$2k$ и константному количество сложений.
В этой статье мы будем использовать первый метод.
Основные соображения по поводу эффективной реализации:
-
Нужно выделять как можно меньше лишней памяти, для чего нужно переиспользовать имеющиеся массивы.
-
Все арифметические операции нужно реализовать как простые линейные проходы по массивам, чтобы компилятор смог их векторизовать.
-
Вместо использования базы вида
if (n == 1) c[0] = a[0] * b[0]
, имеет смысл, начиная с какого-то размера задачи, использовать более эффективное наивное умножение за квадрат.
Код основной рекурсивной процедуры:
void karatsuba(int *a, int *b, int *c, int n) {
if (n <= 64) {
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
c[i + j] += a[i] * b[j];
}
else {
int k = n / 2;
int l[k], r[k], t[n] = {0};
for (int i = 0; i < k; i++) {
l[i] = a[i] + a[k + i];
r[i] = b[i] + b[k + i];
}
karatsuba(l, r, t, k); // считает t
karatsuba(a, b, c, k); // считает p1
karatsuba(a + k, b + k, c + n, k); // считает p2
int *t1 = t, *t2 = t + k;
int *s1 = c, *s2 = c + k, *s3 = c + 2 * k, *s4 = c + 3 * k;
for (int i = 0; i < k; i++) {
int c1 = s2[i] + t1[i] - s1[i] - s3[i];
int c2 = s3[i] + t2[i] - s2[i] - s4[i];
c[k + i] = c1;
c[n + i] = c2;
}
}
}
После трёх рекурсивных вызовов массив
После этого, для подсчета самого многочлена
-
$s_1$ : не меняется — это первая половина$p_1$ -
$s_2$ : выражается как$s_2 + t_1 - s_1 - s_3$ , то есть изменяется на «первую» половину$t - p_1 - p_2$ -
$s_3$ : выражается как$s_3 + t_2 - s_2 - s_4$ , то есть изменяется на «торую» половину$t - p_1 - p_2$ -
$s_4$ : не меняется — это вторая половина$p_2$
Из-за векторизации важно использовать максимально «лёгкий» тип данных и при возможности компилировать с AVX:
#pragma GCC optimize("O3")
#pragma GCC target("avx2")
Реализация достаточно эффективна: она может перемножить два многочлена размера
В олимпиадных задачах длинное умножение само по себе встречается крайне редко. Гораздо чаще оно используется для подсчета каких-либо комбинаторных объектов через умножение многочленов.
Для примера мы решим задачу «Вор в магазине» с Educational Codeforces Round:
Дано
$n \leq 1000$ типов предметов разных положительных стоимостей. Каждая стоимость не превосходит$1000$ . Предметов каждого типа доступно бесконечное количество.Требуется определить всевозможные суммы предметов, которые можно набрать, взяв ровно
$k$ предметов.
Составим многочлен степени
Если возвести этот многочлен в
Если посмотреть на эту формулу комбинаторно, то в получившемся многочлене коэффициент при
Для возведения многочлена в
const int maxn = (1<<20);
int a[maxn], res[maxn];
// мы не всегда хотим модифицировать исходные массивы при перемножении
// поэтому напишем обертку, которая работает, как *=
void mul(int *x, int *y, int n) {
int tx[n], ty[n];
memcpy(tx, x, sizeof tx);
memcpy(ty, y, sizeof ty);
memset(x, 0, sizeof tx);
karatsuba(tx, ty, x, n);
}
void binpow(int k) {
res[0] = 1;
int len = 1024;
while (k > 1) {
if (k & 1)
mul(res, a, len);
mul(a, a, len);
k /= 2;
len *= 2;
}
mul(res, a, len);
}
// осталось скормить функции binpow бинарный массив a,
// и res будет содержать a в k-той степени
В асимптотике будет учитёно только последнее (самое больше) умножение.
Решение в условиях задачи работает за 3 секунды с ограничением в 5. Его можно значительно ускорить, если вместо int
-ов использовать байты или даже биты по аналогии с битсетом, потому что нас не интересует количество способов набрать какую-то сумму, а только равно ли это число нулю.
Похожий метод можно применить к матричному умножению — это называется алгоритмом Штрассена. В нём две матрицы разбиваются на
И в алгоритме Штрассена, и в алгоритме Карацубы можно достичь и лучшей асимптотики, если разбивать объекты на большее число частей. Однако, в реальности это не применяется, потому что в асимптотиках подобных улгоритмов скрыта непрактично большая константа.