多项式 I:拉格朗日插值与快速傅里叶变换

1. 复数和单位根

前置知识:弧度制,三角函数。

1.1 复数的引入

跳出实数域 R,我们定义 i2=1,即 i=1,并在此基础上定义 复数 a+bi,其中将 b0 的称为 虚数。复数域记为 C

像这种从 a 变成 a+bx扩域 操作并不少见,例如初中学习 “平方根” 时,经常用 a+bx(x>0) 表示一个数。这类数的加减乘都是容易的,除法即考虑平方差公式 (c+dx)(cdx)=c2d2x,因此 a+bxc+dx=(acbdx)+(bcad)xc2d2x

x 替换为 1,复数四则运算可由实数四则运算结合 i 的定义直接推广得到。

  • 加法:(a+bi)+(c+di)=(a+c)+(b+d)i
  • 减法:(a+bi)(c+di)=(ac)+(bd)i
  • 乘法:(a+bi)(c+di)=(acbd)+(ad+bc)i
  • 除法:a+bic+di=(a+bi)(cdi)(c+di)(cdi)=ac+bdc2+d2+bcadc2+d2i

1.2 复平面与棣莫弗定理

描述一个复数 a+bi 需要两个值 ab,其中 a 表示 实部b 表示 虚部。这启发我们将其放在平面直角坐标系上描述,称为 复平面。其在复平面上的坐标为 (a,b),实部 a 为横坐标,虚部 b 为纵坐标。

一个复数唯一对应一个平面向量,因为它们都可以用有序实数对描述。将向量起点平移至原点,则其终点指向与其对应的复数。考虑平面向量的一些特征,如其长度与所在直线斜率。将这些概念应用在复数上,我们得到如下定义:

  • 定义复数 z=a+bi|z|=a2+b2
  • 定义复数 z=a+bi辐角Argz=θ,其中 tanθ=ba。满足 π<θπθ 称为 辐角主值,记作 argz,即 argz=arctanba
  • 辐角确定了 z 所在直线,模确定了 z 在直线上的长度。对比实部和虚部,模和辐角主值以另一种有序实数对的形式描述复数。

根据 z=a+bi 的模 r 和辐角 θ,可知 z 的实部 a=rcosθ,虚部 b=rsinθ,据此定义复数的 三角形式 z=r(cosθ+isinθ)

利用 cossin 的和角公式可得 z1z2=r1r2(cos(θ1+θ2)+isin(θ1+θ2))。该等式称为 棣莫弗定理,它说明 复数相乘,模长相乘,辐角相加

  • 根据棣莫弗定理,我们得到对虚数单位 i 的一种直观理解:将一个复数 z 乘以 i 相当于将其 逆时针旋转 π2 弧度。实际上,考虑虚数单位 i 本身在复平面上的位置,发现就是 1 逆时针旋转 π2 度。一般地,有旋转的地方就有 i 存在,i 即旋转。推荐观看:虚数的来源 - Veritasium

1.3 单位根的定义

r=1 时,z=cosθ+isinθ 在单位圆上。此时根据棣莫弗公式有 zn=cos(nθ)+sin(nθ),它在 复数旋转复数乘法 之间构建了桥梁:zn 次幂相当于从 (1,0) 开始,以 z 的角度在单位圆上旋转 n 次得到的结果,称为将 z 旋转 n 次。

考虑将单位圆 n 等分(如下图),取任意 n 等分点 Pk(0k<n),将其旋转 n 次均得到 1,因为跨过的 1n 单位圆弧数为 n 的倍数。这说明 Pkn=1

探究 Pk 的表达式:因为它相当于从 1 开始在单位圆上旋转 2kπn 弧度,因此 Pk=cos(2kπn)+sin(2kπn)。我们称所有 Pkn单位根,将 P1 记作 ωn,则 Pk=P1k=ωnk

根据 Pkn=1 可知任意 n 次单位根 ωnk 均为 xn1=0 的解。除特殊说明外,一般 n 次单位根直接代指 ωn,即从 1 开始逆时针方向的第一个单位根。

可以观看 3b1b 的视频 从 6:00 到 7:30 的部分加深对上述内容的理解。

  • 单位根的循环性:由 ωnn=1 单位根的指数可对 n 取模。换言之,考虑从 1 开始不断乘以 ωn,我们将得到 1,ωn,ωn2,,ωnn1,ωnn=1,ωnn+1=ωn,,循环节为 n。想象从 1 开始不断旋转 2πn 弧度,旋转 n 次后我们将落入循环。换言之,ωnk=ωnk+tn(tZ)
  • ωnk=ωcnck(c>0)。对单位根有可视化认知后容易理解这一点。
  • n 为偶数时,将 ωnk 取反相当于将其逆时针(或顺时针)转半圈,所以 ωnk=ωnk±n2(2n)
  • 单位根的对称性:因为 n 次单位根将单位圆 n 等分,均匀分布在圆周,所以它们的重心就在原点,即 i=0n1ωni=0
  • gcd(k,n)=1,则 ωnk 称为 本原单位根。所有本原单位根的 0n1 次幂互不相同。

1.4 单位根与原根

前置知识:原根,详见 初等数论学习笔记 I:同余相关

单位根和原根有极大的相似性,可以说原根就是模 P 下的整数域的单位根。

n=φ(P)P 存在原根 g,则 g0,g1,,gn1,gn=g0,gn+1=g1 这样的循环和 n 次单位根的循环一模一样。这使得在模 P 意义下涉及 n 次单位根运算时,可直接用原根 g 代替。进一步地,对于 dngnd 可直接代替模 P 意义下的 d 次单位根。

  • 单位根和原根都是对应域上一个大小为 n=φ(P)循环群生成元。它们均满足 n 次幂是对应域的单位元,且它们的 0n1 次幂互不相同。换言之,它们 同构

快速傅里叶变换 FFT 和快速数论变换 NTT 的联系恰在于此。

1.5 欧拉公式

前置知识:自然对数 ln 的底数 e 及其相关性质。

这部分为接下来可能用到的符号做一些补充。

欧拉公式指出

eix=cosx+isinx

即单位圆上从 (1,0) 开始旋转 x 弧度得到的复数,也即大小为 x 的角的终边与单位圆的交点。

欧拉公式的严谨证明超出了讨论范围,相关资料可以参考百度百科。我们给出一个对欧拉公式的感性理解方式,以加深读者对欧拉公式的直观印象与理解,来自 在 3.14 分钟内理解 eiπ - 3Blue1Brown。这个视频相当有启发性。

