【学习笔记】多项式

开个大坑。

FFT 前的前置知识

复数

应该想学FFT的人都会复数吧

复数,即形如 a+bi 的数,其中 a,b 为实数,i 为虚数单位(即 1)。

在复平面上,一个复数代表着一个点。所谓复平面,即以实数为 x 轴,以虚数为 y 轴所构成的平面直角坐标系。

对于复数加减,直接将实部与虚部相加即可。对于复数乘法,(a+bi)(c+di)=(acbd)+(ad+bc)i

在复平面上的复数乘法有这样一个口诀:模相乘,幅角相加

模即在复平面上点到原点的距离,幅角即相对于 x 轴正半轴所逆时针旋转的角度。

单位根

我们在复平面上做一个半径为 1 的圆,并将他五等分。

这样,我们发现,这些点的模长都为 1,并且幅角都是若干倍的关系。

具体的,我们记 n 等分的单位圆上,相对于 x 轴正半轴逆时针旋转的第一个点,记做 ωn,也就是单位根

根据模相乘,幅角相加,那么第二个点就是 ωn2,最后一个点就是 ωnn1。并且,ωnn=1

单位根的一些性质:

  1. ωnk=ω2n2k
    ωnk=ωn2k2
    这两个应该比较容易理解。四等分的第二份,相当于八等分的第四份,也相当于两等分的第一份。
  2. ω2nn+k=ω2nk
    因为 ω2nn=1,所以上式正确。

如何计算单位根?由于是将单位圆进行 n 等分,所以直接使用三角函数计算即可。

即:

ωn=cos(2πn)+sin(2πn)i

C++ 中提供了复数类 complex,可以去网上搜索相关用法。不过也可以直接自己写一个,并不难写。

点值表示法

一般,我们所见的多项式的表示方式都是系数表示法,即直接给出 xi 的系数 ai。而点值表示法,就是给出 n+1 个点,来表示一个多项式。因为给出 n+1 个点后,就可以唯一确定一个 n 次的多项式,参考拉格朗日插值法。

对于多项式乘法来说,系数表示法不好计算,但是点值表示法就很容易计算,直接将相对应的数字相乘即可。所以,我们考虑将系数表示法转换为点值表示法。

离散傅里叶变换

离散傅里叶变换,即 DFT,是一种将系数表示法快速转换为点值表示法的变换。

对于一个 n 次的多项式 f(x)

f(x)=a0+a1x+a2x2++anxn

我们将它按照奇偶性分开:

f(x)=(a0+a2x2+a4x4+)+(a1x+a3x3+a5x5+)

我们分别再设两个多项式 G(x)=a0+a2x+a4x2+H(x)=a1+a3x+a5x2+,那么上式就可以表示为:

f(x)=G(x2)+xH(x2)

我们将 x=ωnk 代入:

f(ωnk)=G((ωnk)2)+ωnkH((ωnk)2)=G(ωn2k)+ωnkH(ωn2k)=G(ωn2k)+ωnkH(ωn2k)

同理:

f(ωnk+n2)=G((ωnk+n2)2)+ωnk+n2H((ωnk+n2)2)=G((ωnk)2)ωnkH((ωnk)2)=G(ωn2k)ωnkH(ωn2k)=G(ωn2k)ωnkH(ωn2k)

那么我们发现,只要求出 G(ωn2k)H(ωn2k) 后,我们就可以同时求出 f(ωnk+n2)f(ωnk)。因此,我们可以直接分治对它进行递归计算。

因为我们进行分治,为了保证左右区间长度相等(不然不好合并),我们可以提前将多项式补到 2m1 次,再进行 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];
}

时间复杂度是 O(nlogn) 的。

但是这样常数会很大。中间我们不断的对系数进行了交换,我们可以考虑优化这一过程,来观察最后系数位置的变化:

我们将位置化为二进制,来模拟一下这个交换过程:

  1. {000,001,010,011,100,101,110,111}
  2. {000,010,100,110}{001,011,101,111}
  3. {000,100}{010,110}{001,101}{011,111}
  4. {000}{100}{010}{110}{001}{101}{011}{111}

发现:一个数最后的位置就是一开始的位置的二进制左右翻转。

