Сколько будет умножить на умножить
Перейти к содержимому

Сколько будет умножить на умножить

  • автор:

Перевод «умножить» на английский

Вам нужно будет умножить эти требования скорости в первом вопросе на количество одновременных стримеров.

You’ll have to multiply those speed requirements in the first question by the number of simultaneous streamers.

Врачу оставалось только умножить их на число проведенных исследований.
The doctor could only multiply them by the number of studies carried out.
91 умножить на 4 это 364.
91 to increase on 4 it is 364.
Возможно, хотят умножить свое и без того значительное число.
They may wish to increase the size of their already considerable force.
Сеть позволяет ему умножить улов в 10 раз.
That net allows him to multiply his catch by 10 times.

Можно умножить количество барабанов и символов и получить в общей сложности 8000 возможных комбинаций.

One can multiply the number of reels and symbols and get the total of 8,000 possible combinations.
Изучая историю, мы можем также умножить наши победы.
By learning from history, we can also multiply our victories.
Альянсы и многосторонние институты могут умножить мощь свободолюбивых государств.
Alliances and multilateral institutions can multiply the strength of freedom-loving nations.
Это поможет нам умножить наши много раз нынешний уровень прибыли.
This will help us to multiply our present profit level many times.
Кто хочет умножить свои деньги, должен рисковать.
Who wants to multiply his money, should take risks.
Я могу умножить случайные числа для начала.
I could multiply random digits just to start with.
И мне нужно умножить это раз 4 компонент вектора.
And I need to multiply this times a 4 component vector.
Также матрицу можно умножить на число, называемое скаляром.
You can also multiply the whole matrix by a number, called a scalar.
Вы должны умножить числитель на то же число.
You have to multiply the numerator by the same number.
Некоторые простые расчеты могут помочь вам: умножить периода потери веса двух.
Some simple calculations can help you: multiply the weight loss period of two.
Для перевода в градусы, нужно умножить на 57,3.
To convert this to degrees, we must multiply by 57.3.

Нужно умножить диаметр выбранных ячеек на общее их количество по высоте, воспользовавшись формулой диагонали квадрата.

It is necessary to multiply the diameter of the selected cells by their total number in height, using the formula of the diagonal of the square.

Пусть нужно умножить 7 на 9.
Suppose we want to multiply 9 by 7.
Для этого нужно умножить количество потерянных килограммов на 10.
To do this, multiply the number of lost kilograms by 10.
Это означает, что число под чертой нужно умножить на 1000.
This means that you should multiply the value under the line by 1,000.
Возможно неприемлемое содержание

Примеры предназначены только для помощи в переводе искомых слов и выражений в различных контекстах. Мы не выбираем и не утверждаем примеры, и они могут содержать неприемлемые слова или идеи. Пожалуйста, сообщайте нам о примерах, которые, на Ваш взгляд, необходимо исправить или удалить. Грубые или разговорные переводы обычно отмечены красным или оранжевым цветом.

Зарегистрируйтесь, чтобы увидеть больше примеров. Это просто и бесплатно
Ничего не найдено для этого значения.
Предложить пример
Больше примеров Предложить пример

Новое: Reverso для Mac

Переводите текст из любого приложения одним щелчком мыши .

Download Reverso app
Перевод голосом, функции оффлайн, синонимы, спряжение, обучающие игры

Результатов: 2190 . Точных совпадений: 2190 . Затраченное время: 68 мс

Помогаем миллионам людей и компаний общаться более эффективно на всех языках.

1.5. Умножение

На этот раз нам понадобятся несколько пятикопеечных монет и, конечно же, снова счеты. Допустим, мы хотим купить себе конфет. Каждая конфета стоит $5$ копеек. Значит, чтобы купить две конфеты, мы должны отдать продавщице две пятикопеечные монеты — кладем их перед собой на стол. Теперь посчитаем, сколько здесь копеек — откладываем на счетах:

Эту же запись можно сделать немножко покороче. Когда я беру $2$ раза по $5$, я записываю это в виде примера на умножение:

Это читается: «Два раза по пять равно десяти». (Хотя это и не общепринятый способ чтения, я всё же настоятельно бы посоветовал пока читать эту запись именно так.) Теперь, допустим, мы хотим купить $3$ конфеты. Выкладываем перед собой три монеты, откладываем на счетах $3$ раза по $5$ и получаем:

Или в виде примера на умножение:

Еще пара таких демонстраций — и ребенок уже способен справляться с подобными примерами самостоятельно. Монеты вскоре становятся не нужны — достаточно одних счет. Теперь снова дело за практикой. Постепенно ребенок научится (почти безошибочно) считать все примеры из таблицы умножения. При этом многие ответы он мимоходом выучит наизусть. Настает время обратить его внимание на некоторые хитрости.

(1) Числа в примерах на умножение можно менять местами. Так, к примеру, $5$ раз по $3$ это ровно столько же, сколько $3$ раза по $5$. В этом легко убедиться если посмотреть на такую картинку:

Здесь пять столбцов по три кружка в каждом столбце, а значит, общее число кружков $$. С другой стороны, здесь три ряда по пять кружков в каждом ряду, то есть всего кружков $$. Таким образом, $5 \cdot 3 = 3 \cdot 5$. Теперь, когда мы узнали эту хитрость, нам позволительно читать запись

как «пять умножить на три». Именно так принято говорить по-русски, однако по сути это неправильно. «Пять умножить на три» означает буквально «берем пятерку три раза», в то время как запись «$$» означает на самом-то деле «берем тройку пять раз». Большой беды в этой путанице нет, потому что «пять умножить на три» в точности равно «трем умножить на пять». Другое русское выражение — «пятью три» — является, безусловно, более правильным. Не берусь утверждать про все иностранные языки, но по крайней мере в английском, немецком и французском выражение «$$» читается (в буквальном переводе) как «пять раз по три».

(2) Умножать на $10$, оказывается, очень легко. Чтобы в этом убедиться, снова достаем полный набор наших монет. Допустим у нас есть $23$ копейки (две дестюльника и три копейки):

Мы хотим, чтобы денег у нас стало в $10$ раз больше. Докладываем монетки таким образом:

Затем каждый ряд дестюльников заменяем на рубль, а каждый ряд копеек заменяем на дестюльник. Получаем $2$ рубля и $3$ дестюльника, то есть $230$ копеек. Чтобы умножить число на $10$, надо к этому числу справа приписать ноль:

$10 \cdot 23 = 23 \cdot 10 = 230$

Пусть у нас на счетах отложено число $23$. Умножить на $10$ — это значит все отложенные бусинки «переселить» на один ряд выше. Впрочем, будет очень полезно один разок действительно десять раз «тупо» отложить на счетах число $23$ (или любое другое) и посмотреть, что получится.

(3) Допустим, мы хотим на счетах умножить на $30$ число $23$. Будем ли мы откладывать $23$ тридцать раз? Нет, конечно. Мы сразу отложим $23$ десять раз, то есть попросту отложим число $230$. Всего надо так сделать $3$ раза, потому что $30$ это не что иное как $3$ раза по $10$. Получаем:

$30 \cdot 23 = 3 \cdot 230 = 690$

Умножать на $100$ тоже очень просто, потому что $100$ — это $10$ раз по $10$. Умножим $23$ на $10$. Приписав ноль, получим $230$. Потом еще раз умножим на десять. Припишем еще один ноль: $2300$. В итоге выходит:

