На этом шаге мы рассмотрим рекурсивную версию алгоритма Карацубы.
Известный ещё со школы классический алгоритм поразрядного умножения двух неотрицательных N-разрядных целых чисел требует времени порядка n2. На этом шаге мы разберём более быстрый алгоритм Карацубы. Метод можно применять к числам в любой системе счисления, но мы сосредоточимся на умножении двоичных чисел. В частности, пусть x и у - два неотрицательных целых числа, состоящих из bx и by двоичных разрядов соответственно. Применив подход "разделяй и властвуй", каждое двоичное число можно разделить на два следующим образом:
x = a * 2m + b, y = c * 2m + d, (6.2)
Например, для x = 594 и у = 69 декомпозиция будет такой:
x = 10010100102 = 1001010{a = 74}010{b = 2} = 74 * 23 + 2, у = 10001012 = 1000{с = 8}101{d = 5} = 8 * 23 + 5,
Во многих языках программирования вычислить значения a, b, c и d можно с помощью операций поразрядного сдвига (в языке Python такими операциями являются << и >>). Сдвиг x влево на m разрядов (x << m) с добавлением m младших нулей равнозначен ⌊x * 2m⌋. Сдвиг x вправо на m разрядов (x >> m) с отбрасыванием m младших разрядов равнозначен ⌊x / 2m⌋. Такие поразрядные операции полезны при целочисленном умножении (или делении) на степень двух и могут использоваться для разбиения x и у согласно (6.2) следующим образом:
a = x >> m, b = x - (a << m), c = у >> m, d = у - (c << m),
Согласно декомпозиции, произведение x и у можно записать как
x * у = (а * 2m + b)(c * 2m + d) = a * c22m + (a * d + b * c)2m + b * d. (6.3)
Этот первоначальный (примитивный) подход разбивает исходную задачу (умножения) на четыре меньшие подзадачи умножения ac, ad, bc и bd, решение которых можно получить четырьмя рекурсивными вызовами (затратами времени на умножение на степень двух можно пренебречь, поскольку его очень эффективно заменяют операции поразрядного сдвига). Однако этот подход не намного эффективнее "школьного", так как он тоже требует n2 поразрядных умножений для двух n-разрядных чисел. Предложенный Карацубой алгоритм может снизить затраты времени примерно до 1,585 * n за счёт перегруппировки слагаемых и увеличения числа операций сложения и вычитания, которые оказывают незначительное влияние на асимптотическую сложность вычислений. В частности, произведение xy можно заново переписать как
x * у = a * c22m + ((a + b)(c + d) - a * c - b * d)2m + b * d, (6.4)
В примере 6.8 приведена реализация метода Карацубы вместе со вспомогательной функцией, вычисляющей количество двоичных разрядов неотрицательного целого числа n, равное ⌊log2n⌋ для n > 1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
def number_of_bits(n): if n < 2: return 1 else: return 1 + number_of_bits(n >> 1) def multiply_karatsuba(x, y): if x == 0 or y == 0: return 0 elif x == 1: return y elif y == 1: return x else: n_bits_x = number_of_bits(x) n_bits_y = number_of_bits(y) m = min(n_bits_x // 2, n_bits_y // 2) a = x >> m b = x - (a << m) c = y >> m d = y - (c << m) ac = multiply_karatsuba(a, c) bd = multiply_karatsuba(b, d) t = multiply_karatsuba(a + b, c + d) - ac - bd return (ac << (2 * m)) + (t << m) + bd |
Размер задачи - min(bx, by). Таким образом, начальные условия должны проверить входы на равенство 0 или 1, приводящие к тривиальным результатам. Рекурсивные условия реализуют (6.4) тремя рекурсивными вызовами.
Что касается эффективности этого алгоритма, то предположим, что оба входа имеют одинаковое количество двоичных разрядов n, являющееся степенью двух (то есть n = 2k). Такое допущение возможно, поскольку к входным целочисленным значениям всегда можно добавить ведущие нули так, чтобы предположение выполнилось. В этом случае время выполнения алгоритма описывается функцией:
1, если n ≤ 1, T(n) = 3T(n/2) + cn + d, если n > 1,
Слагаемое cn + d возникает из-за сложений, вычитаний, операций сдвига и вызовов number_of_bits(), которые выполняются за линейное время относительно n. Применяя основную теорему, T(n) ∈ Θ(n log23) = Θ(n * 1,585...). Таким образом, этот метод эффективнее подхода, связанного с (6.3), чья стоимость вычислений описывается функцией:
1, если n ≤ 1, T(n) = 4T(n/2) + en + f, если n > 1,
Алгоритм Карацубы использует меньше умножений (соответствующих рекурсивным вызовам), чем школьный метод, но выполняет больше сложений и вычитаний. На практике алгоритм будет быстрее для больших значений n. Однако если n мало, дополнительные операции могут сделать его медленнее традиционного метода. В любом случае, метод примечателен тем, что он может сократить количество операций умножения, которые значительно дороже сложения или вычитания.
Со следующего шага мы начнем рассматривать умножение матриц.