根据 (et)=et,考虑 et 随着 t 增大在数轴上的取值。et 以每秒钟 t 均匀增加 1 的速度向数轴正方向移动,则 et 的速度就是它本身的位置。它的起始点为 e0=1

根据 (ekt)=ket,类似可知 ekt 的速度等于 k 倍它本身的位置。

k=i 时,速度相当于将本身的位置逆时针旋转 π2 弧度,与位置垂直。这样,根据基础的高中物理知识,我们知道 eit 随着 t 增大,在单位圆上做匀速圆周运动,且每秒移动 1 弧度。

这样,eit 就等于模长为 1,辐角为 t 的复数,即 cost+isint。这使得我们可以用 reiθ 描述模长为 r,辐角为 θ 的复数,记号变得更简洁。

将该表示法应用至单位根,可知 ωn=e2πin

读者应当在 reiθr(cosθ+isinθ) 及其在复平面上的位置建立直观联系,有助于理解接下来的内容。

带入 t=π,得到著名等式

eiπ=1

带入 t=2π=τ,得

eiτ=1

这说明对于任意 kZ(e2πi)k+tn 相等恰对应 ωnkn+t 相等。

2. 多项式

2.1 基本概念

形如 i=0naixi有限和式 称为 多项式,记作 f(x)=i=0naixi。其中,ai 称为 i 次项的 系数,也称 xi 前的系数,记作 [xi]f(x)。超出最高次数 n 的系数 ai(i>n) 视作 0

当项数无限时,和式形如 i=0aixi,称为 形式幂级数,它暂时超出了我们的讨论范围。

  • 多项式 系数非零 的最高次项的次数称为该多项式的 ,也称次数,记作 degf。如 f(x)=i=0naixi 其中 an0,则 fn 次多项式,记作 degf=n
  • 使得 f(x)=0 的所有 x 称为多项式的
  • ai 均为实数,则称 f 为实系数多项式。若 ai 可以均为复数,则称 f 为复系数多项式。
  • 代数基本定理:任何非零一元 n 次复系数多项式恰有 n 个复数根。这些复数根可能重合。证明略。

2.2 系数表示法和点值表示法

f(x)=i=0naixi 给出了所有 i 次项前的系数,这种描述多项式的方法称为 系数表示法

x=xi 带入,得到 yi=f(xi),称 (xi,yi)fxi 处的点值。用若干点值 (xi,yi) 描述多项式的方法称为 点值表示法

考虑这两种表示法之间的联系。我们尝试探究在给定 n 个点值 (x0,y0),(x1,y1),,(xn1,yn1) 其中 xi 互不相同时,所唯一确定的多项式的最高次数。

一个自然的猜测是 n1,因为 n1 次多项式需要 n 个系数描述,具有 n 单位信息,而 n 个点值同样具有 n 单位信息。

证明即考虑直接带入,得到 n 元线性方程组:对于任意 0j<ni=0n1aixji=yj。该线性方程组的系数矩阵为

[1x0x01x0n11x1x11x1n11x2x21x2n11xn1xn12xn1n1]

xi 互不相同,所以该范德蒙德矩阵的行列式非零,方程组有唯一解。类似的,从线性方程组的角度出发,容易证明 n 个点值不可能唯一确定 n 次或更高次的多项式。

综上,我们得到重要结论:n 个点值唯一确定的多项式最高次数为 n1

  • 从系数表示法转为点值表示法称为 求值(Evaluation)。
  • 从点值表示法转为系数表示法称为 插值(Interpolation)。

2.3 多项式运算

2.3.1 基本运算

f(x)=i=0naixig(x)=i=0mbixi

  • h=f+g,则 h(x)=(i=0naixi)+(j=0mbjxj)=i=0max(n,m)(ai+bi)xi,可知两多项式相加,对应系数相加,deg(f+g)=max(degf,degg)
  • h=fg,则 h(x)=(i=0naixi)(j=0mbjxj)=i=0n+m(j=0iajbij)xi,可知两多项式相乘,每两个系数相乘贡献至次数之和的系数,deg(fg)=degf+degg

因此,在系数表示法下,计算两个多项式相加的复杂度为 O(n+m),计算两个多项式相乘的复杂度为 O(nm)

  • 根据 (f+g)(x)=f(x)+g(x),可知两个多项式相加时,对应点值相加。

  • 根据 (fg)(x)=f(x)g(x),可知两个多项式相乘时,对应点值相乘。

因此,在点值表示法下,计算两个多项式相加需要 max(n,m)+1 个点值,计算两个多项式相乘需要 n+m+1 个点值,复杂度均为 O(n+m)

  • fgfg 表示多项式相乘,即进行加法卷积;用 fg 表示多项式 点乘,即 对应系数相乘

在系数表示法下计算两个多项式相乘,我们首先将其转化为点值表示法,相乘后再转回系数表示法。时间复杂度 O((n+m)log(n+m))。相关算法将在第四章介绍。

3. 拉格朗日插值

在 2.2 小节我们得到结论:n 个点值唯一确定的多项式最高次数为 n1。那么,如何在点值表示法和系数表示法之间快速转换呢?

从系数表示法转为点值表示法,最常用的方法是直接带入,时间复杂度 O(n2)O(nlog2n) 的多项式多点求值则需要高级多项式技巧,此处不作介绍。

从点值表示法转为系数表示法,最直接的方法是高斯消元,时间复杂度 O(n3)。接下来我们将介绍常用的拉格朗日插值法。

3.1 算法详解

拉格朗日插值的核心思想在于利用点值的可加性,每次只考虑一个点值,其它点值均视为 0,由此构造 n 个多项式 fi(x),使得它们在 xi 处取值为 yixj(ji) 处取值为 0,则 f=i=0n1fi中国剩余定理 使用了类似的思想。

为了让 fi(xj)=0 (ij)fi 应包含 xxj 作为因式,因此设 fi(x)=ij(xxj)。但是此时 fi(xi) 不一定等于 yi,我们发现可以调整 fi 的系数,给 fi 乘上 yifi(xi)。综上,我们得到 fi 的表达式

fi(x)=yijixxjxixj

fi 求和得 f,我们得到拉格朗日插值公式

f(x)=i=0n1yijixxjxixj