$100 \cdot 23 = 23 \cdot 100 = 2300$

При умножении на $100$ к числу надо приписать два нуля. А на счетах при умножении на $100$ все отложенные бусинки «переселяются» на два ряда выше.

А если мы захотим умножить $23$ на $300$? Сразу откладываем на счетах $2300$ и делаем так всего три раза. Получаем:

$300 \cdot 23 =3 \cdot 2300 = 6900$

А если надо умножить $230$ на $300$? Тут всё то же самое:

$300 \cdot 230 = 3 \cdot 23000 = 69000$

Мы замечаем одну полезную вещь: когда мы перемножаем «круглые» числа (то есть такие, которые оканчиваются на ноль), мы можем поначалу вовсе отбросить все конечные нули, выполнить умножение без них, а потом к результату приписать столько нулей, сколько мы отбросили. Например, в примере на умножение

мы отбрасываем три конечных нуля и получаем

Теперь сталось только приписать сюда те самые три нуля, которые мы отбросили:

$300 \cdot 230 = 69000$

(4) Если на $10$ умножать очень легко, то и на $11$ ненамного труднее. Допустим, мы хотим умножить на $11$ всё то же самое число $23$. Для этого надо число $23$ отложить на счетах $11$ раз. Но мы уже знаем, какое число мы получим после того, как сделаем это $10$ раз. Это $230$. Поэтому мы сразу откладываем $230$, после чего нам остается отложить $23$ только один раз. Мы получаем ответ всего в два «хода»:

$11 \cdot 23 = 253$

Однако, если так легко умножать на $11$, то и на $12$ — тоже нетрудно. Одни раз отложим $230$ и два раза по $23$. В результате получим (в три «хода»):

$12 \cdot 23 = 276$

Умножать на $20$ мы уже умеем. Для этого понадобится лишь два «хода»: два раза по $230$ — и готово!

А сколько «ходов» нужно чтобы умножить $23$ на $21$? — Только три: откладываем два раза по $230$ и один раз $23$.

Теперь нам нетрудно будет вычислить и такие примеры:

$101 \cdot 23$ (в два «хода),

$102 \cdot 23$ (в три «хода»).

Оказывается, мы уже умеем легко перемножать между собой довольно большие числа.

(5) Пусть нам теперь требуется вычислить $9 \cdot 23$. Значит ли это, что мы должны, по старинке, откладывать на счетах число $23$ целых девять раз? Нет, не значит. Допустим, мы просчитались, и вместо девяти раз отложили это число десять раз. Такую ситуацию легко исправить. Прокручиваем последний, лишний, «ход» в обратную сторону (то есть, вычитаем $23$) — и всё в порядке. Но ведь мы уже заранее знаем, что получится, если отложить число $23$ десять раз. Поэтому мы можем сознательно как бы просчитаться, а потом поправиться. Откладываем $230$, вычитаем $23$, и получаем ответ (в два «хода»):

Подобным же образом, чтобы вычислить $8 \cdot 23$, надо «поправиться» два раза. Откладываем $230$ и два раза отнимаем $23$ (всего три «хода» вместо восьми):

Используя данный прием, мы теперь сумеем легко вычислить:

$19 \cdot 23$ (в три «хода),

$99 \cdot 23$ (в два «хода»)

и тому подобное.

Разумеется, усложнение примеров не самоцель. Важно, чтобы ребенок разобрался во всех хитростях и научился ими пользоваться. Однако перемножать на счетах числа в пределах $24$ — это вполне посильная задача. В этом случае для решения одного примера понадобится не более шести «ходов».

(6) Самый легкий случай — это когда требуется умножить на число $0$ (ноль). Отложим ли мы число $23$ ноль раз или ноль бусинок отложим $23$ раза, результат всё равно один:

Замечание 1. В качестве знака умножения вместо точки часто используют косой крестик. Так, записи $$ и $$ означают одно и то же.

Замечание 2. В русскоязычных школьных учебниках по математике умножение определяется как $$. Хотя такое определение и не приводит к ошибкам в вычислениях, по сути оно неверно. Когда мы говорим две конфеты, мы имеем в виду конфета + конфета. Когда мы говорим две монеты по пять копеек, мы имеем в виду $5$ копеек + $5$ копеек. Здесь двойка отвечает на вопрос сколько и стоит на первом места, а пятерка отвечает на вопрос что и стоит на втором месте. И такой порядок принят всегда — что бы мы ни пересчитывали: будь то предметы, деньги, метры, минуты или что угодно. Отступать от этого порядка при переходе к «голым» числам было бы совершенно нелогично. Важно понимать, что слова «две монеты» задают точно такое же отношение между понятиями двойка и монета, какое существует между числами $2$ и $5$ в записи $$, тем более что за абстрактным числом $5$ вполне может стоять не что иное, как пятикопеечная монета. Знак умножения ($\cdot$) между «голым» числами ставится только для того, чтобы они не слились в одно число ($25$). Во всех остальных случаях этот знак не нужен и, как правило, опускается.

1. Чтобы узнать, сколько стоят три конфеты по пять копеек, мы откладываем на счетах три раза по пять бусинок. На бумаге эта задача и ее ответ записывается в виде $$ или сокращенно $3 \cdot 5 = 15$ (три раза по пять равно пятнадцать). Это пример на умножение: следовало бы говорить, что мы пять умножили на три и получили пятнадцать. Однако на практике запись $3 \cdot 5$ в русском языке принято не совсем правильно читать «три умножить на пять». Эта путаница не приводит к недоразумениям, потому что если взять пять раз по три, то мы получим тот же самый результат — пятнадцать. Числа, входящие в пример на умножение можно менять местами. Пять умножить на три это то же самое, что и три умножить на пять: $3 \cdot 5 = 5 \cdot 3 = 15$.

2. Чтобы умножить число на $10$, надо приписать к нему справа ноль. При этом бусинки на счетах «переселяются» на один ряд выше. При умножении на сто мы приписываем к чилу два нуля, а бусинки на счетах «переселяем» на два ряда выше. Когда мы перемножаем «круглые» числа (оканчивающиеся на $0$), мы можем поначалу отбросить все конечные нули, выполнить умножение без них, а потом к результату приписать столько нулей, сколько мы отбросили.

3. Чтобы умножить $23$ на $30$, откладываем на счетах три раза число $230$. Умножая $23$ на $31$, откладываем на счетах три раза число $230$ и один раз число $23$. При умножении $23$ на $102$ откладываем на счетах один раз число $2300$ и два раза число $23$.

4. Чтобы умножить $23$ на $9$, откладываем на счетах число $230$ и отнимаем от него число $23$. Умножая $23$ на $98$, откладываем $2300$ и два раза отнимаем $23$.

5. При умножении любого числа на ноль получается ноль ($0$).

Задачи (в дополнение к обычным примерам на умножение)

1.5.1. Как удобнее вычислять:

$7$ раз по $8$ или $8$ раз по $7$?

$11$ раз по $19$ или $19$ раз по $11$?

1.5.2. За сколько «ходов» можно вычислить следующие примеры:

Алгоритмы быстрого умножения чисел: от столбика до Шенхаге-Штрассена

При написании высокоуровневого кода мы редко задумываемся о том, как реализованы те или иные инструменты, которые мы используем. Ради этого и строится каскад абстракций: находясь на одном его уровне, мы можем уместить задачу в голове целиком и сконцентрироваться на её решении.