其实再仔细观察下会发现,第 i 次奇偶性分类其实就是将二进制位 i0 的划分到一块,将 1 划分到一块,原来是高位连续,现在变成了低位连续,于是就是将二进制左右翻转了。

于是,我们可以首先预处理出数组 r 代表二进制位翻转过后的数字。

直接放代码:

for (int i = 0; i < limit; i++)
    r[i] = (r[i >> 1] >> 1) | ((i & 1) * limit >> 1)

自己手模一下就可以,不难理解。

提前交换完毕后,我们发现:合并时,我们将 kk+n2 合并起来,还是合并到 kk+n2 这两个位置。所以,我们可以得到 蝴蝶操作

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 操作,其实就是将系数向量乘上了一个矩阵,也就是:

[y0y1y2y3yn1]=[111111ωn1ωn2ωn3ωnn11ωn2ωn4ωn6ωn2(n1)1ωn3ωn6ωn9ωn3(n1)1ωnn1ωn2(n1)ωn3(n1)ωn(n1)2][a0a1a2a3an1]

我们记左面的点值向量为 E,中间的矩阵为 D,右面的系数矩阵为 V,那么 D×V=E

我们求 V,实际上就是要求 D 的逆矩阵。

考虑构造 Aij=Dij1,来计算 A×D

(A×D)ij=k=0n1AikDkj=k=0n1ωnikωnkj=k=0n1ωnk(ji)={n(i=j)k=0n1(ωnji)k=(ωnji)n1ωnji1=(ωnn)ji1ωnji1=0(ij)

发现这个矩阵就是单位矩阵的 n 倍。即 A×Dn=I,那么 D1=1nA

这样,我们将点值向量左乘一个 1nA,就得到了系数表示法。

具体实现上,由于 Aij=Dij1=ωnij=ωnnij,它与 ωnij 在复平面上是关于 x 轴对称的,所以直接对虚部取一个相反数即可。那么,我们将 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. ωnk 对于 k[0,n1] 互不相等。
    若模数 p 为一个质数,那么对于原根 ggk,k[0,p1] 互不相等,那么 (gp1n)k,k[0,n1] 也互不相等。
  2. ωnk=ωn2k2

    (gp1n/2)k2=gp1n/2×k2=(gp1n)k

  3. ω2nk+n=ω2nk
    由费马小定理可得,gp11,那么 gp11(gp121)(gp12+1)0,即 gp12=±1
    又因为 g01,根据性质 1,gp12=1,那么 (gp12n)ngp121,即 (gp12n)n+k(gp12n)k

于是,我们可以得出以下结论:原根就是模意义下的单位根!

不过我们发现,n2m 的形式,那么 p1n 要想是整数,那么 p 必须是 r×2t+1 的形式。

实际上,有:

998244353=119×223+1,g=3

另外一些常用的 NTT 模数:

469762049=7×226+1,g=3

1004535809=479×221+1,g=3

考场上如何检验一个数是不是 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;
    }
};

应用

多项式乘法通常可以用于快速计算卷积

观察多项式乘法的式子:

F×G=i=0nj=0mfigjxi+j

我们可以转换一下:

F×G=i=0n+mj=0ifjgijxi

我们发现,xi 的系数为 j=0nfjgij,也就是卷积的形式。

于是,我们可以将很多形如卷积形式的式子使用 FFT/NTT 进行优化。

例题:

P3338 [ZJOI2014]力

给出 n 个数 q1,q2,qn​,定义

Fj = i=1j1qi×qj(ij)2  i=j+1nqi×qj(ij)2

Ei = Fiqi

1in,求 Ei​ 的值。

首先化式子:

Ei=Fiqi=j=1i1qj(ji)2j=i+1nqj(ji)2=j=1i1qj(ij)2j=1niqi+jj2(jj+i)

左面显然是卷积形式,设 F(x)=qx,G(x)=1x2,那么左面的式子就是 (FG)(i1)

那么看右面:右面是差一定的形式,而不是和一定。我们可以将 F(x) 的下标翻转过来,即设 F(x)=F(nx),那么右面就变为了

j=1niF(nij)G(j)

发现这就也是卷积的形式了,直接计算即可。

P4199 万径人踪灭

上篇文章

全 家 桶 !