为得到 f 的各项系数,只需 O(n2) 求出 F(x)=i=0n1(xxi),对每个 i 暴力 O(n)F 除掉一次式 xxi 算出 F(x)xxi 的各项系数,再乘以 yijixixj 得到 fi,则 f=i=0n1fi。时间复杂度 O(n2)

通常情况下,题目要求我们求出 F(x) 在给定某个 x 处的取值,此时我们不把 x 看做函数的元,而是直接将 x 带入上式。时间复杂度仍为 O(n2)

多项式快速插值在 O(nlog2n) 的时间内将点值表示法转化为系数表示法,这超出了我们的讨论范围。

3.2 连续取值插值

当给定点值横坐标为连续整数时,我们有快速单点插值的方法。

0,1,,n1xi=i 为例:

f(x)=i=0n1yijixjij

分子是 (xi) 挖掉一个项,经典维护前缀后缀积。设 pi=j=0i1xjsi=j=i+1n1xj

分母对于 i>jj=0i1(ij)=i!。对于 i<jj=i+1n1(ij)=(1)(2)(in+1),将所有负号提出来,得 (1)ni+1(in+1)!

因此

f(x)=i=0n1yipisi(1)ni+1i!(in+1)!

预处理阶乘逆元,时间复杂度 O(n)

3.3 应用

  • 计算范德蒙德方阵的逆矩阵,详见 4.4.1 小节。

3.4 例题

P4781 【模板】拉格朗日插值

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e3 + 5;
constexpr int mod = 998244353;
int ksm(int a, int b) {
  int s = 1;
  while(b) {
    if(b & 1) s = 1ll * s * a % mod;
    a = 1ll * a * a % mod, b >>= 1;
  }
  return s;
}
int n, k, ans, x[N], y[N];
int main() {
  cin >> n >> k;
  for(int i = 1; i <= n; i++) cin >> x[i] >> y[i];
  for(int i = 1; i <= n; i++) {
    int deno = 1, nume = 1;
    for(int j = 1; j <= n; j++) {
      if(i == j) continue;
      deno = 1ll * deno * (x[i] + mod - x[j]) % mod;
      nume = 1ll * nume * (k + mod - x[j]) % mod;
    }
    ans = (ans + 1ll * y[i] * nume % mod * ksm(deno, mod - 2)) % mod;
  }
  cout << ans << "\n";
  return 0;
}

4. 快速傅里叶变换

快速傅里叶变换(Fast Fourier Transform,FFT)是多项式算法的根基。想要真正理解它,必须先真正理解单位根,还需要对线性代数有基本了解。

推荐观看:

4.1 求值的本质

f(x)=i=0n1aixi,将 x0 带入,得 f(x0)=i=0n1aix0i。考虑将其写成向量内积(点积)的形式:

f(x0)=i=0n1aix0i=[x00x01x0n1]×[a0a1an1]

这样,如果有 x0,x1,,xm1 需要求值,整个过程就可以写成 m×n 维矩阵乘以 n 维列向量的形式:

[x00x01x0n1x10x11x1n1xm10xm11xm1n1]×[a0a1an1]=[f(x0)f(x1)f(xm1)]

左侧矩阵就是著名的 范德蒙德矩阵

m=n 时为范德蒙德方阵,xi 互不相同时其逆存在,帮助我们快速从点值表示法转回系数表示法。m>n 时任取 xi 互不相同的 n+1 行可以求逆。m<n 时无法还原系数。这体现出 n+1 个点值唯一确定最高次数不超过 n 的多项式。

朴素计算求值的复杂度为 O(nm),因为带入求值一次的复杂度为 O(n)。快速傅里叶变换即在离散傅里叶变换基础上通过选取合适的 xi,使得可以快速求值。

4.2 离散傅里叶变换

在介绍 FFT 之前,我们先给出离散傅里叶变换(Discrete Fourier Transform,DFT)的概念。

DFT 在工程中是将离散信号从时域转为频域的过程。碰巧的是,其表达式刚好可以用来对多项式进行多点求值,只不过这些点值是固定的 单位根 处的点值,但对于求值做多项式乘法已经足够了。

DFT 将一个长度为 N 的复数序列 x0,x1,,xN1 通过如下公式转化为另一个长为 N 的复数序列 X0,X1,,XN1

Xk=n=0N1xne2πiNkn

也即

Xk=n=0N1xnωNkn

设多项式 f(x)=n=0N1xnxi,易知

Xk=n=0N1xn(ωNk)n=f(ωNk)

根据上式,离散傅里叶变换可以看成多项式 f(x)=n=0N1xnxiN 个单位根处求值。

没看懂?没关系。接下来我们将从另一角度出发,得到一个差别微小但更容易理解的算法 —— FFT。

4.3 算法详解

首先我们得搞清楚,DFT 是一个变换,而 FFT 是用于实现 DFT 的算法。在 FFT 的推导过程中,其所实现的变换和 DFT 变换有细微的差别,因此笔者也用 FFT 表示该算法实现的变换。

按理说学习一个算法时需要搞清楚它的用途,但如果直接从 DFT 角度入手尝试简化流程,那么为了让动机看起来自然,我们还要先学习 DFT 的实际含义,这涉及到大量前置知识。

但是,从计算机科学界尤为重要且为各位 OIer 熟知的多项式乘法的优化出发,我们通过自然推理得到一个算法,而该算法恰好可以快速实现 DFT(有一些细小的差别,详见 4.3.5)。

我们明确接下来的目标:给定次数 n1 的多项式 f(x)=i=0n1aixi(an10),快速求出它的至少 n 个点值。

下文中,f(x) 也被视为关于 xn1 次函数。

4.3.1 简化情况

解决一个普遍性的问题,最重要的思想就是 从简化情况入手,分析问题的性质

函数的性质无非就那几个:奇偶性,单调性,周期性等。一般函数没有单调性和周期性,但总可以表示为一个偶函数和一个奇函数之和,这一定是突破点。

  • 对于偶函数 f(x),即所有奇数次项系数为 0,带入两个相反数 ww 时,它们的值相等。

  • 对于奇函数 f(x),即所有偶数次项系数为 0,带入两个相反数 ww 时,它们的值互为相反数。

因此,将任意多项式 f(x) 拆成偶函数 fe(x) 和奇函数 fo(x) 之和,则