И уж конечно, никогда при написании a * b мы не задумываемся о том, как реализовано умножение чисел a и b в нашем языке. Какие вообще есть алгоритмы умножения? Это какая‑то нетривиальная задача?

В этой статье я разберу с нуля несколько основных алгоритмов быстрого умножения целых чисел вместе с математическими приёмами, делающими их возможными.

Оглавление

  1. Его величество столбик
    • Про О-нотацию
  2. Разделяй и властвуй: алгоритм Карацубы
  3. Многочлены vs преобразование Фурье: алгоритм Шенхаге-Штрассена
    • Про быстрое преобразование Фурье
  4. Модульная арифметика и второй алгоритм Шенхаге-Штрассена
    • Про модульную арифметику чисел
    • Про модульную арифметику многочленов

Зачем быстро умножать числа?

Программы постоянно перемножают числа. Перемножения 32-битных, 64-битных, а иногда и более длинных чисел встроены напрямую в арифметико‑логические устройства микропроцессоров; генерация оптимальных цепей для кремния — отдельная инженерная наука. Но не всегда встроенных возможностей хватает.

Например, для криптографии. Криптографические алгоритмы (вроде повсеместно используемого RSA) оперируют числами длиной в тысячи бит. Если мы сводим операции над 4096-битными числами к операциям над 64-битными словами, разница в количестве операций между алгоритмами за и уже составляет десять раз!

Деление тоже сводится к умножению; в некоторых процессорах даже нет инструкции для целочисленного деления.

Но сразу признаюсь: не все существующие алгоритмы быстрого умножения достаточно практичны для широкого применения — по крайней мере на сегодняшний день. Помимо практического здесь силён академический интерес. Как вообще математически устроена операция умножения? Насколько быстро её можно делать? Можем ли мы найти оптимальный алгоритм и чему научимся в процессе поиска?

Его величество столбик

Когда-то очень давно мне попалось на глаза видео с кричащим названием «How to Multiply», описывающее так называемый японский метод умножения. Что меня удивило — так это то, что метод этот подаётся как более простой и интуитивно понятный. Оказывается, если умножение в столбик делать не в столбик, а занимая половину листа бумаги, получается проще и понятнее!

Да-да, принципиально это тот же самый алгоритм умножения столбиком. Можем посмотреть на запись в столбик и сравнить её с картинкой:

92 — это две больших группы красных точек на нижне-правой диагонали. 23 — две маленьких группы на верхне-левой диагонали. Точно так же, как и в столбике, мы вынуждены перемножить попарно все разряды, потом сложить, и потом сделать перенос. Умножение в столбик — не что иное, как компактный на бумаге и относительно удобный для человеческого сознания способ проделать алгоритм, объединяющий в себе практически все придуманные до XX века способы умножения, за редким исключением вроде умножения египетских дробей.

Асимптотическая сложность умножения в столбик не зависит от того, в какой системе счисления мы производим умножение, и составляет операций, где и — количество разрядов в множителях. Каждый из разрядов одного множителя нужно умножить на каждый из разрядов второго множителя, после чего получившиеся маленьких чисел (в каждом не больше двух разрядов) сложить.

Почему асимптотика не зависит от системы счисления?

В разных системах счисления у чисел разное количество разрядов, это верно. Количество десятичных разрядов в числе x равно в двоичной — (скобки-уголки означают округление вверх). Но мы знаем, что логарифмы по разному основанию связаны друг с другом константным множителем:

А константные множители не имеют значения при анализе асимптотики.

Обычно при анализе сложности под и подразумевается количество двоичных разрядов, а числа предполагаются примерно одинаковой длины; тогда формула сложности упрощается до .

Что означает запись O(N²)?

Многих программистов О-нотации учит улица — они сталкиваются с ней сразу в оценке сложности каких-то алгоритмов. Но О-формализм происходит из математического анализа, и у него есть строго определённый смысл, причём не очень хорошо согласующийся с остальными привычными для математики обозначениями.

Если у нас есть функция — в наших примерах это, как правило, будет количество операций, нужных для обработки алгоритмом входных данных в размере бит — то запись означает, что существует некоторая константа C такая, что начиная с достаточно больших

То есть функция растёт не быстрее, чем N^2.

При этом сама запись означает класс функций, для которых выполнено это условие. В привычной теоретико-множественной нотации это было бы правильно записать как — функция входит во множество функций, растущих не быстрее чего-то там.

При этом ничего не сказано о том, может ли функция расти медленнее, чем . Например, в случае алгоритма умножения в столбик мы могли бы без зазрения совести записать — и формально всё ещё были бы правы. Действительно, операций нужно меньше, чем . Полезная ли это информация? Не очень.

Соответственно, одна и та же функция может быть «равна» разным ; при этом между друг другом эти не становятся равны:

Так что обращаться с в выражениях нужно осторожно.

Помимо — наиболее, пожалуй, распространённого среди программистов — есть и другие классы функций с аналогичной записью. означает, что функция растёт медленее, чем ; да не просто медленнее, а настолько, что их частное становится с ростом всё ближе и ближе к нулю:

Например, в матанализе постоянно используют запись , чтобы обозначить незначительную величину, стремящееся к нулю. Единица в скобках при этом не имеет какого-то существенного значения — там могла бы быть любая константа; можете написать и формально это будет верно. Иногда бывает важно указать, с какой именно скоростью величина стремится к нулю. Тогда пишут , и так далее.

Другая встречающаяся нотация означает, что функция растёт точно со скоростью , то есть существуют константы и такие, что начиная с достаточно больших

Помимо простого использования со знаком равенства, как в примерах выше, классы функций можно использовать в арифметических операциях. Например, запись

означает, что функция равна плюс нечто маленькое, стремящееся к нулю при росте . В отличие от или , эта запись не допускает произвольных множителей при — ровно и всё тут.

Работает (при некоторых ограничениях) и привычная арифметика в равенствах. Так, если , обе части равенства можно поделить, например, на константу:

съедает любые константы, поэтому справа ничего не изменилось. А можно поделить на :

Не-константу нельзя просто выкинуть, поэтому она ушла внутрь и сократилась с тем, что там было.

Разделяй и властвуй: алгоритм Карацубы

Первый шаг на пути ускорения умножения совершил в 1960-м году советский математик Анатолий Карацуба. Он заметил, что если длинные числа поделить на две части:

То можно обойтись тремя умножениями этих более коротких частей друг на друга, а не четырьмя, как можно было бы подумать. Вместо прямого подсчёта , требующего двух умножений, достаточно посчитать и вычесть из результата числа и , нужные нам в любом случае для получения младших и старших разрядов.

После этих расчётов остаётся лишь пробежаться по разрядом справа налево и провести суммирование:

По сравнению с умножениями это быстрая операция, не заслуживающая особого внимания — как и два лишних сложения в скобках.

Для построения эффективного алгоритма осталось превратить это наблюдение в рекурсивную процедуру. Половинки чисел тоже будем делить на половинки и так далее, пока не дойдём до достаточно коротких чисел, которые можно перемножить в столбик или, допустим, через lookup table.

Поскольку мы каждый раз делим задачу на три задачи с вдвое меньшими (по количеству бит) числами, для перемножения двух чисел длины нам потребуется рекурсия глубины и суммарно умножений чисел наименьшей длины; отсюда получаем оценку сложности в

