Рассмотрим такую распространённую операцию как умножение двух целых чисел. Квадратичный алгоритм — умножения в столбик — все знают со школы. Долгое время предполагалось, что ничего быстрее придумать нельзя.
Первым эту гипотезу опроверг Анатолий Карацуба. Его алгоритм сводит умножение двух
Чтобы перейти к алгоритму с лучшей оценкой, нам нужно сначала установить несколько фактов о многочленах.
Обратим внимание на то, что любое число можно представить многочленом:
Основание x при этом может быть выбрано произвольно.
Чтобы перемножить два числа, мы можем перемножить соответствующие им многочлены, а затем произвести каррирование: пройтись от нижних разрядов получившегося многочлена и «сдвинуть» переполнившиеся разряды:
const int base = 10;
<int> normalize(vector<int> a) {
vectorint carry = 0;
for (int &x : a) {
+= carry;
x = x / base;
carry %= base;
x }
while (carry > 0) {
.push_back(carry % base);
a/= base;
carry }
return a;
}
<int> multiply(vector<int> a, vector<int> b) {
vectorreturn normalize(poly_multiply(a, b));
}
Прямая формула для произведения многочленов имеет вид
Её подсчёт требует
Теорема. Пусть есть набор различных точек
Доказательство будет конструктивным — можно явным образом задать многочлен, который принимает заданные значения
Корректность. Проверим, что в точке
Для
Для всех остальных слагаемых произведение занулится: один из множетелей будет равен
Уникальность. Предположим, есть два подходящих многочлена степени
для какого-то числа
Этот многочлен называется интерполяционным многочленом Лагранжа, а сама задача проведения многочлена через точки — интерполяцией.
Примечание. На практике интерполяцию решают методом Гаусса: её можно свести к решению линейного уравнения
Важный факт: многочлен можно однозначно задать не только своими коэффициентами, но также корнями и значениями хотя бы в
Что происходит со значениями многочлена-произведения
Основная идея алгоритма: если мы знаем значения в каких-то различных
<int> poly_multiply(vector<int> a, vector<int> b) {
vector<int> A = evaluate(a);
vector<int> B = evaluate(b);
vectorfor (int i = 0; i < A.size(); i++)
[i] *= B[i];
Areturn interpolate(A);
}
Если притвориться, что evaluate
и interpolate
работают за линейное время, то умножение тоже будет работать за линейное время.
К сожалению, непосредственное вычисление значений требует
Но что, если бы мы могли вычислять значения в точках и делать интерполяцию быстрее?
Определение. Комплексные числа — это числа вида
Комплексные числа ввели в алгебре, чтобы работать с корнями из отрицательных чисел:
С комплексными числами можно работать почти так же, как с действительными. Они даже удобнее: все квадратные корни всегда извлекаются, все корни многочленов всегда находятся.
Комплексные числа удобно изображать на плоскости в виде вектора
Модулем комплексного числа называется действительное число
Аргументом комплексного числа называется действительное число
Таким образом комплексное число можно представить в полярных координатах:
Подобное представление удобно по следующей причине: чтобы перемножить два комплексных числа, нужно перемножить их модули и сложить аргументы.
Упражнение. Докажите это.
Определим число Эйлера
Просто введём такую нотацию для выражения
Геометрически, все такие точки живут на единичном круге:
Такая нотация удобна, потому что можно обращаться с
Упражнение. Проверьте это: раскройте скобки и проделайте немного алгебры.
У комплексных чисел есть много других замечательных свойств, но нам для алгоритма на самом деле потребуется только следующее:
Утверждение. Для любого натурального
А именно, это будут числа вида:
где
На комплексной плоскости эти числа располагаются на единичном круге на равном расстоянии друг от друга:
Первый корень
Будем обозначать
Упражнение. Докажите, что других корней быть не может.
Дискретным преобразованием Фурье называется вычисление значений многочлена в комплексных корнях из единицы:
Обратным дискретным преобразованием Фурье называется, как можно догадаться, обратная операция — интерполяция коэффициентов
Почему эта формула верна? При вычислении ПФ мы практически применяем матрицу к вектору:
То есть преобразование Фурье — это просто линейная операция над вектором:
Как будет выглядеть эта
Проверим, что при перемножении
Значение
Значение любого недиагонального (
Внимательный читатель заметит симметричность форм
Напомним, что мы изначально хотели перемножать многочлены следующим алгоритмом:
Посчитаем значения в
Перемножим эти значения за
Интерполяцией получим многочлен-произведение.
В общем случае быстро посчитать интерполяцию и даже просто посчитать значения в точках нельзя, но для корней единицы — можно. Если научиться быстро считать значения в корнях и интерполировать (прямое и обратное преобразование Фурье), но мы можно решить исходную задачу.
Соответствующий алгоритм называется быстрым преобразованием Фурье (англ. fast Fourier transform). Он использует парадигму «разделяй-и-властвуй» и работает за
Обычно, алгоритмы «разделяй-и-властвуй» делят задачу на две половины: на первые
Представим многочлен в виде
Пусть
Зная это, исходную формулу для значения многочлена в точке
Ключевое замечание: корней вида
У нас по сути в два раза меньше корней (но они так же равномерно распределены на единичной окружности) и в два раза меньше коэффициентов — мы только что успешно уменьшили нашу задачу в два раз.
Сам алгоритм заключается в следующем: рекурсивно посчитаем БПФ для многочленов
Заметим, что если
Таким образом, мы свели преобразование размера
Заметим, что предположение о делимости
Приведём код, вычисляющий БПФ по схеме Кули-Тьюки:
typedef complex<double> ftype;
const double pi = acos(-1);
template<typename T>
<ftype> fft(vector<T> p, ftype w) {
vectorint n = p.size();
if(n == 1)else { {
return {p[0]};
else {
<T> AB[2];
vectorfor(int i = 0; i < n; i++)
[i % 2].push_back(p[i]);
ABauto A = fft(AB[0], w * w);
auto B = fft(AB[1], w * w);
<ftype> res(n);
vector= 1;
ftype wt int k = n / 2;
for(int i = 0; i < n; i++) {
[i] = A[i % k] + wt * B[i % k];
res*= w;
wt }
return res;
}
}
<ftype> evaluate(vector<int> p) {
vectorwhile(__builtin_popcount(p.size()) != 1)
.push_back(0);
preturn fft(p, polar(1., 2 * pi / p.size()));
}
Как обсуждалось выше, обратное преобразование Фурье удобно выразить через прямое:
<int> interpolate(vector<ftype> p) {
vectorint n = p.size();
auto inv = fft(p, polar(1., -2 * pi / n));
<int> res(n);
vectorfor(int i = 0; i < n; i++)
[i] = round(real(inv[i]) / n);
resreturn res;
}
Теперь мы умеем перемножать два многочлена за
<int> poly_multiply(vector<int> a, vector<int> b) {
vector<int> A = fft(a);
vector<int> B = fft(b);
vectorfor (int i = 0; i < A.size(); i++)
[i] *= B[i];
Areturn interpolate(A);
}
Примечание. Приведённый выше код, являясь корректным и имея асимптотику
Читателю рекомендуется самостоятельно задуматься о том, как можно улучшить время работы и точность вычислений. Из наиболее важных недостатков:
внутри преобразования не должно происходить выделений памяти
работать желательно с указателями, а не векторами
корни из единицы должны быть посчитаны наперёд
Следует избавиться от операций взятия остатка по модулю
Вместо вычисления преобразования с
Здесь приведена одна из условно пригодных реализаций.
Но главная проблема в численной стабильности — мы нарушили первое правило действительных чисел. Однако, от неё можно избавиться.
Нам от комплексных чисел на самом деле нужно было только одно свойство: что у единицы есть
Нам нужно просто найти такую пару
Это число простое, и при этом является ровно на единицу больше числа, делящегося на большую степень двойки. При
Реализация практически не отличается.
const int MOD = 998244353, W = 805775211, IW = 46809892;
const int MAXN = (1 << 19), INV2 = 499122177;
// W - первообразный корень MAXN-ной степени из 1, IW - обратное по модулю MOD к W
// Первообразный корень (1 << 23)-й степени из 1 по модулю MOD равен 31; тогда первообразный корень (1 << X)-й степени для X от 1 до 23 равен (31 * (1 << (23 - X))) % MOD
// INV2 - обратное к двум по модулю MOD
// Данная реализация FFT перемножает два целых числа длиной до 250000 цифр за ~0.13 секунд без проблем с точностью и занимает всего 30 строк кода
int pws[MAXN + 1], ipws[MAXN + 1];
void init() {
[MAXN] = W; ipws[MAXN] = IW;
pwsfor (int i = MAXN / 2; i >= 1; i /= 2) {
[i] = (pws[i * 2] * 1ll * pws[i * 2]) % MOD;
pws[i] = (ipws[i * 2] * 1ll * ipws[i * 2]) % MOD;
ipws}
}
void fft(vector<int> &a, vector<int> &ans, int l, int cl, int step, int n, bool inv) {
if (n == 1) { ans[l] = a[cl]; return; }
(a, ans, l, cl, step * 2, n / 2, inv);
fft(a, ans, l + n / 2, cl + step, step * 2, n / 2, inv);
fftint cw = 1, gw = (inv ? ipws[n] : pws[n]);
for (int i = l; i < l + n / 2; i++) {
int u = ans[i], v = (cw * 1ll * ans[i + n / 2]) % MOD;
[i] = (u + v) % MOD;
ans[i + n / 2] = (u - v) % MOD;
ansif (ans[i + n / 2] < 0) ans[i + n / 2] += MOD;
if (inv) {
[i] = (ans[i] * 1ll * INV2) % MOD;
ans[i + n / 2] = (ans[i + n / 2] * 1ll * INV2) % MOD;
ans}
= (cw * 1ll * gw) % MOD;
cw }
}
С недавнего времени некоторые проблемсеттеры начали использовать именно этот модулю вместо стандартного
Даны две бинарные строки
и . Нужно найти такой циклический сдвиг строки , что количество совпадающих соответствующих символов с станет максимально.
Сперва научимся для каждого циклического сдвига