实际上我就学了几个,为了防止过段时间全忘了,就先记一下。

多项式乘法逆

给定多项式 F(x),求一个多项式 G(x) 使得 F(x)G(x)1(modxn)

考虑倍增。假如现在已经求出了 F(x)H(x)1(modxn2)

F(x)H(x)1(modxn2)F(x)G(x)1(modxn2)F(x)(G(x)H(x))0(modxn2)G(x)H(x)0(modxn2)(G(x)H(x))20(modxn)G(x)22G(x)H(x)+H(x)20(modxn)F(x)G(x)22F(x)G(x)H(x)+F(x)H(x)20(modxn)G(x)2H(x)+F(x)H(x)20(modxn)G(x)2H(x)F(x)H(x)2(modxn)

n=1 时,直接求逆元即可。

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;
}

多项式开根

给定多项式 F(x),求一个多项式 G(x) 使得 G(x)2F(x)(modxn)

同样考虑倍增:假设已经求得 H(x)2F(x)(modxn2),那么:

H(x)2F(x)(modxn2)G(x)2F(x)(modxn2)H(x)2G(x)2(modxn2)H(x)G(x)(modxn2)H(x)G(x)0(modxn2)(H(x)G(x))20(modxn)H(x)22H(x)G(x)+G(x)20(modxn)H(x)22H(x)G(x)+F(x)0(modxn)G(x)H(x)2+F(x)2H(x)(modxn)

求逆元,然后倍增算就可以了。

对于 n=1 的情况,在强化版中,要计算二次剩余 x2a0我不会,咕了。

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;
}

多项式对数函数(多项式 ln

给定多项式 F(x),求一个多项式 G(x) 使得 G(x)lnF(x)(modxn)

直接求不好求,我们给他求个导:

G(x)F(x)F(x)(modxn)

再积分回去:

G(x)F(x)F(x)(modxn)dx

求导与积分:

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);
}

多项式指数函数(多项式 exp

需要用一个东西:

牛顿迭代:

什么是牛顿迭代?

牛顿迭代是用来求零点的方法。首先有一个近似值 x0,做 x0 处的切线,交 x 轴于一点,再以这点作为 x0 继续迭代。

假设函数是 f(x),我们写出切线方程:

y=f(x0)(xx0)+f(x0)

y=0,那么

x=x0f(x0)f(x0)

将其运用到多项式上,就是:

G(x)=G0(x)F(G0(x))F(G0(x))

这样每一次迭代精度会翻倍(证明见 OI-Wiki),于是我们可以使用牛顿迭代来进行一些多项式操作。

例如此题:

G(x)eF(x)(modxn)

我们两边取对数:

lnG(x)F(x)(modxn)lnG(x)F(x)0(modxn)

于是我们设 H(G(x))=lnG(x)F(x),由于 F(x) 在这里是常数,那么求导后得到 H(G(x))=1G(x),那么牛顿迭代:

G(x)=G0(x)lnG0(x)F(x)1G0(x)=G0(x)(1lnG0(x)+F(x))

于是继续倍增就可以了!

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;
}

多项式除法

给定 n 次多项式 F(x)m 次多项式 G(x),求一个 nm 次多项式 Q(x) 和一个 m1 次多项式 R(x),满足 F(x)=Q(x)G(x)+R(x)

我们设 FR(x) 为将 F(x) 系数翻转之后的式子,也就是:

FR(x)=i=0nanixi

我们将 1x 带入:

F(1x)=i=0nai(1x)i=i=0naixi=i=0nanixin=1xni=0nanixiF(1x)xn=FR(x)

那么有:

F(1x)=G(1x)Q(1x)+R(1x)F(1x)xn=G(1x)Q(1x)xn+R(1x)xnF(1x)xn=G(1x)xmQ(1x)xnm+R(1x)xm1xnm+1FR(x)=GR(x)QR(x)+RR(x)xnm+1

FR(x)GR(x)QR(x)(modxnm+1)QR(x)FR(x)GR(x)(modxnm+1)

Q(x) 正好是 nm 次的,所以直接计算即可得到 Q(x)

那么 R(x)=F(x)G(x)Q(x),即可计算出 R(x)

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);
    }
};
posted @   APJifengc  阅读(232)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示