Занятный факт — аналогичные фокусы с экономией за счёт одновременных умножений используются ещё в ряде мест. Так, два комплексных числа и также можно перемножить за три вещественных умножения вместо четырёх, вычислив , и . А алгоритм Пана для быстрого перемножения матриц основывается на том, что можно одновременно вычислить произведения двух пар матриц и за меньшее число умножений, чем по отдельности [1, 2]. Не говоря уж о том, что само по себе перемножение матриц быстрее, чем за — это быстрое одновременное умножение матрицы на векторов.

Числа vs многочлены: алгоритмы Тоома-Кука

При виде магического сокращения вычислительной сложности при разбиении множителей на две части как-то сам собой возникает вопрос: а можно разбить множители на бóльшее число частей и получить бóльшую экономию?

Ответ — да; и подход, позволяющий это сделать, включает в себя алгоритм Карацубы как частный случай. Но для его формулировки нам придётся проделать некий фокус.

Давайте попробуем разбить те же самые числа на три части:

Если мы внимательно посмотрим на коэффициенты при степенях десятки, которые возникают при перемножении этих скобок:

То (при наличии опыта в алгебре) заметим, что где-то мы такое уже видели. При перемножении многочленов!

Если взять части наших чисел a и b и объявить их коэффициентами многочленов и при соответствующих степенях, числа в таблице выше будут не чем иным, как коэффициентами многочлена :

Сами же числа получаются из многочленов путём вычисления значения в некоторой точке. Какой именно — зависит от системы счисления и размера частей:

Хорошо, свели одну задачу к другой. В чём преимущество многочленов? Помимо того, что это некоторые формальные конструкции с параметрами, это также функции; чтобы вычислить в произвольной точке, не нужно перемножать многочлены — достаточно вычислить оба значения в этой точке и перемножить их. Если бы я писал продакшн-код, в котором по какой-то причине нужно перемножать многочлены, я бы и вовсе сделал перемножение ленивым.

Но нам не нужно вычислять значение в точке. Нам нужны коэффициенты. А коэффициенты многочлена можно восстановить по значениям в точках, решив систему линейных уравнений:

Здесь — это точки, в которых нам известны значения многочлена, в правой части — сами эти значения, а — неизвестные нам коэффициенты многочлена. Количество неизвестных соответствует степени многочлена + 1; чтобы решение было единственным, нужно, соответственно, знать значения в таком же количестве точек. В нашем примере это пять точек, потому что у многочлена-произведения степень 4:

Если переписать эту систему уравнений в матричном виде, получим

Соответственно, для создания алгоритма быстрого умножения чисел нам достаточно:

  1. выбрать, на сколько частей мы разбиваем числа (можно даже разбить и на разное количество частей);
  2. выбрать точки , в которых мы будем вычислять значения многочленов;
  3. построить по ним матрицу Вандермонда;
  4. вычислить её обратную матрицу.

В ходе алгоритма нам нужно будет два раза умножить на прямую матрицу (по разу для каждого из чисел, которые мы хотим перемножить), перемножить результаты алгоритмом умножения чисел меньшего размера, и один раз умножить получившийся вектор на обратную матрицу.

Давайте посмотрим на матрицу для точек :

from sympy import Rational from sympy.matrices import Matrix # Точки, в которых будем вычислять значения многочленов: x = [0, 1, -1, 2, -2] # Матрица Вандермонда, построенная по этим точкам: v = Matrix([ [ Rational(x_i ** j ) for j, _ in enumerate(x) ] for x_i in x ])

(Серым я отметил числа, не имеющие значения, поскольку соответствующие коэффициенты в многочленах-множителях заведомо равны нулю.)

И на её обратную:

За исключением некоторой проблемы с делением на 3, подавляющая часть вычислений здесь представляется битовыми сдвигами, сложениями и вычитаниями «коротких» чисел, а это операции «простые» — имеющие линейную сложность.

Как проанализировать итоговую сложность получившегося алгоритма? Давайте обозначим количество операций через , где — количество бит в наших множителях, и выведем рекуррентную формулу:

Что здесь написано: умножение двух чисел длиной бит мы умеем сводить к 5 умножениям чисел длиной бит и ещё какому-то количеству простых операций над числами длиной бит. Применяем основную теорему о рекуррентных соотношениях и получаем итоговую сложность

Таким образом мы ускорили умножение с до .

Алгоритмы, получаемые таким образом, называются алгоритмами Тоома-Кука. Они активно используются на практике; в одной только библиотеке GMP поддержано 13 разных вариантов разбиения чисел.

Закономерный следующий вопрос: что дальше? Если число бьётся на частей, сложность получается

Показатель степени при большом можно упростить:

Получается, мы можем построить алгоритм со сколь угодно близкой к 1 степенью в асимптотике? Увы, тут есть сложности.

Во-первых, это непрактично. Логарифм возрастает крайне медленно; асимптотика, соответственно, тоже будет падать медленно, а константа при этом будет быстро расти, потому что числа в матрицах будут всё больше и разнообразнее.

Во-вторых, это неинтересно. Да-да! Дальше мы будем обсуждать алгоритмы, сложность которых лучше, каким маленьким бы ни было .

Занятный факт — аналогичной методу Тоома-Кука техникой строятся быстрые алгоритмы перемножения матриц. Однако процедура перемножения матриц по своей математической природе оказалась намного сложнее перемножения многочленов; поэтому до сих пор нет уверенности, что можно перемножать матрицы за для сколь угодно малого . Лучшая асимптотика на сегодняшний день составляет примерно .

Многочлены vs преобразование Фурье: алгоритм Шенхаге-Штрассена

Раз рекуррентное деление на несколько частей нам не интересно, давайте попробуем сделать радикальный шаг. Что, если делить числа не на фиксированное количество частей, а на части фиксированной длины?

Например, на десятичные разряды. В таком случае нам нужно будет точек для -разрядного числа; в остальном подход как будто бы тот же самый, что в предыдущем разделе. Что ж, давайте возьмём и проведём численный эксперимент, чтобы убедиться, что всё работает!

import math import numpy as np # Числа, которые мы изначально собирались перемножить: a = 235739098113 b = 187129102983 m = len(str(a)) # Коэффициенты соответствующих многочленов в порядке # возрастания степени. Поскольку результирующий многочлен # будет степени 2*m-1, добиваем нулями до нужной ширины: a_coefs = [int(a_i) for a_i in str(a)[::-1]] + [0] * (m - 1) b_coefs = [int(b_i) for b_i in str(b)[::-1]] + [0] * (m - 1) n = len(a_coefs) # Точки, в которых будем вычислять значения многочленов: x = np.arange(n) # Матрица Вандермонда, построенная по этим точкам: v = np.vander(x, increasing=True) # Вычисление значения многочлена в точке — не что иное, как # умножение этой матрицы на вектор коэффициентов: a_y = v @ a_coefs b_y = v @ b_coefs # Поточечно перемножаем значения, чтобы получить # значения многочлена-произведения: c_y = a_y * b_y # Восстанавливаем коэффициенты многочлена-произведения: c_coefs = np.linalg.solve(v, c_y) # Для сверки считаем коэффициенты "в лоб": actual_c_coefs = [ sum(a_coefs[j] * b_coefs[i-j] for j in range(i+1)) for i in range(n) ] # Считаем длину вектора-разности и делим на длину настоящего, # чтобы получить относительную погрешность: print(np.linalg.norm(c_coefs - actual_c_coefs) / np.linalg.norm(actual_c_coefs))
2.777342817120168e+17

