【学习笔记】多项式
开个大坑。
FFT 前的前置知识
复数
应该想学FFT的人都会复数吧
复数,即形如
在复平面上,一个复数代表着一个点。所谓复平面,即以实数为
对于复数加减,直接将实部与虚部相加即可。对于复数乘法,
在复平面上的复数乘法有这样一个口诀:模相乘,幅角相加。
模即在复平面上点到原点的距离,幅角即相对于
单位根
我们在复平面上做一个半径为
这样,我们发现,这些点的模长都为
具体的,我们记
根据模相乘,幅角相加,那么第二个点就是
单位根的一些性质:
这两个应该比较容易理解。四等分的第二份,相当于八等分的第四份,也相当于两等分的第一份。
因为 ,所以上式正确。
如何计算单位根?由于是将单位圆进行
即:
C++ 中提供了复数类 complex
,可以去网上搜索相关用法。不过也可以直接自己写一个,并不难写。
点值表示法
一般,我们所见的多项式的表示方式都是系数表示法,即直接给出
对于多项式乘法来说,系数表示法不好计算,但是点值表示法就很容易计算,直接将相对应的数字相乘即可。所以,我们考虑将系数表示法转换为点值表示法。
离散傅里叶变换
离散傅里叶变换,即 DFT,是一种将系数表示法快速转换为点值表示法的变换。
对于一个
我们将它按照奇偶性分开:
我们分别再设两个多项式
我们将
同理:
那么我们发现,只要求出
因为我们进行分治,为了保证左右区间长度相等(不然不好合并),我们可以提前将多项式补到
贴一段 OI-Wiki 的代码(我没写过递归版本的):
#include <cmath>
#include <complex>
typedef std::complex<double> Comp; // STL complex
const Comp I(0, 1); // i
const int MAX_N = 1 << 20;
Comp tmp[MAX_N];
void DFT(Comp *f, int n, int rev) { // rev=1,DFT; rev=-1,IDFT
if (n == 1) return;
for (int i = 0; i < n; ++i) tmp[i] = f[i];
for (int i = 0; i < n; ++i) { // 偶数放左边,奇数放右边
if (i & 1)
f[n / 2 + i / 2] = tmp[i];
else
f[i / 2] = tmp[i];
}
Comp *g = f, *h = f + n / 2;
DFT(g, n / 2, rev), DFT(h, n / 2, rev); // 递归 DFT
Comp cur(1, 0), step(cos(2 * M_PI / n), sin(2 * M_PI * rev / n));
// Comp step=exp(I*(2*M_PI/n*rev)); // 两个 step 定义是等价的
for (int k = 0; k < n / 2; ++k) {
tmp[k] = g[k] + cur * h[k];
tmp[k + n / 2] = g[k] - cur * h[k];
cur *= step;
}
for (int i = 0; i < n; ++i) f[i] = tmp[i];
}
时间复杂度是
但是这样常数会很大。中间我们不断的对系数进行了交换,我们可以考虑优化这一过程,来观察最后系数位置的变化:
我们将位置化为二进制,来模拟一下这个交换过程:
发现:一个数最后的位置就是一开始的位置的二进制左右翻转。
其实再仔细观察下会发现,第
于是,我们可以首先预处理出数组
直接放代码:
for (int i = 0; i < limit; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) * limit >> 1)
自己手模一下就可以,不难理解。
提前交换完毕后,我们发现:合并时,我们将
Complex x = a[k], y = w * a[k + n / 2];
a[k] = x + y, a[k + n / 2] = x - y;
(仅为示例,在实际代码中有所不同)
这样,我们就可以不用任何额外数组,就做到了合并一次答案。最后,我们还可以将递归舍去:直接枚举要合并的区间的长度和区间的位置进行合并。
放代码:
void dft(int limit) {
for (int i = 0; i < limit; i++) if (i < r[i]) swap(a[i], a[r[i]]);
for (int mid = 1; mid < limit; mid <<= 1) { // 枚举区间长度
Complex step(cos(PI / mid), sin(PI / mid)); // 单位根
for (int l = 0, len = mid << 1; l < limit; l += len) { // 枚举区间左端点
Complex cur(1, 0);
for (int k = 0; k < mid; k++, cur = cur * step) { // 枚举 k
Complex x = a[l + k], y = cur * a[l + k + mid];
a[l + k] = x + y;
a[l + k + mid] = x - y;
}
}
}
}
逆离散傅里叶变换
逆离散傅里叶变换,即 IDFT,就是将点值表示法转回系数表示法的方法。
我们从线性代数的角度考虑上面的 DFT 操作,其实就是将系数向量乘上了一个矩阵,也就是:
我们记左面的点值向量为
我们求
考虑构造
发现这个矩阵就是单位矩阵的
这样,我们将点值向量左乘一个
具体实现上,由于 type
参数,表示是否是逆操作,那么就可以写出下面的代码:
void dft(int limit, int type) {
for (int i = 0; i < limit; i++) if (i < r[i]) swap(a[i], a[r[i]]);
for (int mid = 1; mid < limit; mid <<= 1) { // 枚举区间长度
Complex step(cos(PI / mid), type * sin(PI / mid)); // 单位根
for (int l = 0, len = mid << 1; l < limit; l += len) { // 枚举区间左端点
Complex cur(1, 0);
for (int k = 0; k < mid; k++, cur = cur * step) { // 枚举 k
Complex x = a[l + k], y = cur * a[l + k + mid];
a[l + k] = x + y;
a[l + k + mid] = x - y;
}
}
}
if (type == -1) // 不要忘记 1/n
for (int i = 0; i < limit; i++) a[i].r /= limit;
}
附封装版的完整代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 60005;
struct Complex {
double r, i;
Complex(double r = 0, double i = 0) : r(r), i(i) {}
Complex operator+(Complex b) { return { r + b.r, i + b.i }; }
Complex operator-(Complex b) { return { r - b.r, i - b.i }; }
Complex operator*(Complex b) { return { r * b.r - i * b.i, r * b.i + i * b.r }; }
};
int r[MAXN];
const double PI = acos(-1.0);
struct Polynomial {
vector<Complex> a;
int len;
Complex& operator[](int b) { return a[b]; }
Polynomial(int len = 0) : len(len) { a.resize(len + 1); }
void set(int len) { this->len = len, a.resize(len + 1); }
void dft(int limit, int type) {
set(limit);
for (int i = 0; i < limit; i++)
if (i < r[i]) swap(a[i], a[r[i]]);
for (int mid = 1; mid < limit; mid <<= 1) {
Complex step(cos(PI / mid), type * sin(PI / mid));
for (int l = 0; l < limit; l += (mid << 1)) {
Complex w(1, 0);
for (int j = 0; j < mid; j++, w = w * step) {
Complex x = a[l + j], y = w * a[l + j + mid];
a[l + j] = x + y, a[l + j + mid] = x - y;
}
}
}
if (type == -1) for (int i = 0; i < limit; i++) a[i].r /= limit;
}
Polynomial operator*(Polynomial b) {
Polynomial a = *this, c;
int n = len + b.len;
int limit = 1;
while (limit <= n) limit <<= 1;
c.set(limit);
for (int i = 0; i < limit; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) * limit >> 1);
a.dft(limit, 1), b.dft(limit, 1);
for (int i = 0; i < limit; i++) c[i] = a[i] * b[i];
c.dft(limit, -1);
c.set(n);
return c;
}
};
NTT 快速数论变换
FFT 有一个重要的问题:精度问题。毕竟涉及到浮点数运算,精度误差是不可避免的,而有时候我们需要求取模意义下的数,这时候普通的 FFT 就不适用了。这时候,我们就引出了 NTT。
精度的瓶颈在哪?显然是单位根的运算。我们考虑模意义下什么东西可以代替单位根。
——没错,就是原根。
不明白原根的建议自行百度,上一篇博客写到了原根,但是写的并不详细。
我们来一一看这几条性质:
对于 互不相等。
若模数 为一个质数,那么对于原根 , 互不相等,那么 也互不相等。
由费马小定理可得, ,那么 ,即 。
又因为 ,根据性质 1, ,那么 ,即 。
于是,我们可以得出以下结论:原根就是模意义下的单位根!
不过我们发现,
实际上,有:
另外一些常用的 NTT 模数:
考场上如何检验一个数是不是 NTT 模数?
gnome-calculator 有个功能叫质因数分解
如果模数不是 NTT 模数怎么办?
那你就大骂出题人↓↑
NTT code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3000005, P = 998244353, G = 3, GI = 332748118;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = 1ll * ans * a % P;
a = 1ll * a * a % P;
b >>= 1;
}
return ans;
}
int r[MAXN];
struct Polynomial {
vector<int> a;
int len;
int& operator[](int b) { return a[b]; }
Polynomial(int len = 0) : len(len) { a.resize(len + 1); }
void set(int b) { len = b, a.resize(b + 1); }
void ntt(int limit, bool rev) {
set(limit);
for (int i = 0; i < limit; i++)
if (i < r[i]) swap(a[i], a[r[i]]);
for (int mid = 1; mid < limit; mid <<= 1) {
int step = qpow(rev ? GI : G, (P - 1) / (mid << 1));
for (int l = 0; l < limit; l += (mid << 1)) {
int w = 1;
for (int j = 0; j < mid; j++, w = 1ll * w * step % P) {
int x = a[l + j], y = 1ll * w * a[l + j + mid] % P;
a[l + j] = (x + y) % P, a[l + j + mid] = (1ll * P + x - y) % P;
}
}
}
if (rev) {
int ninv = qpow(limit, P - 2);
for (int i = 0; i < limit; i++)
a[i] = 1ll * a[i] * ninv % P;
}
}
Polynomial operator*(Polynomial b) {
Polynomial a = *this, c; int n = a.len + b.len;
int limit = 1; while (limit <= n) limit <<= 1;
c.set(limit);
for (int i = 1; i < limit; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) * limit >> 1);
a.ntt(limit, 0);
b.ntt(limit, 0);
for (int i = 0; i <= limit; i++) c[i] = 1ll * a[i] * b[i] % P;
c.ntt(limit, 1);
c.set(n);
return c;
}
};
应用
多项式乘法通常可以用于快速计算卷积。
观察多项式乘法的式子:
我们可以转换一下:
我们发现,
于是,我们可以将很多形如卷积形式的式子使用 FFT/NTT 进行优化。
例题:
给出
个数 ,定义 对
,求 的值。
首先化式子:
左面显然是卷积形式,设
那么看右面:右面是差一定的形式,而不是和一定。我们可以将
发现这就也是卷积的形式了,直接计算即可。
见 上篇文章。
全 家 桶 !
实际上我就学了几个,为了防止过段时间全忘了,就先记一下。
多项式乘法逆
给定多项式
考虑倍增。假如现在已经求出了
Polynomial inv(int n) {
Polynomial b;
b[0] = qpow(a[0], P - 2);
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this;
a.set(d - 1);
int limit = d << 1;
calcRev(limit);
a.ntt(limit, 0), b.ntt(limit, 0);
for (int i = 0; i < limit; i++)
b[i] = (2ll - 1ll * a[i] * b[i] % P + P) % P * b[i] % P;
// 直接对点值进行计算,而不是两次多项式乘法,优化常数
b.ntt(limit, 1);
b.set(d - 1);
}
b.set(n - 1);
return b;
}
多项式开根
给定多项式
同样考虑倍增:假设已经求得
求逆元,然后倍增算就可以了。
对于 我不会,咕了。
Polynomial sqrt(int n) {
static int TWOINV = qpow(2, P - 2);
Polynomial b;
b[0] = 1;
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this, c = b.inv(d);
a.set(d - 1);
int limit = d << 1;
calcRev(limit);
a.ntt(limit, 0), c.ntt(limit, 0);
for (int i = 0; i < limit; i++) a[i] = 1ll * a[i] * c[i] % P;
a.ntt(limit, 1);
b.set(d - 1);
for (int i = 0; i < d; i++) b[i] = 1ll * (a[i] + b[i]) * TWOINV % P;
}
b.set(n - 1);
return b;
}
多项式对数函数(多项式 )
给定多项式
直接求不好求,我们给他求个导:
再积分回去:
求导与积分:
Polynomial derivative() {
Polynomial b(len - 1);
for (int i = 1; i <= len; i++) b[i - 1] = 1ll * a[i] * i % P;
return b;
}
Polynomial integral() {
Polynomial b(len + 1);
for (int i = 0; i <= len; i++) b[i + 1] = 1ll * a[i] * qpow(i + 1, P - 2) % P;
return b;
}
对数:
Polynomial ln(int n) {
return (derivative() * inv(n)).integral().set(n - 1);
}
多项式指数函数(多项式 )
需要用一个东西:
牛顿迭代:
什么是牛顿迭代?
牛顿迭代是用来求零点的方法。首先有一个近似值
假设函数是
令
将其运用到多项式上,就是:
这样每一次迭代精度会翻倍(证明见 OI-Wiki),于是我们可以使用牛顿迭代来进行一些多项式操作。
例如此题:
我们两边取对数:
于是我们设
于是继续倍增就可以了!
Polynomial exp(int n) {
Polynomial b;
b[0] = 1;
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this, e = b.ln(d);
a.set(d - 1);
b = b * (e * (P - 1) + a + 1);
b.set(d - 1);
}
b.set(n - 1);
return b;
}
多项式除法
给定
我们设
我们将
那么有:
而
那么
void reverse() { std::reverse(a.begin(), a.end()); }
pair<Polynomial, Polynomial> operator/(Polynomial b) {
int n = len, m = b.len;
Polynomial ra = *this, rb = b, rc, d;
ra.reverse(), rb.reverse();
rc = ra * rb.inv(n - m + 1);
rc.set(n - m);
rc.reverse();
d = *this - b * rc;
d.set(m - 1);
return make_pair(rc, d);
}
附上我目前的模板:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1200005, P = 998244353, G = 3, GI = 332748118;
const int INV2 = (P + 1) / 2;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = 1ll * ans * a % P;
a = 1ll * a * a % P;
b >>= 1;
}
return ans;
}
int r[MAXN];
struct Polynomial {
vector<int> a;
int len;
int& operator[](int b) { return a[b]; }
Polynomial(int len = 0) : len(len) { a.resize(len + 1); }
Polynomial(initializer_list<int> l) : len(l.size() - 1), a(l) {}
Polynomial& set(int b) { len = b, a.resize(b + 1); return *this; }
static Polynomial resize(Polynomial a, int s) { Polynomial b = a; return b.set(s); }
void reverse() { std::reverse(a.begin(), a.end()); }
static void calcRev(int limit) {
for (int i = 1; i < limit; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) * limit >> 1);
}
void ntt(int limit, bool rev) {
set(limit);
for (int i = 0; i < limit; i++)
if (i < r[i]) swap(a[i], a[r[i]]);
for (int mid = 1; mid < limit; mid <<= 1) {
int step = qpow(rev ? GI : G, (P - 1) / (mid << 1));
for (int l = 0; l < limit; l += (mid << 1)) {
int w = 1;
for (int j = 0; j < mid; j++, w = 1ll * w * step % P) {
int x = a[l + j], y = 1ll * w * a[l + j + mid] % P;
a[l + j] = (x + y) % P, a[l + j + mid] = (1ll * P + x - y) % P;
}
}
}
int invn = qpow(limit, P - 2);
if (rev) {
for (int i = 0; i < limit; i++)
a[i] = 1ll * a[i] * invn % P;
}
}
void print() { for (int i : a) printf("%d ", i); printf("\n"); }
Polynomial operator*(Polynomial b) {
Polynomial a = *this, c;
int n = a.len + b.len;
int limit = 1;
while (limit <= n) limit <<= 1;
c.set(limit);
calcRev(limit);
a.ntt(limit, 0), b.ntt(limit, 0);
for (int i = 0; i < limit; i++) c[i] = 1ll * a[i] * b[i] % P;
c.ntt(limit, 1);
c.set(n);
return c;
}
Polynomial operator*(int b) {
Polynomial c = *this;
for (int& i : c.a) i = 1ll * i * b % P;
return c;
}
Polynomial operator+(int b) {
Polynomial c = *this;
c[0] = (1ll * c[0] + b + P) % P;
return c;
}
Polynomial operator-(int b) {
Polynomial c = *this;
c[0] = (1ll * c[0] - b + P) % P;
return c;
}
Polynomial operator+(Polynomial b) {
Polynomial c = *this;
c.set(max(c.len, b.len));
for (int i = 0; i <= b.len; i++) c[i] = (c[i] + b[i]) % P;
return c;
}
Polynomial operator-(Polynomial b) {
Polynomial c = *this;
c.set(max(c.len, b.len));
for (int i = 0; i <= b.len; i++) c[i] = (c[i] - b[i] + P) % P;
return c;
}
Polynomial shift(int x) {
Polynomial b(len - x);
for (int i = max(x, 0); i <= len; i++)
b[i - x] = a[i];
return b;
}
Polynomial inv(int n) {
Polynomial b;
b[0] = qpow(a[0], P - 2);
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this;
a.set(d - 1);
int limit = d << 1;
calcRev(limit);
a.ntt(limit, 0), b.ntt(limit, 0);
for (int i = 0; i < limit; i++) b[i] = (2ll - 1ll * a[i] * b[i] % P + P) % P * b[i] % P;
b.ntt(limit, 1);
b.set(d - 1);
}
b.set(n - 1);
return b;
}
Polynomial sqrt(int n) {
static int INV2 = qpow(2, P - 2);
Polynomial b;
b[0] = 1;
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this, c = b.inv(d);
a.set(d - 1);
int limit = d << 1;
calcRev(limit);
a.ntt(limit, 0), c.ntt(limit, 0);
for (int i = 0; i < limit; i++) a[i] = 1ll * a[i] * c[i] % P;
a.ntt(limit, 1);
b.set(d - 1);
for (int i = 0; i < d; i++) b[i] = 1ll * (a[i] + b[i]) * INV2 % P;
}
b.set(n - 1);
return b;
}
Polynomial derivative() {
Polynomial b(len - 1);
for (int i = 1; i <= len; i++) b[i - 1] = 1ll * a[i] * i % P;
return b;
}
Polynomial integral() {
Polynomial b(len + 1);
for (int i = 0; i <= len; i++) b[i + 1] = 1ll * a[i] * qpow(i + 1, P - 2) % P;
return b;
}
Polynomial ln(int n) {
return (derivative() * inv(n)).set(n - 1).integral().set(n - 1);
}
Polynomial exp(int n) {
Polynomial b;
b[0] = 1;
for (int d = 1; d < (n << 1); d <<= 1) {
Polynomial a = *this - b.ln(d) + 1;
a.set(d - 1);
int limit = d << 1;
calcRev(limit);
a.ntt(limit, 0), b.ntt(limit, 0);
for (int i = 0; i < limit; i++) {
b[i] = 1ll * a[i] * b[i] % P;
}
b.ntt(limit, 1);
b.set(d - 1);
}
b.set(n - 1);
return b;
}
Polynomial pow(int b, int n) {
return (ln(n) * b).exp(n);
}
pair<Polynomial, Polynomial> operator/(Polynomial b) {
int n = len, m = b.len;
Polynomial ra = *this, rb = b, rc, d;
ra.reverse(), rb.reverse();
rc = ra * rb.inv(n - m + 1);
rc.set(n - m);
rc.reverse();
d = *this - b * rc;
d.set(m - 1);
return make_pair(rc, d);
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探