Многочлены - Алгоритмика
Многочлены

Многочлены

автор Сергей Слотин
обновлено сент. 14, 2021
Многочленами или полиномами (англ. polynomial) называются конечные суммы вида P(x1,x2,,xn)=I={i1,i2,,in}cIx1i1x2i2xnin P(x_1, x_2, \ldots, x_n) = \sum_{I=\{i_1,i_2,\ldots,i_n\}} c_I \cdot x_1^{i_1} \cdot x_2^{i_2} \cdot \ldots \cdot x_n^{i_n} В частности, многочлены от одной переменной это суммы вида P(x)=c0+c1x1++cnxn P(x) = c_0 + c_1 x^1 + \ldots + c_n x^n

Говорят, что степень многочлена равна nn, если его наибольший ненулевой коэффициент стоит у xnx^n.

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

#Представление многочленов и чисел

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

A(x)=a0+a1x+a2x2++anxn=a0+a110+a2102++an10n \begin{aligned} A(x) &= a_0 + a_1\cdot x + a_2 \cdot x^2 + \dots + a_n \cdot x^n \\ &= a_0 + a_1\cdot 10 + a_2 \cdot 10^2 + \dots + a_n \cdot 10^n \end{aligned}

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

const int base = 10;

vector<int> normalize(vector<int> a) {
    int carry = 0;
    for (int &x : a) {
        x += carry;
        carry = x / base;
        x %= base;
    }
    while (carry > 0) {
        a.push_back(carry % base);
        carry /= base;
    }
    return a;
}

vector<int> multiply(vector<int> a, vector<int> b) {
    return normalize(poly_multiply(a, b));
}

Из соображений производительности следует выбирать настолько большое основание, насколько вмещается в используемый тип данных (например, 10910^9 или 2302^{30} для int).

#Коэффициенты разложений

Коэффициенты при axbya^x b^y, получаемые при возведении бинома (a+b)(a+b) в nn-ную степень, называются биномиальными коэффициентами:

(a+b)n=k=0nCnkakbnk (a + b)^n = \sum_{k=0}^n C_n^k \cdot a^k \cdot b^{n-k} Их удобно считать по следующей формуле: Cnk=(nk)=n!(nk)!k! C_n^k = \binom{n}{k} = \frac{n!}{(n - k)! k!} В более общем случае определяют полиномиальный коэффициент, равный количеству раз, которое элемент a1x1a2x2akxka_1^{x_1} a_2^{x_2} \ldots a_k^{x_k} появится при раскрытии скобки (a1+a2++ak)n(a_1+a_2+\ldots+a_k)^n: P(x1,x2,,xk)=n!(xi!) P(x_1, x_2, \ldots, x_k) = \frac{n!}{\prod (x_i!)}

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

#Умножение многочленов

При умножении двух многочленов степени nn и mm получается многочлен степени (n+m)(n+m). Прямая формула для произведения многочленов имеет вид

(i=0naixi)(j=0mbjxj)=k=0n+mxki+j=kaibj \left(\sum_{i=0}^n a_i x^i\right)\cdot\left(\sum_{j=0}^m b_j x^j\right)=\sum_{k=0}^{n+m}x^k\sum_{i+j=k}a_i b_j

Её наивный подсчёт требует O(n2)O(n^2) операций, но далее в этой главе мы разберем несколько более эффективных алгоритмов — самый быстрый из которых работает всего за O(nlogn)O(n \log n).

Этот факт позволяет сводить много комбинаторных задач к произведению многочленов и использованию уже известных алгоритмов для его подсчета. Разберем несколько примеров таких задач.

#2-рюкзак

Даны два массива aa и bb размера nn и mm. Требуется найти число различных возможных сумм (ai+bj)(a_i + b_j).

n,m,ai,bi105n, m, a_i, b_i \le 10^5.

Рассмотрим многочлены A(x)A(x) и B(x)B(x), в которых коэффициент при kk-той степени равен числу равных kk элементов в соответствующем массиве.

Рассмотрим произведение C=ABC = A \cdot B. В получившемся многочлене коэффициент ctc_t при xtx^t будет равен

ctxt=p+q=tapbqxp+q c_t \cdot x^t = \sum_{p+q=t} a_p \cdot b_q \cdot x^{p+q}

что в свою очередь равно количеству способов набрать сумму ровно tt.

Значит, мы можем перемножить эти два многочлена за O(nlogn)O(n \log n) и просто подсчитать число ненулевых коэффициентов результата.

#Мульти-рюкзак

Задача «Вор в магазине» является небольшой модификацией предыдущей:

Имеется nn типов предметов различных целых стоимостей aia_i. Требуется найти количество различный сумм стоимостей наборов из ровно kk предметов (возможно, с повторениями).

n,k,ai1000n, k, a_i \le 1000

Опять же, рассмотрим многочлен, в котором коэффициент при ii-той степени равен единице, если существует предмет со стоимостью ii, и нулю в противном случае.

Если k=2k=2, наша задача свелась к предыдущей: нужно домножить многочлен на самого себя и посмотреть на число ненулевых коэффициентов. В общем же случае нам нужно возвести многочлен в степень kk и также посчитать ненулевые коэффициенты результата.

Если возводить многочлен в kk-ную степень наивно, то асимптотика такого решения будет O(nk2log(nk))O(n k^2 \log (nk)): нужно O(k)O(k) раз перемножать два многочлена, больший из которых имеет длину O(nk)O(nk).

Воспользуемся бинарным возведением в степень: умножение многочленов ведь ассоциативно. В данном случае асимптотика будет не более O(nklog(nk)logk)O(nk \log (nk) \log k): нужно O(logk)O(\log k) раз умножать два многочлена порядка O(nk)O(nk). Но на самом деле, так как на каждой итерации размер многочлена будет увеличиваться в два раза, в асимптотике учтется только последнее (самое большое) умножение, и поэтому в действительности время работы составит O(nklog(nk))O(nk \log (nk)).

#Свёртки

Свёрткой (англ. convolution) называется операция применения некоторой «оконной» функции ко всем отрезкам фиксированной длины исходной функции.

Свёртка «площадь функции на единичном отрезке»

В дискретном случае свертке соответствует сумме вида

(fg)(x)=f(1)g(x1)+f(2)g(x2)++f(k)g(xk) (f * g)(x)= f(1) \cdot g(x-1) + f(2) \cdot g(x-2) + \dots + f(k) \cdot g(x - k) В ещё более узком смысле, свертка это результат перемножения многочленов: (AB)k=a0bk+a1bk1++akb0 (A \cdot B)_k = a_0 \cdot b_k + a_1 \cdot b_{k-1} + \ldots + a_k \cdot b_0

то есть kk-тый коэффициент результата равен применению какой-то оконной функции, заданной коэффициентами B(x)B(x), к коэффициентам A(x)A(x). Значит, подобные функции можно быстро считать через матричное умножение.

Например, так можно (неэффективно) искать битовую подстроку tt в строке ss: запишем символы ss как коэффициенты многочлена A(x)A(x) и символы tt в обратном порядке как коэффициенты многочлена B(x)B(x) и перемножим. В позициях многочлена-результата, где коэффициенты равны t|t|, строка tt входит в ss.

Также с помощью этого трюка можно решать и другие задачи, например выполнять «fuzzy searching»: коэффициенты, равные (td)(|t|-d), соответствуют вхождениям с ровно dd ошибками.