Хо-хо, да это не просто мимо, это фантастически мимо! Но почему?

Оказывается [3], у матриц Вандермонда есть неприятное свойство — решать системы с ними в подавляющим большинстве случаев очень плохая идея, потому что погрешность результата растёт экспоненциально с размером матрицы. Можно улучшить ситуацию, взяв вместо комплексные точки на единичной окружности:

x = np.exp(1j * np.random.uniform(0, math.pi, size=n)) . 0.00015636299542432733

Но лучшим выбором оказывается взять точки, равномерно распределённые на единичной окружности и являющиеся корнями из единицы степени .

x = np.exp(-1j * np.linspace(0, 2*math.pi, n, endpoint=False))

Что ещё за корни из единицы?

Хотя в привычной нам вещественной арифметике равенство выполнено только при , на комплексной плоскости всё гораздо веселее. У каждого многочлена степени есть ровно комплексных корней, и многочлена это правило (также известное как основная теорема алгебры) тоже касается.

Корнями из единицы степени являются числа на комплексной плоскости, расположенные в вершинах правильного -угольника, вписанного в единичную окружность:

Поскольку при умножении комплексных чисел их модули перемножаются (а у всех корней из единицы модуль равен единице — они же на единичной окружности), а фазы складываются, при возведении числа в последовательные степени мы получим по очереди все корни из единицы. За это свойство называется первообразным корнем из единицы.

Запускаем новый вариант кода и получаем

1.5964929527133826e-15

Отлично! Мы дошли до предела машинной точности.

Но самое изумительное — при таком выборе иксов матрица представляет собой не что иное, как матрицу дискретного преобразования Фурье! А значит, мы можем умножать векторы на эту матрицу и решать систему уравнений с ней алгоритмом быстрого преобразования Фурье за операций сложения и умножения — вместо наивного алгоритма за .

Что такое дискретное преобразование Фурье?

Преобразование Фурье досталось нам из высшей математики и волновой физики. В физике оно известно как способ разложить сигнал (например, аудио) какой-то произвольной формы

на сумму элементарных сигналов — гармоник с длиной волны, кратной ширине отрезка:

Одна гармоника соответствует постоянному звуку некой фиксированной частоты и громкости; сумма гармоник может дать любую мелодию. Сколь угодно сложный сигнал можно разложить на гармоники, если взять их достаточно много. Совокупность гармоник (частоты + амплитуды) называется спектром сигнала.

Преобразование Фурье — построение в первую очередь теоретическое; Жозеф Фурье открыл его задолго до появления компьютеров. На практике мы не можем оперировать сигналами как функциями; у нас есть только дискретизации, замеры значения сигнала с каким-то шагом по времени.

Согласно теореме Котельникова-Найквиста-Шеннона при такой ограниченной информации мы всё ещё можем восстановить гармоники (а с ними — и весь сигнал), если предположим, что сигнал был достаточно простым (в нём не было каких-то сверхвысоких частот). Этим и занимается дискретное преобразование Фурье — фактически это матрица, которую нужно умножить на вектор со значениями дискретизированного сигнала, чтобы получить вектор с амплитудами, соответствующими разным частотам гармоник.

Так, дискретизация на картинке выше недостаточно частая, чтобы восстановить самую высокую частоту:

А более частая, удовлетворяющая условию теоремы — достаточна:

По роковому стечению обстоятельств матрица дискретного преобразования Фурье является матрицей Вандермонда, построенной по корням из единицы.

В целом анализ Фурье — очень интересный раздел математики, связывающий воедино множество ранее разобщённых концепций. Его подробное описание, конечно, уходит совсем далеко за рамки статьи; можно начать со статей на Хабре «Простыми словами о преобразовании Фурье» и «Преобразование Фурье в действии: точное определение частоты сигнала и выделение нот».

Что такое быстрое преобразование Фурье?

Дискретное преобразование Фурье применяется повсеместно для обработки сигналов (например, шумоподавления), часто реализуется аппаратно и встречается с совершенно неожиданных устройствах (например, МРТ-сканерах [4]). Это создаёт потребность в создании настолько быстрого алгоритма, насколько возможно.

Умножение на матрицу «в лоб» стоит — это считается довольно медленным в практических алгоритмах. Быстрое преобразование Фурье — название семейства алгоритмов, достигающих асимптотики за счёт сведения задачи к нескольким задачам меньшего размера с линейной стоимостью одного шага рекурсии. Самый простой вариант — алгоритм для , сводящий задачу к двум задачам размера ; рекурсия глубины приводит к общей сложности .

Про него в интернете, конечно, написано много материалов разной степени читабельности. Но давайте я попробую очень быстро на пальцах показать, за счёт чего вычисление можно ускорить до .

Дискретное преобразование Фурье и обратное дискретное преобразование Фурье — восстановление сигнала по спектру — очень похожи математически, и алгоритм почти идентичный; объяснить его будет проще на примере обратного преобразования.

Обратное преобразование заключается в том, что нужно взять гармоник, у которых мы знаем частоту и амплитуду, посчитать их значения в точках отрезка и сложить, получив вектор из значений сигнала-суммы гармоник. На рисунке пример для :

Поделив отрезок пополам, можно заметить, что гармоники делятся на два типа:

В верхней части — кривые, половинки которых одинаковы в первой и второй половине отрезка; в нижней — выглядящие вертикально отражёнными, то есть одна половина получается из другой умножением на . А значит, можно сэкономить, вычисляя значения всех гармоник только на половине отрезка!

При этом задача для произвольной гармоники на половине отрезка соответствует задаче для гармоники с вдвое меньшей частотой и точек:

Получается, что для решения исходной задачи восстановления сигнала в точках по данным гармоникам нам достаточно:

  1. просуммировать гармоник первого типа (взяв вдвое меньшую частоту) в точках;
  2. просуммировать гармоник второго типа (также взяв вдвое меньшую частоту) в точках;
  3. первую сумму — вектор длины — повторить два раза, получив вектор длины ;
  4. вторую сумму — также вектор длины — тоже повторить два раза, но второй раз — со знаком минус;
  5. два получившихся вектора сложить.

Поскольку мы свели задачу для к двум задачам для и дополнительным шагом обработки стоимостью , итоговая сложность получается равной .

Так мы и получили предельно простой пересказ алгоритма Кули-Тьюки!

Делить числа на части по десятичным разрядам — это, конечно, не самый эффективный способ. Тут даже нет как таковой рекурсии — после первого разделения на части мы сразу приходим к числам от 0 до 9, которые можно перемножить по таблице умножения. Наилучшая асимптотика в таком алгоритме получается, если делить -битные числа на части длиной примерно бит.

Полностью алгоритм Шенхаге-Штрассена выглядит так.

    Берём на вход два числа, которые хотим перемножить; числа эти длины бит. На практике это значит, что бóльшее из чисел имеет бит.

Формула асимптотической сложности получившегося алгоритма довольно страшная, в целом асимптотика получается чуть-чуть хуже . Но можно оценить полёт мысли и количество математических конструкций, требующихся для достижения такого результата.

Модульная арифметика и второй алгоритм Шенхаге-Штрассена

