На днях нужно было разобраться с этим алгоритмом, но беглый поиск в google ничего путнего не дал. На Хабре тоже нашлась только одна статья, которая мне не особо помогла. Разобравшись, попробую поделиться с общественностью в доступной форме:
Алгоритм Карацубы — метод быстрого умножения со сложностью вычисления nlog23. В то время, как наивный алгоритм, умножение в столбик, требует n2 операций. Следует заметить, что при длине чисел короче нескольких десятков знаков (точнее определяется экспериментально), быстрее работает обычное умножение.
Представим, что есть два числа A и B длиной n в какой-то системе счисления BASE:
A = an-1an-2...a0
B = bn-1an-2...a0, где a?, b? — значение в соотв. разряде числа.
Каждое из них можно представить в виде суммы их двух частей, половинок длиной m = n / 2 (если n нечетное, то одна часть короче другой на один разряд:
A0 = am-1am-2...a0
A1 = an-1an-2...am
A = A0 + A1 * BASEm
B0 = bm-1bm-2...b0
B1 = bn-1bn-2...bm
B = B0 + B1 * BASEm
Тогда: A * B =( A0 + A1 * BASEm ) * ( B0 + B1 * BASEm ) = A0 * B0 + A0 * B1 * BASEm + A1 * B0 * BASEm + A1 * B1 * BASE2 * m = A0 * B0 + ( A0 * B1 + A1 * B0 ) * BASEm + A1 * B1 * BASE2 * m
Здесь нужно 4 операции умножения (части формулы * BASE? * m не являются умножением, фактически указывая место записи результата, разряд). Но с другой стороны:
( A0 + A1 ) * ( B0 + B1 ) = A0 * B0 + A0 * B1 + A1 * B0 + A1 * B1
Посмотрев на выделенные части в обоих формулах. После несложных преобразований количество операций умножения можно свести к 3-м, заменив два умножения на одно и несколько операций сложения и вычитания, время выполнения которых на порядок меньше:
A0 * B1 + A1 * B0 =( A0 + A1 ) * ( B0 + B1 ) — A0 * B0 — A1 * B1
Окончательный вид выражения:
A * B =A0 * B0 + (( A0 + A1 ) * ( B0 + B1 ) — A0 * B0 — A1 * B1 ) * BASEm + A1 * B1 * BASE2 * m
Графическое представление:
Для примера умножим два восьмизначных числа в десятичной системе 12345 и 98765:
Из изображении явно видна рекурсивная природа алгоритма. Для числа длиной менее четырех разрядов применялось наивное умножение.
Наверное, следует начать с того, как хранятся длинные числа. Длинные числа удобно представлять в виде массивов, где каждый элемент соответсвует разряду, причем младшие разряды хранятся в элементах с меньшими индексами (то есть задом-наперед) — так их удобнее обрабатывать. Например:
Для увеличения быстродействия желательно выбрать за основание системы счисления максимальное число в рамках базовых типов. Но при этом на него накладываются следующие условия:
Ниже приведена рабочая функция умножения с комментариями со всеми необходимыми вспомогательными объявлениями и функциями. Для более высокой производительности следует поменять основание системы счисления, тип для хранения разрядов и раскоментировать небольшой отрывок кода в месте отвечающем за наивное умножение:
Алгоритм
Алгоритм Карацубы — метод быстрого умножения со сложностью вычисления nlog23. В то время, как наивный алгоритм, умножение в столбик, требует n2 операций. Следует заметить, что при длине чисел короче нескольких десятков знаков (точнее определяется экспериментально), быстрее работает обычное умножение.
Представим, что есть два числа A и B длиной n в какой-то системе счисления BASE:
A = an-1an-2...a0
B = bn-1an-2...a0, где a?, b? — значение в соотв. разряде числа.
Каждое из них можно представить в виде суммы их двух частей, половинок длиной m = n / 2 (если n нечетное, то одна часть короче другой на один разряд:
A0 = am-1am-2...a0
A1 = an-1an-2...am
A = A0 + A1 * BASEm
B0 = bm-1bm-2...b0
B1 = bn-1bn-2...bm
B = B0 + B1 * BASEm
Тогда: A * B =
Здесь нужно 4 операции умножения (части формулы * BASE? * m не являются умножением, фактически указывая место записи результата, разряд). Но с другой стороны:
( A0 + A1 ) * ( B0 + B1 ) = A0 * B0 + A0 * B1 + A1 * B0 + A1 * B1
Посмотрев на выделенные части в обоих формулах. После несложных преобразований количество операций умножения можно свести к 3-м, заменив два умножения на одно и несколько операций сложения и вычитания, время выполнения которых на порядок меньше:
A0 * B1 + A1 * B0 =
Окончательный вид выражения:
A * B =
Графическое представление:
Пример
Для примера умножим два восьмизначных числа в десятичной системе 12345 и 98765:
Из изображении явно видна рекурсивная природа алгоритма. Для числа длиной менее четырех разрядов применялось наивное умножение.
Реализация на C++
Наверное, следует начать с того, как хранятся длинные числа. Длинные числа удобно представлять в виде массивов, где каждый элемент соответсвует разряду, причем младшие разряды хранятся в элементах с меньшими индексами (то есть задом-наперед) — так их удобнее обрабатывать. Например:
int long_value[] = { 9, 8, 7, 6, 5, 4} //представление числа 456789
Для увеличения быстродействия желательно выбрать за основание системы счисления максимальное число в рамках базовых типов. Но при этом на него накладываются следующие условия:
- Квадрат максимального числа в выбранной системе счисления должен помещаться в выбранный базовый тип. Это необходимо для хранения произведения одного разряда на другой в промежуточных вычислениях.
- Выбранный базовый тип желательно брать знаковый. Это позволить избавиться от нескольких промежуточных нормализаций.
- Лучше, чтобы в разряд помещалось сумма из нескольких квадратов максимального числа. Это позволит избавиться от нескольких промежуточных нормализаций.
Ниже приведена рабочая функция умножения с комментариями со всеми необходимыми вспомогательными объявлениями и функциями. Для более высокой производительности следует поменять основание системы счисления, тип для хранения разрядов и раскоментировать небольшой отрывок кода в месте отвечающем за наивное умножение:
- #include <cstring>
-
- #define BASE 10 //система счисления
- #define MIN_LENGTH_FOR_KARATSUBA 4 //числа короче умножаются квадратичным алгоритмом
- typedef int digit; //взят только для разрядов числа
- typedef unsigned long int size_length; //тип для длинны числа
-
- using namespace std;
-
- struct long_value { //тип для длинных чисел
- digit *values; //массив с цифрами числа записанными в обратном порядке
- size_length length; //длинна числа
- };
-
- long_value sum(long_value a, long_value b) {
- /* функция для суммирования двух длинных чисел. Если суммируются числа разной длинны
- * то более длинное передется в качестве первого аргумента. Возвращает новое
- * ненормализованное число.
- */
- long_value s;
- s.length = a.length + 1;
- s.values = new digit[s.length];
-
- s.values[a.length - 1] = a.values[a.length - 1];
- s.values[a.length] = 0;
- for (size_length i = 0; i < b.length; ++i)
- s.values[i] = a.values[i] + b.values[i];
- return s;
- }
-
- long_value &sub(long_value &a, long_value b) {
- /*функция для вычитания одного длинного числа из другого. Изменяет содержимое первого
- * числа. Возвращает ссылку на первое число. Результат не нормализован.
- */
- for (size_length i = 0; i < b.length; ++i)
- a.values[i] -= b.values[i];
- return a;
- }
-
- void normalize(long_value l) {
- /*Нормализация числа - приведение каждого разряда в соответствие с системой счисления.
- *
- */
- for (size_length i = 0; i < l.length - 1; ++i) {
- if (l.values[i] >= BASE) { //если число больше максимального, то организовавается перенос
- digit carryover = l.values[i] / BASE;
- l.values[i + 1] += carryover;
- l.values[i] -= carryover * BASE;
- } else if (l.values[i] < 0) { //если меньше - заем
- digit carryover = (l.values[i] + 1) / BASE - 1;
- l.values[i + 1] += carryover;
- l.values[i] -= carryover * BASE;
- }
- }
- }
-
- long_value karatsuba(long_value a, long_value b) {
- long_value product; //результирующее произведение
- product.length = a.length + b.length;
- product.values = new digit[product.length];
-
- if (a.length < MIN_LENGTH_FOR_KARATSUBA) { //если число короче то применять наивное умножение
- memset(product.values, 0, sizeof(digit) * product.length);
- for (size_length i = 0; i < a.length; ++i)
- for (size_length j = 0; j < b.length; ++j) {
- product.values[i + j] += a.values[i] * b.values[j];
- /*В случае изменения MIN_LENGTH_FOR_KARATSUBA или BASE расскоментировать следующие
- * строки и подобрать соотв. значения для исключения переполнения разрядов.
- * Например для десятичной системы счисления число 100, означает, что организовавается
- * перенос 1 через один разряд, 200 - перенос 2 через один разрряд, 5000 - 5 через два.
- * if (product.values[i + j] >= 100){
- * product.values[i + j] -= 100;
- * product.values[i + j + 2] += 1;
- * }
- */
- }
- } else { //умножение методом Карацубы
- long_value a_part1; //младшая часть числа a
- a_part1.values = a.values;
- a_part1.length = (a.length + 1) / 2;
-
- long_value a_part2; //старшая часть числа a
- a_part2.values = a.values + a_part1.length;
- a_part2.length = a.length / 2;
-
- long_value b_part1; //младшая часть числа b
- b_part1.values = b.values;
- b_part1.length = (b.length + 1) / 2;
-
- long_value b_part2; //старшая часть числа b
- b_part2.values = b.values + b_part1.length;
- b_part2.length = b.length / 2;
-
- long_value sum_of_a_parts = sum(a_part1, a_part2); //cумма частей числа a
- normalize(sum_of_a_parts);
- long_value sum_of_b_parts = sum(b_part1, b_part2); //cумма частей числа b
- normalize(sum_of_b_parts);
- long_value product_of_sums_of_parts = karatsuba(sum_of_a_parts, sum_of_b_parts);
- // произведение сумм частей
-
- long_value product_of_first_parts = karatsuba(a_part1, b_part1); //младший член
- long_value product_of_second_parts = karatsuba(a_part2, b_part2); //старший член
- long_value sum_of_middle_terms = sub(sub(product_of_sums_of_parts, product_of_first_parts), product_of_second_parts);
- //нахождение суммы средних членов
-
- /*
- * Суммирование многочлена
- */
- memcpy(product.values, product_of_first_parts.values,
- product_of_first_parts.length * sizeof(digit));
- memcpy(product.values + product_of_first_parts.length,
- product_of_second_parts.values, product_of_second_parts.length
- * sizeof(digit));
- for (size_length i = 0; i < sum_of_middle_terms.length; ++i)
- product.values[a_part1.length + i] += sum_of_middle_terms.values[i];
-
- /*
- * Зачистка
- */
- delete [] sum_of_a_parts.values;
- delete [] sum_of_b_parts.values;
- delete [] product_of_sums_of_parts.values;
- delete [] product_of_first_parts.values;
- delete [] product_of_second_parts.values;
- }
-
- normalize(product); //конечная нормализация числа
-
- return product;
- }
* This source code was highlighted with Source Code Highlighter.