多项式全家桶(长期更新)
更新日志:
24.7.25:更新了暴力多项式全家桶部分。
24.7.27:更新了单位根性质、以及 部分。
24.7.29:更新了导数部分。
24.8.5:更新了积分,泰勒展开,快速多项式全家桶,常系数齐次线性递推以及Lagrange插值部分。
25.2.11:挖了更多的坑,加入了多项式全家桶的部分封装代码。
Code:
前言
若无特殊说明,默认运算在
暴力多项式全家桶
加法
Sol:
乘法
Sol:
取模
Sol:
如高精度除法模拟,
求逆
Sol:
注意到
我们将
于是就有
求导&积分
求
Sol:
开根
求
Sol:
类似求逆时的处理方法:
移一下项就有:
注意
-
若
,则 的解数,取决于 的平方根数量。 -
若
,令 ,满足 ,那么若 为奇数,则 无解;若 为偶数,记 ,那么 。
保证
求
Sol:
对原式左右两边求导:
先求出
那么有
保证
求
Sol:
狠狠的求导:
那么就有
外加
三角函数
求
Hint:
(由欧拉公式得到)
Sol:
直接把
若
否则可以扩域,复杂度都是
前置
多项式插值
证:
利用待定系数法可以得到一个
观察系数矩阵,可以发现其一定是满秩的。(关于矩阵的秩,见线性代数)
也就意味着消元时不可能消掉一个变量的同时消掉另一个变量,那当然就有一个唯一解。
复数
复数
-
加法:向量加法。
-
乘法:模长相乘,辐角相加。
单位根
- 代数基本定理:任何复系数一元
次多项式方程在复数域上至少有一根。 - 推论:任何复系数一元
次多项式方程在复数域上恰好有 个根。
其中
根据其几何意义,可知
并且
单位根将单位圆
拓宽一下
我们现在要求
根据多项式插值中的原理,我们可以将
(这一步将系数表示法转化为点值表示法。)
由
所以将上文两系列点相乘即可得到
普通单点求值是
那么如何选取
Sol:
选取
由于单位根的一些性质,我们可以
定义操作
考虑如何计算
若
于是可以递归,每次
如何以优秀的时间复杂度将点值表示转化为系数表示?
Sol:
定义操作
注意到一个重要的式子:
(即单位根反演)
证明:
发现这是等比数列求和的形式,讨论公比
-
若
,则 ,故 -
若
,则 ,故
综上,原式得证。
于是又有另一个式子:
证明:
有了这个式子后,
至此,我们成功以
如何代码实现?
Sol:
直接按上面的流程做,是递归版本的写法。
(摘自oiwiki)
#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];
// rev=1,DFT; rev=-1,IDFT
void DFT(Comp* f, int n, int rev) {
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
DFT(g, n / 2, rev), DFT(h, n / 2, rev);
// cur 是当前单位复根,对于 k = 0 而言,它对应的单位复根 omega^0_n = 1。
// step 是两个单位复根的差,即满足 omega^k_n = step*omega^{k-1}*n,
// 定义等价于 exp(I*(2*M_PI/n*rev))
Comp cur(1, 0), step(cos(2 * M_PI / n), sin(2 * M_PI * rev / n));
for (int k = 0; k < n / 2;++k) {
// F(omega^k_n) = G(omega^k*{n/2}) + omega^k*n\*H(omega^k*{n/2})
tmp[k] = g[k] + cur * h[k];
// F(omega^{k+n/2}*n) = G(omega^k*{n/2}) - omega^k_n*H(omega^k\_{n/2})
tmp[k + n / 2] = g[k] - cur * h[k];
cur *= step;
}
for (int i = 0; i < n; ++i) f[i] = tmp[i];
}
当然,我们还可以进一步压缩常数,就是将递归版改成非递归版。
事实上从上往下递归的过程中将
因此我们可以非递归地完成这一步,然后向上合并即可。
交换位置的操作通过预处理可做到
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1<<22;
const double pi=acos(-1.0);
struct com{
double x,y;
com(double a=0,double b=0):x(a),y(b){}
com operator+(const com &a) const{
return com(x+a.x,y+a.y);
}
com operator-(const com &a) const{
return com(x-a.x,y-a.y);
}
com operator*(const com &a) const{
return com(x*a.x-y*a.y,x*a.y+y*a.x);
}
}a[maxn],b[maxn];
int n,m,len,rev[maxn];
void init(int len){
for(int i=0;i<len;++i){
rev[i]=rev[i>>1]>>1;
if(i&1) rev[i]|=len>>1;
}
}
void DFT(com f[],int len,int rv){
for(int i=0;i<len;++i){
if(i<rev[i]) swap(f[i],f[rev[i]]);
}
for(int h=2;h<=len;h<<=1){
com wn(cos(2.0*pi/h),sin(2.0*rv*pi/h));
for(int i=0;i<len;i+=h){
com w(1.0,0);
for(int k=i;k<i+h/2;++k){
com u=f[k];
com v=w*f[k+h/2];
f[k]=u+v;
f[k+h/2]=u-v;
w=w*wn;
}
}
}
if(rv==-1){
for(int i=0;i<len;++i){
f[i].x/=len;
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;++i) cin>>a[i].x;
for(int i=0;i<=m;++i) cin>>b[i].x;
len=1;
while(len<=n+m) len<<=1;
init(len);
DFT(a,len,1),DFT(b,len,1);
for(int i=0;i<len;++i) a[i]=a[i]*b[i];
DFT(a,len,-1);
for(int i=0;i<=n+m;++i) printf("%d ",(int)(a[i].x+0.5));
return 0;
}
注意非递归版最适合实战。
一些小细节:
-
从代码中可以看到,向上合并的过程是倍增的。所以多项式的长度要拓展到
并且大于相乘的两个多项式 的长度之和。 -
这样做是不会对答案有影响的,因为多出来的位上系数默认为
。最后输出时去掉多出来的位数即可。 -
之后别忘了 。 -
别混淆要处理的多项式长度
与要保留的多项式长度 。
我们已知复数域中有性质良好的单位根
我们发现,当
但是这里要求
我们发现对于质数知名常用模数(尽管有时题目与多项式无关)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=4e6+5,mo=998244353;
ll n,m,a[maxn],b[maxn],rev[maxn];
void init(int len){
for(int i=1;i<=len;++i){
rev[i]=rev[i>>1]>>1;
if(i&1) rev[i]|=len>>1;
}
}
ll ksm(ll a,ll b){
ll res=1;
a%=mo;
while(b){
if(b&1) res=res*a%mo;
a=a*a%mo;
b>>=1;
}
return res;
}
void NTT(ll f[],int len,int rv){
for(int i=0;i<len;++i){
if(i<rev[i]) swap(f[i],f[rev[i]]);
}
for(int h=2;h<=len;h<<=1){
int wn=ksm(3,(mo-1)/h);
for(int i=0;i<len;i+=h){
ll w=1;
for(int k=i;k<i+h/2;++k){
int u=f[k],v=w*f[k+h/2]%mo;
f[k]=(u+v)%mo;
f[k+h/2]=(u-v+mo)%mo;
w=w*wn%mo;
}
}
}
if(rv==-1){
reverse(f+1,f+len);
int inv=ksm(len,mo-2);
for(int i=0;i<len;++i) f[i]=f[i]*inv%mo;
}
}
int main(){
scanf("%lld%lld",&n,&m);
for(int i=0;i<=n;++i) scanf("%lld",&a[i]);
for(int i=0;i<=m;++i) scanf("%lld",&b[i]);
int len=1;
while(len<=n+m) len<<=1;
init(len);
NTT(a,len,1),NTT(b,len,1);
for(int i=0;i<len;++i) a[i]=a[i]*b[i]%mo;
NTT(a,len,-1);
for(int i=0;i<=n+m;++i) printf("%lld ",a[i]%mo);
return 0;
}
快速多项式全家桶
前置
部分高等数学
一些高等数学。
快去看。
牛顿迭代法( )
快速求零点
对于一条曲线,我们要求它的零点。
先猜测一个零点,再求出该点处的切线,然后取切线的零点重复以上过程,可以快速逼近零点。
快速求函数
我们已知
先猜测一个函数
正确性证明:
最开始手动求出
假设现在已知
我们把
注意到
所以
即:
整理后得到:
我们令
正式开始
加法
求
Sol:
直接加,
乘法
求
Sol:
直接上
求导&积分
同暴力多项式全家桶部分,略。
求逆
求
Sol:
化式子:
首先
然后带入牛顿迭代的式子得到:
化简一下得到:
即
倍增即可。
取模
求
Sol:
设
那么
设
同理有
容易得到
证明上式:
由
知
于是
这也就是
注意到
故
于是通过求逆就可以求出
开根
求
Sol:
化式子:
带入牛顿迭代的式子:
直接求就可以,或者再稍微化简:
关于
保证
Sol:
两边求导:
求出
保证
Sol:
两边取
代入牛顿迭代式子有:
化简得到:
注意这里要多次求
三角函数
同暴力多项式全家桶部分套用公式,并使用多项式
分治FFT
多项式多点求值
多项式快速插值
多项式平移
多项式连续点值平移
常系数齐次线性递推
已知
Sol:
显然矩乘加速过不了。
以斐波那契数列为例找找规律:
我们每次考虑将下标最大的一项换成下标更小的项,注意到这个形式与多项式取模类似。
更一般地,对于递推式
再看上面的例子,
所以
即
更严谨的证明:
定义
由于
所以
设
那么
所以
也就是求
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效