Хотя первый алгоритм Шенхаге-Штрассена круто улучшает асимптотику по сравнению с ранее существовавшими методами, у него всё ещё есть зоны роста. Во-первых, на каждом шаге рекурсии происходит экспоненциальное уменьшение размеров чисел — с до . Из-за этого глубина рекурсии очень мала и воспользоваться ускорением получается не в полном объёме. Во-вторых, необходимость производить расчёты с плавающей точкой разной точности весьма непрактична — мы все привыкли к float и double, а реализация чисел с плавающей точкой настраиваемой ширины весьма нетривиальна. (Здесь читатель может возразить, что многое из вышеописанного было нетривиально. Могу только согласиться.)

Второй алгоритм Шенхаге-Штрассена лишён этих двух недостатков, более быстр и в теории, и в практике и наиболее широко используется — например, в библиотеке GMP.

Для решения первой проблемы давайте попробуем разбить числа на более крупные фрагменты — например, размера вместо . Здесь мы незамедлительно столкнёмся с проблемой: для дискретного преобразования Фурье в таком случае потребуется операций умножения чисел из бит; каждое такое умножение будет стоить в лучшем случае (напомню, все алгоритмы выше стоили ещё дороже), что даёт нам суммарную сложность не меньше , что уже хуже предыдущего алгоритма. Что-то здесь нужно ускорить.

Для решения второй проблемы можно вспомнить, что в модульной арифметике тоже можно делать быстрое преобразование Фурье. Тогда мы будем иметь дело только с целыми числами! А если подобрать в качестве элементов матрицы Фурье степени двойки, то на них можно будет умножать быстрее, чем за . В идеале вообще за , потому что умножение на степень двойки — это просто битовый сдвиг.

Что такое модульная арифметика?

Арифметика по модулю — это ровно то, о чём можно подумать из названия. Привычные арифметические операции (сложение, вычитание, умножение) берутся с единственным изменением — после проведения операции надо взять остаток от деления результата на некое фиксированное число p. Так, в арифметике по модулю 7 имеем , , но , . Принято писать

Арифметика по модулю нам, программистам, очень близка. Если вы оперируете 32-битными (или 64-битными) беззнаковыми целыми числами в своём коде, вы фактически делаете все операции в арифметике по модулю (или, соответственно, ).

Примечательный факт в том, что при таком определении арифметических операций мы получаем полноценную арифметику, в которой сохраняется большинство свойств, к которым мы привыкли в обычной арифметике. Но, конечно, есть нюансы.

Множество чисел в арифметике по модулю p называется кольцом вычетов по модулю и обозначается ; в Серьёзных Книгах также можно встретить запись . В нём содержатся числа

Само число p в арифметике по модулю эквивалентно нулю (таков остаток от деления на само себя); эквивалентно 1 и так далее. Отрицательные числа работают по схеме, привычной программистам: эквивалентно , эквивалентно и так далее.

Деление в модульной арифметике — это первый нюанс. В нет дробей; как определить частное двух чисел, не делящихся друг на друга? По определению деления — это такое число, что . Возвращаясь к примерам в арифметике по модулю 7, из равенства получаем

Однако не всегда в модульной арифметике возможно поделить два числа друг на друга. Это второй нюанс — произведение двух ненулевых чисел в модульной арифметике может быть равно нулю! Так, в арифметике по модулю 6 имеем . Такие числа называются делителями нуля и делить на них не получится. Само существование и в арифметике по модулю 6 противоречило бы некоторым аксиомам; это всё равно, что делить на ноль. На практике мы можем легко проверить перебором, что подходящих на роль частного чисел не существует:

Z_6 = range(6) [(x * 2) % 6 for x in Z_6] # [0, 2, 4, 0, 2, 4] — нет единицы [(x * 3) % 6 for x in Z_6] # [0, 3, 0, 3, 0, 3] — нет единицы

Множество чисел, не являющихся делителями нуля, называется мультипликативной группой кольца вычетов по модулю p и обозначается . Например, в случае имеем

— ведь как мы выяснили только что, 2 и 3 являются делителями нуля; кратная двойке четвёрка — тоже: .

Примечательный факт про мультипликативную группу — любые два числа в ней можно умножать и делить друг на друга, и снова получать элементы из этой группы. Так, .

Если основание арифметики — простое число, делителей нуля в ней не существует и можно делить друг на друга любые числа. Соответственно, в мультипликативной группе в этом случае содержатся все числа из , кроме нуля:

Как находить частное двух чисел на практике, кроме как перебором? Занимательный факт из алгебры: если в мультипликативной группе элементов, то любой её элемент в степени равен единице. Давайте проверим на паре примеров:

Z_7_mul = range(1, 7) [pow(x, 6, 7) for x in Z_7_mul] # [1, 1, 1, 1, 1, 1] Z_8_mul = [1, 3, 5, 7] [pow(x, 4, 8) for x in Z_8_mul] # [1, 1, 1, 1]

А это значит, что

и, соответственно, . Возвести число в -ю степень можно за умножений алгоритмом быстрого возведения в степень.

Отсюда нюанс третий — в мультипликативной группе все числа являются корнями из единицы степени ! Эти корни при правильном обращении ведут себя аналогично корням из единицы в комплексной арифметике, и для них тоже можно построить алгоритм быстрого преобразования Фурье.

Наблюдение первое: самая удобная арифметика

Для того, чтобы быстро делать умножения во время быстрого преобразования Фурье, нам нужно, чтобы некие числа сочетали два свойства: были степенями двойки — чтобы можно было быстро на них умножать, и были корнями из единицы — чтобы быстрое преобразование Фурье в принципе работало. Значит, привычная всем программистам n-битная арифметика не подходит — потому что это фактически вычисления по модулю , а значит, любая степень двойки при возведении в достаточно большую степень обращается в ноль и никак не может быть корнем из единицы:

num_bits = 5 [pow(2, i, 2**num_bits) for i in range(2*num_bits)] # [1, 2, 4, 8, 16, 0, 0, 0, 0, 0]

Если же основание арифметики кратно двум, но не степени двойки, мы не получим ноль, но и никогда не получим единицу:

mod = 6 [pow(2, i, mod) for i in range(2*mod)] # [1, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2]

Потому что двойка является делителем нуля (в примере выше её можно умножить на три и получить 0 по модулю 6) и не является членом мультипликативной группы.

Гораздо лучше дела обстоят с основаниями, взаимно простыми с двойкой:

mod = 5 [pow(2, i, mod) for i in range(2*mod)] # [1, 2, 4, 3, 1, 2, 4, 3, 1, 2]

Поскольку в этом случае двойка состоит в мультипликативной группе, всегда есть степень, в которую её можно возвести, чтобы получить 1 — и это справедливо также для любой степени двойки. У нас есть неограниченный запас простых чисел, мы могли бы просчитать их наперёд с запасом и делать вычисления по модулю достаточно большого простого числа… Но умножать числа по произвольному модулю быстро не получится даже на степень двойки, потому что после битового сдвига нужно брать остаток от деления, а это медленно.

Время для первого наблюдения — идеально для наших нужд подходит арифметика по модулю . Это число гарантированно взаимно простое с двойкой (потому что нечётное), и по нему легко брать остаток от деления. Как?

Допустим, мы умножили некое число x на некую степень двойки , сделав битовый сдвиг. Если результат уместился в первые n бит, всё отлично. Если результат равен , тоже неплохо. Если больше — то есть случилось переполнение — все биты, начиная с -го, надо обнулить. И для этого есть простой алгоритм!

В вычислениях по модулю имеем . Домножая равенство на , получим . Соответственно, каждый лишний -й бит мы можем превратить в вычитание-го бита. Собираем по битам число, которое надо вычесть, и вычитаем по модулю — эта операция стоит . Et voilà!