{f(x)=fe(x)+fo(x)f(x)=fe(x)fo(x)

选择 n2 对两两互为相反数的值 (xi,xi) ,求出所有 xife(x)fo(x) 处的取值。

不妨设 n 为偶数,则 fen2 次多项式,fen1 次多项式,本质上依然相当于求出 n1 次多项式的 n 个点值,对时间复杂度没有优化。

但是 fefo 的项数减半,尝试利用该性质。

因为 fe 只有偶数次项 a0x0+a2x2+,故考虑换元 u=x2,则 fe(u)=a0u0+a2u1+。换言之,我们设 fe(x)=a0x0+a2x1+,则 fe(x)=fe(x2)

同理,从 fo 中提出一个 x,设 fo(x)=a1x0+a3x1+,则 fo(x)=xfo(x2)

因此,

{f(x)=fe(x2)+xfo(x2)f(x)=fe(x2)xfo(x2)

这样才是真正意义上的规模减半。若问题可递归,则时间复杂度为 T(n)=2T(n2)+O(n),解得 T(n)=O(nlogn)

4.3.2 引入单位根

问题来了,如何保证递归得到的问题也满足两两互为相反数呢?

考虑一开始的 (xi,xi),这说明存在 i 使得 xi2=xi2,它们互不相同但它们的 4 次方相等。

进一步推演,因为问题会递归 w=log2n 层,所以我们需要找到 k=2w 个互不相等的 x,但它们的 k 次幂相等。这个相等的 k 次幂可以任意选择,方便起见设为 1,则 xk=1x 为所有 k 次单位根。

逆推得到结果后,我们再顺着检查一遍:初始令 x 为所有 k 次单位根,每层递归会将这些数平方,得到所有 k2,k4 次单位根。因为 k2,k4, 都是偶数,所以它们可以两两正负配对,直到递归 w 层的平凡情况:k2w=1 次单位根即 x=1

由此可得通常使用(补齐到 2 的幂)的快速傅里叶变换的范德蒙德方阵形式:

[(ωn0)0(ωn0)1(ωn0)n1(ωn1)0(ωn1)1(ωn1)n1(ωnn1)0(ωnn1)1(ωnn1)n1]×[a0a1an1]=[f(ωn0)f(ωn1)f(ωnn1)]

4.3.3 递归公式

得到大致框架后,我们具体地描述整个算法流程:

首先将 n 补齐到不小于 n 的最小的 2 的幂,即 2log2n

设当前需要求值的多项式 f 长度为 L(L=2w,wN),若 L=1 则直接返回 a0。否则我们需求出 f(x) 在所有 L 次单位根 ωLk(0k<L) 处的取值。

f(x) 写成 fe(x2)+xfo(x2),则对于 0k<L2,有

f(ωLk)=fe(ωL2k)+ωLkfo(ωL2k)f(ωLk+L2)=fe(ωL2k+L)+ωLk+L2fo(ωL2k+L)

根据单位根的性质(在单位根部分有介绍):

  • ωnk=ω2n2k
  • ωnk=ωnk+tn(tZ)
  • ωnk=ωnk+n2(2n)

f(ωLk)=fe(ωL2k)+ωLkfo(ωL2k)f(ωLk+L2)=fe(ωL2k)ωLkfo(ωL2k)

这样,我们只需求出 L2 次多项式 fefo 在所有 L2 次单位根处的取值。

4.3.4 蝴蝶变换

递归处理比较慢,我们希望像位运算卷积一样通过递推实现整个过程。

因为在边界条件下,每个位置的取值与其对应的系数相关,所以递归转递推的关键在于考察系数的变化。

考虑 n=8 的情况,初始为 (a0,a1,a2,a3,a4,a5,a6,a7)

进行第一层递归时,将 fe 的系数写在左半部分,fo 的系数写在右半部分,得 (a0,a2,a4,a6),(a1,a3,a5,a7)

进行第二层递归时,类似地将每个子问题的 fefo 的系数分别写在左右两侧,得 (a0,a4),(a2,a6),(a1,a5),(a3,a7)

进行第三层递归时,共有 4 个大小为 2 的子问题,且进行上述操作后系数的位置不发生改变。

我们看到,如果一个系数在规模为 2n 的问题中的位置为 p,若 p 是偶数,则递归至左半部分,若 p 是奇数,则递归至右半部分,且在规模为 n 的问题中的位置为 p2

进一步地,一个系数在第 i 次递归时的方向决定了它最终下标在二进制下第 wi(n=2w) 位的取值。若向左递归则为 0,向右递归则为 1。而它递归的方向又受到 p2i1 的奇偶性的影响,即 p 在二进制下第 i 位的取值,若为 0 则向左递归,为 1 则向右递归。

这就说明,p 在二进制下第 i 位的取值,等于它对应的系数的最终下标在二进制下第 wi 位的取值。

因此我们断言,系数 apw 次递归后的下标等于 p 二进制翻转后的值,设为 rp。这里翻转指翻转第 0w1 位的值。

rp 可递推求得:r0=0。对于 ri(i>0),先右移一位得到 i=i2,则 ri 的低 w1 位(第 0w2 位)的值可由 ri 右移一位得到。第 w1 位的值只需检查 i 的奇偶性。因此,ri=ri2+n2(imod2),其中 i=i2

因此,在算法一开始先对系数数组 a 执行 蝴蝶变换,即同时令 aiari,然后类似 FWT,枚举问题规模 2d(1d<n),枚举每个子问题 2di(02di<n),再枚举当前子问题的所有对应位置 (x=2di+k,y=2di+k+d)(0k<d) 执行变换,即设 xy 对应位置的当前值为 fxfy,则 fx=fx+ω2dkfyfy=fxω2dkfy

进一步地,根据 r 的定义,我们有 rri=i。因此,执行蝴蝶变换只需对所有 i<ri 执行 swap(ai,ari)

这就是 FFT,整个过程称为 对多项式 f 做长度为 n 的快速傅里叶变换,时间复杂度 O(nlogn)。代码在 4.5 小节。

4.3.5 DFT 和 FFT

对比 DFT 和 FFT 矩阵:

FD=[(ωN0)0(ωN0)1(ωN0)N1(ωN1)0(ωN1)1(ωN1)N1(ωn(N1))0(ωn(N1))1(ωN(N1))N1]FF=[(ωn0)0(ωn0)1(ωn0)n1(ωn1)0(ωn1)1(ωn1)n1(ωnn1)0(ωnn1)1(ωnn1)n1]

可以发现 DFT 和 FFT 基本一致,它们的差别在于:

  • 朴素 FFT 要求 n2 的幂,但 DFT 序列长度可以是任意正整数。
  • DFT 和 FFT 带入单位根的顺序是相反的。

注意这些细微差别,不要把 DFT 和 FFT 搞混了。

4.3.6 循环卷积

4.4 离散傅里叶逆变换

离散傅里叶逆变换(Inverse Discrete Fourier Transform,IDFT)可以视为单位根处插值的过程。即给出 n=2w 个在所有 n 次单位根处的点值 Pk=(ωnk,f(ωnk))(0k<n),要求还原 f 的各项系数,其中 f 的次数不大于 n1

类似地,IDFT 和 IFFT 之间也存在一些差异。想必各位读者根据之前的内容已经可以猜出这种差异是什么了。

4.4.1 范德蒙德方阵逆

考虑范德蒙德方阵

A=[x00x01x0n1x10x11x1n1xn10xn11xn1n1]

这玩意怎么求逆?我们考虑最暴力的方法:拉格朗日插值!

因为范德蒙德方阵可以看成多项式多点求值,根据

[x00x01x0n1x10x11x1n1xn10xn11xn1n1]×[a0a1an1]=[f(x0)f(x1)f(xn1)]

再结合拉格朗日插值公式

f(x)=i=0n1f(xi)jixxjxixj

可知从 f(xj) 贡献到 ai 的系数为 (A1)ij=[xi]kixxkxjxk

这就是范德蒙德方阵逆当中每个元素的表达式。

4.4.2 算法介绍

很显然,f 在经过快速傅里叶变换后,再进行快速傅里叶逆变换,仍得到 f

因此,只需对快速傅里叶变换的矩阵求逆,即可得到快速傅里叶逆变换的矩阵。

ω 表示 ωn,则

F=[(ω0)0(ω0)1(ω0)n1(ω1)0(ω1)1(ω1)n1(ωn1)0(ωn1)1(ωn1)n1]

(F1)ij=[xi]kjxωkωjωk

分子和分母均形如 g(x)=0k<n(xωk)xωj,我们研究该函数的性质。

首先,因为 ωk(0k<n)xn1=0n 个互不相同的根,根据因式定理,0k<n(xωk) 就等于 xn1。感性理解:将 0k<n(xωk) 展开,再应用单位根的 对称性

模拟短除法 xn1xωj,可知

g(x)=xn1+ωjxn2+(ωj)2xn3++(ωj)n1x0

因此

(F1)ij=[xi]g(x)g(ωj)=(ωj)n1in(ωj)n1=(ωj)iωjnωj=ωijn

这就说明

F1=1n[(ω0)0(ω0)1(ω0)n1(ω1)0(ω1)1(ω1)n1(ω(n1))0(ω(n1))1(ω(n1))n1]

因此,对一个序列做 IFFT,只需将 FFT 递归公式里面的 ωLk 换成 ωLk,并在最后将所有数除以 n 即可。

这就引出了 IDFT 公式及其对应矩阵:

xn=1Nk=0N1Xke2πiNkn=k=0N1XkωNknFD1=1N[(ωN0)0(ωN0)1(ωN0)N1(ωN1)0(ωN1)1(ωN1)N1(ωNN1)0(ωNN1)1(ωNN1)N1]

4.5 快速多项式乘法

通过 FFT 和 IFFT,我们可以在 O(nlogn) 的时间内实现 n1 次多项式在系数表示法和点值表示法之间的转换。

计算两个次数分别为 n1m1 的多项式 a,b 相乘,设结果为 c,则 cn+m2 次多项式,我们需要 n+m1 个点值才能确定它。根据点值的性质,首先找到不小于 n+m1 的最小的 2 的幂 L,对系数表示法的 a,b 分别做长度为 L 的 FFT,将对应点值相乘得到 c^,再对 c^ 做 IFFT 还原 c


首先我们需要实现一个复数类,它支持复数的加减乘运算。也可以使用 C++ 自带复数类 std::complex<T>,用法见 CPP reference

FFT 和 IFFT 大体上一致,只有一些细小差别。我们可以类似实现 FWT 和 IFWT 那样,用同一个函数实现它们,并用一个参数区别。

等式 ωn=cos(2πn)+isin(2πn) 帮助我们求出 n 次单位根。

  • 注意浮点数的精度。当多项式系数的绝对值较大时,需使用 long double 甚至 5.2 小节的任意模数卷积。

模板题 P3803 代码:

#include <bits/stdc++.h>
using namespace std;

constexpr int N = 1 << 21;
constexpr double pi = acos(-1);

struct comp {
  double a, b; // a + bi
  comp operator + (const comp &x) const {return {a + x.a, b + x.b};}
  comp operator - (const comp &x) const {return {a - x.a, b - x.b};}
  comp operator * (const comp &x) const {return {a * x.a - b * x.b, a * x.b + b * x.a};}
} f[N], g[N], h[N];
int n, m, r[N];

void FFT(int L, comp *f, bool type) { // L 表示长度, type = 1 表示 FFT, type = 0 表示 IFFT
  for(int i = 1; i < L; i++) {
    r[i] = (r[i >> 1] >> 1) + (i & 1 ? L >> 1 : 0);
    if(i < r[i]) swap(f[i], f[r[i]]);
  }
  for(int d = 1; d < L; d <<= 1) {
    comp wd = {cos(pi / d), sin(pi / d)}; // 2d 次单位根
    if(!type) wd.b = -wd.b; // IFFT 单位根取倒数, 相当于沿 x 轴对称
    static comp w[N];
    w[0] = {1, 0};
    for(int j = 1; j < d; j++) w[j] = w[j - 1] * wd; // 用数组记录 0 ~ d-1 次单位根, 减少复数乘法次数
    for(int i = 0; i < L; i += d << 1) {
      for(int j = 0; j < d; j++) {
        comp x = f[i | j], y = w[j] * f[i | j | d];
        f[i | j] = x + y, f[i | j | d] = x - y;
      }
    }
  }
  if(!type) for(int i = 0; i < L; i++) f[i].a /= L; // IFFT 时所有数要除以长度 L, 我们只用到了实部所以只需将实部除以 L
}

int main() {
  cin >> n >> m;
  for(int i = 0; i <= n; i++) cin >> f[i].a;
  for(int i = 0; i <= m; i++) cin >> g[i].a;
  int L = 1;
  while(L <= n + m) L <<= 1;
  FFT(L, f, 1), FFT(L, g, 1);
  for(int i = 0; i < L; i++) h[i] = f[i] * g[i];
  FFT(L, h, 0);
  for(int i = 0; i <= n + m; i++) cout << (int) (h[i].a + 0.5) << (i < n + m ? ' ' : '\n');
  return 0;
}

4.6 快速数论变换

前置知识:原根和阶。

我们将 DFT 的数值范围从复数域推广至任意存在 n 次单位根 α 的环 R,其中 α 满足 αk(0k<n) 互不相同,对 x0,x1,,xn1 DFT 得 X0,X1,,Xn1,则

Xi=j=0n1xjαij

也可以视作 Xi=f(αi),其中 f(t)=j=0n1xjtj

类似可知 IDFT

xj=1ni=0n1Xiαij

即 DFT 和 IDFT 对序列进行的变换的本质抽象。

4.6.1 算法介绍

快速数论变换即在模 p 意义下的整数域 F=Z/p 进行的快速傅里叶变换。

设变换长度为 n,则需存在 n 次单位根 a 满足 δp(a)=n。大部分题目的模数 p 满足:

  • p 为质数。
  • 2kp1,其中 2k 不小于最大的可能的 NTT 长度。

第一条性质保证 p 存在原根 gn 可逆,第二条性质保证存在 n 次单位根。注意它不是充要条件,只是充分条件。

根据原根的性质,δp(g)=p1,即 g0p2 次幂互不相同,则 gF 上的性质和复数域上 p1 次单位根的性质完全一致:gk(0k<p1)ωp1k(0k<p1) 形成的域是同构的,gk 等价于 ωp1k

因此,q=gp1n 等价于 ωp1p1nωn,它的 0n1 次幂互不相同,即 δp(q)=n,也可以通过阶的性质 δp(gk)=δp(g)gcd(δp(g),k) 说明。

常见 NTT 模数有:

  • 998244353=7×17×223+1,有原根 3。它可以用来做长度不超过 223 的 NTT,也是最常见的模数。
  • 1004535809=479×221+1,有原根 3
  • 469762049=7×226+1,有原根 3

如果不是常见模数,我们需要检验 p 是否为形如 q2k+1 的质数,2k 是否足够大,再运用原根相关的知识枚举并判定找到任意一个原根,就可以做 NTT 了。

注意以上只是模数 p 可 NTT 的充分条件,它的更弱的条件是存在 δp(a)=nn1。如果 NTT 是正解的一部分,那么一个合格的出题人应将 p 设为常见模数,因为模数不是考察重点。

4.6.2 代码实现

朴素 NTT 已经比 FFT 快了不少,因为复数运算很耗时。

接下来加入一些常数优化:

  • 预处理 2d 次单位根的 0d1 次幂,这样对每个 i 枚举 j 的时候就不需要重复计算单位根的幂。
  • unsigned long long16 次模一次的技巧,减少取模次数。类似技巧也用于优化矩阵乘法。

综上,写出如下代码(依然是 模板题):尽管题目没有要求取模,但可视为在模 p 意义下进行多项式乘法,其中 p 大于答案的每一项。

#include <bits/stdc++.h>
using namespace std;

using ull = unsigned long long;
constexpr int N = 1 << 21;
constexpr int mod = 998244353;
constexpr int ivg = (mod + 1) / 3;

int ksm(int a, int b) {
  int s = 1;
  while(b) {
    if(b & 1) s = 1ll * s * a % mod;
    a = 1ll * a * a % mod, b >>= 1;
  }
  return s;
}

int n, m, r[N], f[N], g[N], h[N];
void FFT(int L, int *a, bool type) {
  static ull f[N], w[N];
  for(int i = 0; i < L; i++) f[i] = a[r[i] = (r[i >> 1] >> 1) | (i & 1 ? L >> 1 : 0)];
  for(int d = 1; d < L; d <<= 1) {
    int wd = ksm(type ? 3 : ivg, (mod - 1) / (d + d));
    for(int j = w[0] = 1; j < d; j++) w[j] = 1ll * w[j - 1] * wd % mod;
    for(int i = 0; i < L; i += d << 1) {
      for(int j = 0; j < d; j++) {
        int y = w[j] * f[i | j | d] % mod;
        f[i | j | d] = f[i | j] + mod - y, f[i | j] += y;
      }
    }
    if(d == (1 << 16)) for(int p = 0; p < L; p++) f[p] %= mod;
  }
  int inv = ksm(L, mod - 2);
  for(int i = 0; i < L; i++) a[i] = f[i] % mod * (type ? 1 : inv) % mod;
}
int main() {
  cin >> n >> m;
  for(int i = 0; i <= n; i++) cin >> f[i];
  for(int i = 0; i <= m; i++) cin >> g[i];
  int L = 1;
  while(L <= n + m) L <<= 1;
  FFT(L, f, 1), FFT(L, g, 1);
  for(int i = 0; i < L; i++) h[i] = 1ll * f[i] * g[i] % mod;
  FFT(L, h, 0);
  for(int i = 0; i <= n + m; i++) cout << h[i] << (i < n + m ? ' ' : '\n');
  return 0;
}

5. 应用与技巧

FFT 和 NTT 是所有快速多项式操作的基础。

设多项式 f DFT 得到 f^,也记为 DFT(f)。可以发现,DFT 是线性变换,因此它具有 线性性,这是它最重要且最常用的一个性质:

  • cDFT(f)+dDFT(g)=DFT(cf+dg)

5.1 常数优化

在分析变换次数的时候,一般不区分 DFT 和 IDFT。

5.1.1 三次变两次优化

计算两 实系数 多项式 f,g 相乘。

根据 (a+bi)2=(a2b2)+2abi,将 f+gi 平方后取出虚部再除以 2 即可。

这样,三次 DFT 变成了两次 DFT,对常数有显著优化。代码

这个优化被接下来稍复杂的技巧完全包含,它们的核心思想都是利用实系数的性质。

5.1.2 合并两次实系数 DFT

同时计算两 实系数 多项式 f,g 的 DFT。

类似地,我们设 u=f+igv=fig。先求出 u 的 DFT u^,考虑能否利用 u^ 直接求出 v^

在给出做法之前,我们还要引入一些复数相关的概念:

  • 定义:对于两个复数 z1z2,若它们实部相等,虚部互为相反数,则称 z1,z2 互为 共轭复数z2z1 的共轭,z1z2 的共轭。
  • 表示:复数 z 的共轭记作 zconj(z)
  • 运算性质:两个共轭复数的辐角互为相反数,即 argz1=argz2。根据棣莫弗定理,可知 复数积的共轭,等于它们共轭的积。这样理解:求共轭相当于把整个复平面沿 x 轴翻转,求积再翻转等价于翻转再求积。
  • 易知复数和的共轭等于它们共轭的和,且一个复数的共轭的共轭等于它本身。

首先,v(ωnk)=i=0n1vi(ωnk)i。为了让它和 u 产生联系,因为 f,g 为实系数多项式,所以 uv 的各项系数互为共轭,我们对整个结果求两次共轭,并将一次共轭根据共轭的性质下放至 viωnk

v^k=v(ωnk)=i=0n1vi(ωnk)i=conj(conj(i=0n1vi(ωnk)i))=conj(i=0n1conj(vi(ωnk)i))=conj(i=0n1conj(vi)conj(ωnk)i)=conj(i=0n1conj(vi)conj(ωnk)i)=conj(i=0n1ui(ωnnk)i)={conj(u^nk)(1k<n)conj(u^0)(k=0)

求得 v^ 之后,因为 u^=f^+ig^v^=f^ig^,所以 f^=u^+v^2g^=u^v^2i=(v^u^)i2

5.1.3 合并两次实系数 IDFT

这里实系数指 IDFT 后的各项系数为实数。如果是 IDFT 前的各项系数为实数,直接使用上一小节的技巧即可。

设需要 IDFT 的两个多项式为 f^g^。计算 IDFT(f^),各项系数均为实数,虚部信息被浪费了。考虑计算 IDFT(f^+ig^),则各项系数的实部即 f 的系数,虚部即 g 的系数。

5.2 任意模数卷积

给定两多项式 f,g,在系数模 p 意义下求出它们的卷积 h=fg

p 不是 NTT 模数,我们不能朴素地使用 FFT 求解该问题,因为 h 的系数可达 np2。取 n=106p=109,则 np2=1024long double 也无法接受。

接下来介绍两种常见的解决该问题的方法。它们也被称为 MTT(非正式),得名于毛啸 2016 年的集训队论文。

5.2.1 拆系数 FFT

B=p,将所有系数表示为 kB+r(0r<B) 的形式,得到四个多项式 f=f0B+f1g=g0B+g1,计算它们两两相乘的结果,则 fg=(f0g0)B2+(f0g1+f1g0)B+f1g1

显然有一个四次 DFT 和三次 IDFT 的朴素做法:对 f0,f1,g0,g1 进行 DFT,f^0g^0,f^0g^1+f^1g^0,f^1g^1 进行 IDFT。

使用 6.1.2 和 6.1.3 的技巧,可以做到两次 DFT 和两次 IDFT。系数值域 nB2=np=1015,勉强可以接受。

模板题 P4245 任意模数多项式乘法代码。注意用 long doubledouble 会被卡精度,或者换一种精度较高的 FFT 写法

5.2.2 三模数 NTT

前置知识:(扩展)中国剩余定理。

选取三个常用 NTT 模数,分别算出 fg 在这些模数下的结果,再使用中国剩余定理合并即可。

我们选择 p1=998244353p2=1004535809p3=469762049,设结果分别为 h1,h2,h3

如果使用 CRT,则需要 __int128,因为 h 的真实值为

(h1p2p3×inv(p2p3,p1)+h2p1p3×inv(p1p3,p2)+h3p1p2×inv(p1p2,p3))mod(p1p2p3)

使用 exCET 就不需要 __int128

  • 合并 hh1(modp1)hh2(modp2)。设 h=h1+yp1,则 h1+yp1h2(modp2),解得 y[0,p2) 之后得到 hh1+yp1(modp1p2),记作 hx(modp1p2)
  • 合并 hx(modp1p2)hh3(modp3)。设 h=x+yp1p2,类似解得 y[0,p3),得到 hx+yp1p2(modp1p2p3)。因为 x+yp1p2<p1p2p3,所以它就是真实值,可以直接取模。

效率比拆系数 FFT 低不少,因为进行了九次 DFT。代码

5.3 分治 NTT

前置知识:CDQ 分治。

分治 NTT 在信息竞赛界应用广泛,这归功于他所解决的问题:形如 fi=j=0i1fjgij半在线卷积

5.3.1 算法介绍

形如给定 g1gn1f0,求 f1fn1 满足 fi=j=0i1fjgij 的问题被称为 半在线卷积。因为 fi 很大,一般会对 NTT 模数 p 取模。

我们不能通过简单的 NTT 解决这个问题,因为 f 的每一项均和之前项有关,这要求我们在尝试计算 fi 时必须已经求出 f0fi1,而 NTT 做不到这一点。或者说,它们解决的问题形式不同。

这是一个在线的问题,考虑使用 CDQ 分治 转化为静态问题。

  • 设当前分治区间为 [l,r],其中 f0fl1flfr 的贡献已经计算完毕。
  • l=r,说明已经求出 fl,返回。
  • 否则,令 m=l+r2,先向左递归 [l,m] 求解 flfm
  • 为了求解 fm+1fr,我们需要计算 flfm 对它们的贡献:fi=j=lmfjgij。因为 flfm 已知,所以总的贡献相当于两个已知的多项式相乘的结果。具体地,令 fj=fj+l(0jml),计算 h=fg,则 fi 受到 hil 的贡献。
  • 向右递归 [m+1,r] 求解 fm+1fr

因为 f,g 的长度均不超过区间长度,所以时间复杂度 O(nlog2n)

模板题 P4721 分治 FFT 代码:

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using ull = unsigned long long;

constexpr int N = 1 << 17;
constexpr int mod = 998244353;
void add(int &x, int y) {x += y, x >= mod && (x -= mod);}
int ksm(int a, int b) {
  int s = 1;
  while(b) {
    if(b & 1) s = 1ll * s * a % mod;
    a = 1ll * a * a % mod, b >>= 1;
  }
  return s;
}

int n, f[N], g[N];
void solve(int l, int r) {
  if(l == r) return;
  int m = l + r >> 1, L = 1;
  solve(l, m);
  while(L < r - l + 1) L <<= 1;
  static int a[N], b[N], c[N];
  memset(a, 0, L << 2);
  memcpy(a, f + l, m - l + 1 << 2);
  memcpy(b, g, L << 2);
  NTT(L, a, 1), NTT(L, b, 1);
  for(int i = 0; i < L; i++) c[i] = 1ll * a[i] * b[i] % mod;
  NTT(L, c, 0);
  for(int i = m + 1; i <= r; i++) add(f[i], c[i - l]);
  solve(m + 1, r);
}

int main() {
  cin >> n;
  for(int i = 1; i < n; i++) scanf("%d", &g[i]);
  f[0] = 1, solve(0, n - 1);
  for(int i = 0; i < n; i++) printf("%d%c", f[i], i + 1 < n ? ' ' : '\n');
  return 0;
}

5.3.2 阶梯网格路径计数

阶梯网格路径计数是一类可以使用分治 NTT 解决的经典问题。

给定 n+1m+1 行的网格图,左下角和右上角分别记为 (0,0)(n,m)。从 (0,0) 出发,每次只能向右或向上走,求走到 (n,m) 的方案数。让我们回忆:方案数为 (n+mn)

当然,问题没有这么简单。我们限制第 i 列不能走到行数大于 ci 的点,其中 ci 单调不降,且 cn=m。换言之,求出在一个阶梯网格从左下角走到右上角的方案数。

显然有 O(nm) 的动态规划:设 fi,j 表示走到 (i,j) 的方案数,则 fi,j 转移至 fi+1,jfi,j+1

考虑一段 ci 相同的极长区间 [l,r](l<r)fl,j1 转移到 fr,j2 的贡献系数:为防止重复计数,从 (l,j1) 出发的第一步应当向右,因此有系数 ((rl1)+(j2j1)rl1)。设 gi 表示 (rl1+ii),则 fl,j1gifr,j1+i,为卷积形式。

在不受 ci 影响的时候,我们确实可以这样做。但是对于每个 i,它内层的所有 j 之间也有转移,这让我们不方便借助分治和卷积加快整个过程。考虑进行一些等价转换便于分层。

稍作思考,设 fk,j 表示走到 (i,j) 其中 i+j=k 的方案数,则 fk,j 转移至 fk+1,jfk+1,j+1

进一步地,为了避免在 k>n 时受到 jkn 的限制,考虑从 (n,0) 开始沿左上方向把整个阶梯网格劈成两半,分别计算从 (0,0)(n,m) 到斜对角线上的点(i+j=n)的方案数,而这两个问题是对称的。

综合上述分析,我们将问题转化为:从 (0,0) 出发,每次可以向右或右上走一步,求走到最右侧一列每个点的方案数。其中,在第 i 列不能走到行数大于 ci 的点。这个 ci 可通过原问题的 ci 进行简单转化得到,且容易证明其仍不降且满足很好的性质:ci+1ci1

因此,从 fl 推到 fr 的时候,我们会发现对于 jcr(rl),设不等号右侧的数为 xfl,jfr 的贡献不会受到 c 的影响:因为 ci+1ci1,所以从 (l,j) 开始,就算每一步都往右上走,也不会突破限制。这样,fl,0fl,xfr 的贡献可直接卷积计算。对于 fl,x+1fl,cl,分治下放至左右子区间 (l,m](m,r] 进行递归。

有了大致思路,设计算法就很简单了:设 solve(l,r,Δ,F) 表示当前区间为 (l,r],传入多项式 F 的第 i 项表示 fl,i+Δ,返回多项式 G 的第 i 项表示 fr,i+Δ。也可视作暂时将 clcr 全部减去 Δ,传入 fl=F,返回 G=fr,接下来就使用这种视角。

对于 jcr(rl),设不等号右侧的数为 xF0FxH0Hrl 卷积求出 F0Fx 转移得到的 G0Gcr,其中 Hi 即转移系数 (rli)

对于 j>x,分治下放:设 F=Fx+1Fclmid=l+r2,则先递归左半部分 Fsolve(l,mid,Δ+x+1,F),再递归右半部分 Fsolve(mid,r,Δ+x+1,F),则此时 F 表示从 Fx+1Fcl 转移得到的 Gx+1Gcr

两部分相加即得 G,返回即可。

边界 rl=1 的处理是平凡的。

两次下传 F 时,F 的长度显然不超过 crx=rl,因此在处理区间 (l,r] 时涉及到的所有多项式的长度均不超过其父区间长度。设 n,m 同级,则时间复杂度为 O(nlog2n)

接下来对问题进行一些扩展:

  • 如果没有 ci+1ci1 的性质,但 ci 单调不降,还能做吗?答案是可以,只需将 x 的定义改为 cl(rl),此时 (l,r] 涉及到的多项式长度不超过父区间的长度加上端点处 c 的差值,可以接受。

5.4 例题

CF1770G Koxia and Bracket

相比于 F,这道题就略显套路了。

将左括号视为 1,右括号视为 1,找到最后一个使得前缀和小于 0 的位置 lst,则 s1slst 的每个左括号都有用,必然不会删去。同理,slst+1sn 的每个右括号也都有用。

对于 s1slst 的每个右括号 si,如果它对应的前缀和为 ci,则为保证前缀和非负,在 s1si 中至少需要删掉 ci 个右括号。我们只关心 ci 的前缀最大值,因为若这些位置满足条件,则所有位置满足条件,而每个前缀最大值一定会比先前的前缀最大值大 1,所以我们将问题转化为:给定长为 m 的序列,其中有 c 个位置被打上了标记,求出选择 c 个位置的方案数,使得对于每个前缀,选择的位置数不小于被打上标记的位置数。

fi,j 表示考虑到第 i 个位置,当前选择位置数减去打标记位置数的数量为 j。对于没有被打标记的位置 pfp1,j 转移到 fp,j/j+1,否则转移到 fp,j1/j

非常明显的阶梯格路计数问题,稍有变形,不过解法大差不差,核心思想是一致的:考虑 fl 转移到 fr,用卷积描述一部分转移,剩下来 O(rl) 个位置分别递归两侧处理。

对于本题,就是 fl,j(jcnt)fr 的贡献用卷积描述,其中 cnt 表示 l+1r 的被打上标记的位置。剩余部分递归,长度只有 cnt,而且因为截取的是低位,我们甚至不需要记录偏移量 Δ

rl=1 的边界情况是平凡的。

slst+1sn 的左括号类似处理即可。

每个分治区间涉及到的多项式长度不超过其父区间长度,因此时间复杂度为 O(nlog2n)代码

参考资料

第一章:

第二章:

第四章:

第五章:

posted @   qAlex_Weiq  阅读(6849)  评论(19编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示