И дополнительный приятный факт: . Двойка является корнем из единицы заведомо известной нам степени .

Что там с асимптотикой?

Окей, мы решили, что сводим задачу умножения длинных чисел к нескольким задачам умножения чисел по модулю посредством быстрого преобразования Фурье. Более формально, если на входе у нас были числа длиной бит, мы делим их на частей по бит, , по уже хорошо знакомой нам процедуре:

Дальше снова превращаем их в многочлены степени :

Коэффициенты этих многочленов рассматриваем в арифметике по модулю , где — количество бит, способное вместить сумму произведений двух чисел длины бит, чтобы не было переполнения. Именно таким может быть самый большой коэффициент в произведении этих двух многочленов:

Мы уже знаем, что можно свести умножение таких многочленов к умножению чисел в посредством быстрого преобразования Фурье; эти умножения поменьше легко можно провести этим же алгоритмом, а взять потом остаток от деления на , как мы уже выяснили, можно легко и быстро. Само сведение — быстрое преобразование Фурье в модульной арифметике с быстрыми корнями из единицы — обойдётся нам в сложений и умножений стоимости , то есть суммарно битовых операций. Итого сложность совершения умножения имеет вид

Насколько крутое ускорение мы можем получить? Сейчас я проведу сильно упрощённый анализ «на пальцах»; более строгий можно глянуть, например, в [5].

Если выбрать и учесть, что очень мало по сравнению с , имеем

Сделав замену , получим более простую для анализа формулу:

Так, рекуррентное соотношение. От бит переходим к , потом к , потом к и так далее. Раскрывая раз, получаем

убывает очень быстро, и как только мы дойдём до какого-то достаточно малого числа, рекурсия остановится. Какая будет глубина рекурсии? Можем заметить, что на -м шаге длина чисел, с которыми мы имеем дело, имеет вид

Соответственно, решая уравнение , получаем глубину рекурсии . Складывая степени двойки от нулевой до (в равенстве выше они умножаются на const), получаем и суммарную сложность

И это медленно! В первом алгоритме Шенхаге-Штрассена мы уже добились сложности чуть хуже . Нужно ускорить ещё.

Наблюдение второе: самые короткие многочлены

В выражении, оценивающем сложность

можно заметить, что основной вклад в слишком большой результат вносят множители-степени двойки. Они там потому, что разбивая число на частей, мы сводим задачу к умножений. Это нужно, потому что при умножении двух -битных чисел получается -битное число.

Но нам не нужно -битное число. Как только мы сделали первый шаг рекурсии, все вычисления у нас происходят по модулю . А при вычислениях по модулю длина чисел не может изменяться! Можно ли как-то вместо удвоения длины и взятия остатка от деления обойтись расчётами на битах?

Если мы хотим при этом сохранить преимущества быстрого преобразования Фурье, от перемножения многочленов мы отказаться не можем. Есть ли в мире многочленов что-то, похожее на арифметику по модулю? Есть!

Арифметика многочленов по модулю.

В жизни нужно довольно рано свернуть не туда, чтобы заработать опыт в арифметике многочленов. Но ничего страшного в ней нет; связь между многочленами и числами, которую мы используем для построения быстрых алгоритмов умножения, также делает арифметику многочленов довольно похожей на привычную.

Складывать и умножать многочлены мы уже умеем — там всё очевидно. Менее очевидно, что многочлены также можно друг на друга делить с остатком — причём это можно делать вот прямо в столбик, как обычные целые числа.

Можем быстро проверить в SymPy, что всё правильно:

import sympy x = sympy.Symbol("x") f = 5*x**5 + 4*x**4 + 3*x**3 + 2*x**2 + x g = x**2 + 2*x + 3 sympy.div(f, g, domain="Q") # (5*x**3 - 6*x**2 + 20, -39*x - 60)

Как только есть деление с остатком, можно ввести арифметику по модулю; основанием такой модульной арифметики, соответственно, будет уже не число , а многочлен . Подобно тому, как мы строим модульную арифметику, объявляя, что теперь число эквивалентно нулю, здесь мы объявляем, что многочлен эквивалентен нулю. Многочлен становится эквивалентен , — многочлену и так далее.

Всё это продолжает работать, если мы меняем арифметику коэффициентов многочленов, заменяя привычные нам целые или вещественные числа на арифметику по модулю . Выстраивается каскад абстракций: многочлен — это сумма степеней икса с коэффициентами из некоторого множества; неважно, какого именно — главное, чтобы мы эти коэффициенты могли складывать, умножать и (иногда) делить. Этого достаточно, чтобы определить сложение, умножение и деление для самих многочленов, в том числе по модулю . Коэффициенты при этом живут своей независимой жизнью.

При обычном перемножении многочленов коэффициент при соответствует битам произведения чисел, начиная с -го. Если же мы делаем расчёты по модулю , коэффициент при становится эквивалентен нулевому коэффициенту (при ) со знаком минус:

А в задаче умножения по модулю -й бит… тоже соответствует нулевому биту со знаком минус!

Это и есть наше ключевое второе наблюдение: перемножение многочленов по модулю эквивалентно перемножению чисел по модулю .

Теперь у нашего многочлена-произведения есть только значащих коэффициентов. Но есть нюанс: как только мы начинаем рассматривать многочлены по модулю, фраза значение многочлена в точке теряет смысл. Многочлен в арифметике по модулю — это не конкретная функция , а совокупность функций вида

Коэффициенты могут быть произвольными, и могут изменять значения многочлена в разных точках.

Мы можем быстренько провести вычислительный эксперимент, чтобы проверить, насколько хорошо заработает наш проверенный подход с матрицей Вандермонда, если мы попытаемся перемножать многочлены по модулю, просто урезав длину вектора коэффициентов. Попробуем взять разные наборы точек и посмотрим, что будет.

# Будем делить числа на 4 куска по 2 бита. m = 4 n_ = 4 # n' с запасом # Основание арифметики коэффициентов: mod = 2**n_ + 1 # 17 # Два 8-битных множителя: a_coefs = [0b01, 0b01, 0b10, 0b01] # 101 b_coefs = [0b01, 0b11, 0b00, 0b01] # 77 def construct_vandermonde_matrix_and_inverse(xs: List[int]): # Строим матрицу Вандермонда "в лоб", # потому что нам нужна модульная арифметика: v = np.array([ [pow(xs[i], j, mod) for j in range(m)] for i in range(m) ]) # Дальше хитрый фокус, чтобы обратить матрицу # в модульной арифметике. Да, мне было лень писать # метод Гаусса или конвертировать матрицу из sympy. det = int(round(np.linalg.det(v))) det_inv = pow(det, mod - 2, mod) v_inv_real = np.linalg.inv(v) * det * det_inv v_inv = np.array(np.round(v_inv_real), dtype=int) % mod # Проверяем, что действительно получилась # обратная матрица в модульной арифметике: assert np.all((v @ v_inv) % mod == np.eye(m, dtype=int)) return v, v_inv # Перебираем разные наборы точек: for xs in [ [2, 8, 15, 9], [2, 4, 8, 16], [3, 5, 7, 11], [5, 7, 11, 13], ]: v, v_inv = construct_vandermonde_matrix_and_inverse(xs) a_y = (v @ a_coefs) % mod b_y = (v @ b_coefs) % mod c_y = (a_y * b_y) % mod c_coefs = (v_inv @ c_y) % mod print(c_coefs) # [14 2 4 8] # [ 2 10 4 16] # [ 4 4 0 8] # [ 0 6 4 13]

В зависимости от выбранных точек получаем разные коэффициенты произведения! Но, в отличие от прошлого раза, теперь дело заведомо не в вычислительной погрешности — уж перемножать целые числа мы умеем точно.

Дело как раз в том, что мы никак не учли здесь, что мы умножаем по модулю . Как это учесть?

Посмотрим ещё раз на многочлен по модулю:

На самом деле есть несколько точек, в которых фраза значение многочлена в точке не бессмысленна. Эти точки — корни многочлена ! В этих точках «произвольная» часть этой обобщённой функции всегда обращается в ноль, благодаря чему значение фиксируется.

Какие это точки в примере выше?

def xmp1(x): return (pow(x, m, mod) + 1) % mod #

А это — нечётные степени двойки, взятые по модулю:

[pow(2, i, mod) for i in range(1, mod, 2)] # [2, 8, 15, 9, 2, 8, 15, 9]

В более общем случае это нечётные степени числа .

Нечётные степени не очень удобны для быстрого преобразования Фурье. Тут остаётся последний шаг — факторизовать матрицу Вандермонда для чисел , превратив в произведение матрицы Фурье и диагональной (которая не мешает, потому что на неё можно быстро умножать):

Итак, с перемножением многочленов по модулю разобрались — вернёмся к анализу сложности алгоритма. Поскольку мы перешли от многочленов порядка к многочленам порядка , множитель 2 в формуле сложности алгоритма исчезает:

Глубина рекурсии остаётся порядка , но без накапливающегося множителя-двойки также становится порядка :

Суммарная сложность теперь имеет вид

Что наконец-то превосходит первый алгоритм Шенхаге-Штрассена!

Собираем алгоритм

Итак! Перейдём к полному описанию второго алгоритма.

    Формулируем изначальную задачу как задачу умножения по модулю . Какие бы у нас ни были изначально числа, всегда можно взять достаточно большим, чтобы результат умножения влез в бит. Для удобства рекурсии выбираем в качестве степень двойки.

Вот и всё! Все гениальные приёмы второго алгоритма Шенхаге-Штрассена лежат у нас перед глазами. Конечно, для реализации этого алгоритма, эффективной на практике, нужно учесть ещё много нюансов и применить много приёмов; но на уровне идейном мы освоили его целиком.

В совсем не таком уж далёком 1960 году Андрей Колмогоров, один из величайших математиков своего времени, выдвинул гипотезу, что невозможно умножать числа быстрее, чем за . И совершенно изумительно видеть, насколько быстрее оказалось возможно умножать числа, чем это изначально предполагалось. В этой статье мы рассмотрели четыре основополагающих алгоритма, перейдя от к ; для их понимания нам пришлось разобраться в теории Фурье, модульной арифметике и алгебре многочленов.

За рамками статьи остался алгоритм Фюрера и его варианты, а также опубликованный в 2020-м году [6] алгоритм с заявленной сложностью ; если эта статья окажется востребована, возможно, когда-нибудь мы разберём и их.

Спасибо за внимание!

P. S.

Эту статью я старался написать максимально популярно, избегая присущей учебникам математической строгости. Некоторые формулы намеренно упрощены, где-то вообще удалось обойти без них (мало где); в их оформлении там, где они остались, я сделал акцент на читаемость с ходу, без разбора с бумажкой и ручкой (выделение цветом, зачёркивание сокращённых величин); также я старался избегать математической терминологии, где это возможно («арифметика по модулю» вместо «в кольце вычетов по модулю»). Судьёй того, насколько моя задумка удалась, предстоит быть тебе, читатель; мне же остаётся лишь надеяться, что кто-то сможет почерпнуть из этого текста свежий взгляд на математику, обычно такую возвышенно-труднодоступную.

Литература

  1. Pan, Victor. «How can we speed up matrix multiplication?.» SIAM review 26.3 (1984): 393-415. PDF
  2. Pan, Victor. «Strassen’s algorithm is not optimal trilinear technique of aggregating, uniting and canceling for constructing fast algorithms for matrix operations.» 19th Annual Symposium on Foundations of Computer Science (sfcs 1978). IEEE, 1978.
  3. Pan, Victor. «How bad are Vandermonde matrices?.» SIAM Journal on Matrix Analysis and Applications 37.2 (2016): 676-694. PDF
  4. Pharr, Matt, and Randima Fernando. GPU Gems 2: Programming techniques for high-performance graphics and general-purpose computation (gpu gems). Addison-Wesley Professional, 2005. HTML
  5. Kruppa, Alexander. «A GMP-based implementation of Schonhage-Strassen’s large integer multiplication algorithm.» PDF
  6. Harvey, David, and Joris Van Der Hoeven. «Integer multiplication in time O (n log n).» Annals of Mathematics 193.2 (2021): 563-617. PDF

Библиотеки

  1. NumPy использовалась для большинства численных экспериментов в статье.
  2. SymPy использовалась для нескольких символьных вычислений.
  3. GMP неоднократно упоминалась как стандартная реализация длинной арифметики.

Умножение столбца чисел на одно и то же число

Предположим, нужно умножить столбец чисел на одно и то же число в другой ячейке. Для этого перед копированием формулы ее нужно изменить, добавив в адрес ячейки множителя символы $.

В нашем примере ниже требуется умножить все числа в столбце A на число 3 в ячейке C2. Формула =A2*C2 даст правильный результат (4500) в ячейке B2. Однако копирование формулы в последующие ячейки столбца B не будет работать, поскольку ссылка на ячейку C2 изменяется на C3, C4 и т. д. Так как в этих ячейках нет данных, результат в ячейках от B3 до B6 будет равен нулю.

Данные в столбце A, формулы в столбце B и число 3 в ячейке C2

Чтобы умножить все числа в столбце A на ячейку C2, добавьте символы $ в ссылку на ячейку следующим образом: $C$2, как показано в следующем примере.

Числа в столбце A, формула в столбце B с символами $ и число 3 в столбце C

Использование символов $ сообщает Excel, что ссылка на C2 является «абсолютной», поэтому при копировании формулы в другую ячейку ссылка всегда будет на ячейку C2. Чтобы создать формулу, выполните следующие действия:

  1. В ячейке B2 введите знак равенства (=).
  2. Щелкните ячейку A2, чтобы добавить ее в формулу.
  3. Введите символ «звездочка» (*).
  4. Щелкните ячейку C2, чтобы добавить ее в формулу.
  5. Введите символ $ перед C и еще один перед 2: $C$2.
  6. нажмите клавишу ВВОД.

Совет: Вместо ввода символа $ можно поместить точку вставки перед или после ссылки на ячейку, которую вы хотите сделать абсолютной, и нажать клавишу F4, которая добавит символы $.

Теперь вернемся немного назад и рассмотрим простой способ скопировать формулу в последующие ячейки столбца после нажатия клавиши ВВОД в ячейке B2.

  1. Выберите ячейку B2.
  2. Дважды щелкните маленький зеленый квадрат в правом нижнем углу ячейки.

Формула автоматически копируется на последующие ячейки столбца до ячейки B6.

Зеленый квадрат в правом нижнем углу ячейки B2

После копирования формулы в столбце B появляются правильные результаты.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *