多项式
单项式:由数字和字母组成的积的代数式称为单项式
多项式:由若干个单项式相加组成的代数式叫做多项式
(废话,这上过初中的人都知道)
一、一元多项式
设(nin N^*),则我们称多项式
为一元多项式
其中,我们称(a_ix^i)为(i)次项,(a_i)为(i)次项系数。
两个多项式(f(x))和(g(x))相等,当且仅当它们同次项的系数相等。
若(a_n ot= 0),则我们称(a_n)为多项式的首项,(n)为多项式的次数,记为(deg) (f)
二、 多项式的基本运算
1.加法与乘法运算
若
则
多项式的加法和乘法满足交换律和结合律,乘法还满足分配律
2.带余除法
若 (f(x))和(g(x))是的两个多项式,且(g(x))不等于0,则有唯一的多项式 (q(x))和(r(x)),满足
其中(r(x))的次数小于(g(x))的次数。
此时,
(q(x)) 称为(g(x))除(ƒ(x))的商式。
(r(x))称为余式。
若(r(x)=0),则我们称(g(x))为(f(x))的因式
多项式的带余除法不是本文研究的重点,有兴趣的可以自行查阅资料
三、关于多项式的几个重要定理
1.代数基本定理
任何复系数一元n次多项式方程在复数域上至少有一根。
代数基本定理在数学中的运用十分广泛。
2.唯一分解定理
数域(P)上的每个次数大于零的多项式(f(x))都可以分解为数域(P)上的不可约多项式的乘积
这与算术基本定理十分相似。
3.高斯引理
本原多项式:若多项式(a_nx^n+a_{n-1}x^{n-1}+...+a_1x+a_0)满足(gcd(a_1,a_2,...,a_n)=1),则我们称该多项式为本原多项式
高斯引理: 两个本原多项式的积是本原多项式
这些引理的应用是数学竞赛的范围了,有兴趣的可以自行了解。
四、多项式的零点
若有一数(a)使得多项式(f(x)=0),则我们称(a)为多项式(x)的零点。
这里我们有两个重要的定理:
1.余式定理
(f(x))除以(x-a)的余式是(f(a))
证明:
设(g(x))为商式,(r(x))为余式,则我们有:
把(x=a)代入得:
证毕。
余式定理的几个推论:
1.因式定理
如果多项式(f(a)=0),那么多项式(f(x))必定含有因式(x-a)。
2.多项式(a_nx^n+a_{n-1}x^{n-1}+...+a_1x+a_0)至多有(n)个不同的零点。
用反证法可以证明,这里略去。
3.如果多项式(a_nx^n+a_{n-1}x^{n-1}+...+a_1x+a_0)至少有(n+1)个零点,那么该多项式为(0)
如果此时他不是(0),则他至多有(n)个零点,由(2)导出矛盾。
2.恒等定理
若有无穷多个(a)使得(f(a)=g(a)),则(f(x)=g(x))
令(h(x)=f(x)-g(x)),因为此时(h(x))有无穷多个零点,由上面的第3个结论可知,(h(x)=0)
看到这里,可能很多人不耐烦了:
我们是OIer,你给我们讲那么多这些玩意干什么?
如果上面是纯数学竞赛内容的话,那么下面就是数竞与信竞的交集了。
--------------------------------------------------------------分割线------------------------------------------------------------------------------------
五、多项式的插值与差分
(注意:本章将涉及到一些OI中的知识,数竞党看不懂可略过)
我们回到多项式乘法。
如果我们暴力乘两个(n)次多项式,那么时间复杂度将是(O(n^2))的
那么我们可不可以对一般的多项式乘法进行些优化呢?
我们可以通过改变运算顺序(CPU的运算原理相关)来优化,但我们最多只能优化些常数因子罢了
如果我们不能跳出上面的思维局限,那么我们不可能在根本上优化算法的时间复杂度
我们得换个角度观察问题。
在平面直角坐标系中,我们给出平面上(n)个不同的点:
那么,我们能否找到一条曲线,使得该曲线穿过这些点呢?
换言之,我们希望找到一个函数(f(x)),使得:
但显然,这样的函数有无数个。
一般而言,多项式函数是这些函数中最简单的。
那么,多项式就有另外一种表示法:
多项式的点值表示法
我们用满足(f(x)=y)的(n)个数对
来表示一个(n-1)次多项式。
如果我们给定一个多项式,给出(n)组点值最简单的方法就是任选(n)个不同的(x_i),分别计算此时多项式的值
用(n)个点值对也可以唯一确定一个不超过(n-1)次多项式,这个过程称为插值
于是,一个著名的公式出现了:
拉格朗日插值公式
对于多项式的点值
存在一个唯一的不超过(n-1)次的多项式(f(x)),并且(f(x))可以表示为:
整理的好看点:
为了证明插值公式,我们必须证明多项式插值的存在性和唯一性
存在性证明:
(这部分涉及一些线性代数的知识,如果实在不会可以略过)
(估计也只有我这种蒟蒻不怎么会线性代数)
我们可以把多项式的每个点值看成一个方程,那么我们就有了(n)个方程,它们组成了线性方程组。
我们把这个线性方程组改写成矩阵的形式:
我们不妨记为(XA=Y)
该式可以转化为(A=X^{-1}Y)
因此我们只需要证明(X)有没有逆矩阵即可。
Q:什么是逆矩阵啊
A:若我们有(AB=E)((E)为单位矩阵),则我们称(B)为(A)矩阵的逆
Q:单位矩阵又是啥啊
A:主对角线上全是1的举证就是单位矩阵。单位矩阵有一个性质,即任何矩阵乘单位矩阵都等于它本身
这里我们介绍一个东西:范德蒙行列式
(线代julao可略过)
长成上面这样的矩阵就是范德蒙矩阵了。
而范德蒙行列式是有它的特殊的计算公式的。
对于一个范德蒙矩阵,它的行列式的计算公式为:
显然,(x_j-x_i ot=0),所以矩阵的行列式值不等于0,即该矩阵有逆
唯一性证明:
如果有两个这样的多项式,那么它们的差就至少有(n)个不同的零点(点值中(y)值差为0),由上面余式定理的第三个推论可得,这个差值一定为0
正确性证明:
现在我们来看看拉格朗日插值公式是怎么构造这个多项式的。
在特殊情形(y_1=y_2=...=y_{n-1}=0)时,求出满足条件多项式(f_1(x))。这时,(f_1(x))有(n-1)个不同的零点(x_1,x_2,...,x_{n-1}),根据因式定理,该多项式被((x-x_1)(x-x_2)...(x-x_{n-1}))整除。
我们有要求多项式的次数尽可能的低,故我们得到:
其中(c)是一个常数,由(f_1(x_0)=y_0)确定。
我们就有:
所以
于是
类似的,我们可以构造(f_i(x)),使得(f_i(x_i)=y_i)。
显然,当(x ot=i)时((x)为(x_0,x_1,...x_{n-1})中的任意一个),(f_i(x)=0)(因为分子总有一个因式值为0)
所以,这样的(f(x)=f_1(x)+f_2(x)+...f_n(x))满足条件。
优缺点
拉格朗日插值公式结构紧凑,十分优美,易于理解,在理论分析中十分方便。
然而,当一个插值点改变时,整个插值公式必须要重新计算。
解决方案就是使用重心拉格朗日插值法或者牛顿迭代法。
然而我太菜了,这两个方法我都不会。
还是得多多学习啊!
插值介绍完了,现在轮到差分了。
广大OIer对差分的影响,估计是这样的:
差分不就是和前缀和结合在一起用的东西吗!
树状数组区间修改用差分!(注:局限性很大)
树上差分!天天爱跑步!(
明明线段树合并就解决了)
多项式差分正是差分众多形式中的一种。
对于函数(f(x))和固定的(h ot=0),
称为(f(x))的步长为(h)的一阶差分,记作(Delta_h^1f(x))。(Delta_h^1f(x))的差分
称为(f(x))的(步长为(h))的二阶差分,记作(Delta_h ^2f(x))
对于(f(x))的(n)阶差分,我们有以下公式:
上述公式在(n=1)时显然成立,于是我们可以结合数学归纳法证明,此处略去。
讲了这么多,一点代码也没有,总该来道题吧!
于是模板题来了!(数竞党慎入)
P4781【模板】拉格朗日插值法
直接上代码吧:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const int MOD=998244353;
typedef long long ll;
int n,k;
ll x[maxn];
ll y[maxn];
inline ll ksm(ll x,int y){
if(!y) return 1;
if(y==1) return x%MOD;
ll tmp=ksm(x,y/2);
tmp=(tmp*tmp)%MOD;
if(y&1) return (tmp*x)%MOD;
else return tmp;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%lld%lld",&x[i],&y[i]);
ll ans=0;
for(int i=1;i<=n;i++){
ll s1=y[i]%MOD;
ll s2=1;
for(int j=1;j<=n;j++) if(i!=j){
s1=(s1*(k-x[j]))%MOD;
s1=(s1+MOD)%MOD;
s2=(s2*(x[i]-x[j]))%MOD;
s2=(s2+MOD)%MOD;
}
ans=(ans+(s1*ksm(s2,MOD-2))%MOD)%MOD;
ans=(ans+MOD)%MOD;
}
printf("%lld
",ans);
return 0;
}
六、多项式乘法的优化
我们先前讨论过多项式的点值表示法。
普通的多项式乘法是(O(n^2))的,但如果我们换成点值表示法呢?
现在我们有两个多项式(f(x))和(g(x)),记它们的积为(r(x))
则对于同样的(a),它们分别有点值((a,y_f)),((a,y_g)),((a,y_r))。
显然,(y_r=y_f*y_g)。
所以如果我们对于每个参与乘法的多项式有(n)个点值的话,我们可以在(O(n))的时间内完成乘法运算!
于是,对于系数表示法的多项式的乘法运算,我们可以尝试用点值表示法优化。
过程如下:
1.将系数表达法的多项式改写为点值表示法的多项式(点值过程)
2.用点值表示法计算多项式的乘法。
3.将点值表示法转化为系数表示法。(插值过程)
下面,我们重点介绍完成这一过程的算法。
七、DFT,IDFT与FFT
DFT:离散傅里叶变换,用来完成点值过程。
IDFT:离散反傅里叶变换,用来完成插值过程。
FFT:DFT和IDFT的高效算法。
前置知识
1.复数
我们把形如(a+bi(a,bin R))的数称为复数,其中(i)为虚数单位,规定
其中(a)被称为实部,(b)被称为虚部。
知道这玩意有啥用?(我还是个初中的蒟蒻)
一会你就知道了。
我们大家都知道笛卡尔发明的万恶的平面直角坐标系。
对于复数,我们有一个类似的东西:复平面
复平面的横轴是实数轴,纵轴是虚数轴。
于是对于每一个复数,我们都有一个唯一对应的向量了。
我们可以用((r, heta))来表示一个虚数,其中(r)为虚数的长度,( heta)为该向量与(x)轴正半轴的夹角。
复数也是有它的四则运算的。
对于两个复数(z_1=a+bi)和(z_2=c+di),它们的和 和 积为:
复数相加和向量加法一样,满足平行四边形法则。
对于乘法,如果用坐标表示的话,我们有:
即传说中的模长相乘,辐角相加。
所以如果两个复数的长度都是1,那他们的乘法就知识单纯的角度相加了。
2.单位根
对于我们的点值过程,我们当然可以随便取几个值代入去算。
但显然,暴力计算(x,x^2,x^3...,x^n)当然是(O(n^2))的。
我们可不可以不计算这些呢?
我们当然希望这些数是(1,-1)这样的数,因为它们的若干次方是1。但明显,这不够。
傅里叶:这个圆上的每个点都可以。
没错,这个圆是单位圆!
我们可以把这个圆(n)(注意:(n)是2的幂次)等分,从((1,0))开始,编号为(0)到(n-1)。我们记编号为(k)的点为(w_n^k)。
显然,((w_n^1)^k=w_n^k(k ot=0))(模长相乘,辐角相加)。
我们把(w_n^1)称为(n)次单位根,简单记为(w_n)。
而这些(w_n^0,w_n^1,w_n^2,...,w_n^{n-1})有个极其重要的性质:它们的(n)次方等于1!
下面我们就要来证明这个性质:
对于(w_n^0),它恒等于1。
而对于大于0的正整数(k)而言,(w^k_n=(w^1_n)^k)
所以只要证明(w_n)的(n)次方等于1即可。
由于我们是把单位圆(n)等分,我们很容易得到(注意这是单位圆):
由欧拉公式
中(x)取(frac{2pi}{n})得
所以
欧拉公式中的(x)取(2pi)得
所以
证毕。
单位根还有以下两个性质:
1.(w_{2n}^{2k}=w_n^k)
它们表示的复数(向量)是相同的,可以感性理解以下。
2.(w_n^{k}=-w_{n}^{k+frac{n}{2}})
它们表示的向量等大而且反向((frac{n}{2})表示二分之一个圆周)
上面两个性质可以结合欧拉公式给出证明,但我觉得证明了反而不是那么好理解了。
快速傅里叶变换(FFT)
有了单位根,我们就可以在DFT的过程中减少次方运算了。
但对于不是(2)的幂次的项,还是要我们暴力乘出来,总体的时间复杂度依然是(O(n^2))的。
这时,FFT闪亮登场!
第一步:离散傅里叶变换(DFT)
现在我们有一个(n)项的多项式(同样地,(n)是2的幂次,且这里(nge2)):
我们现在根据奇偶性把这个多项式分为两半:
我们再令:
把(x^2)代入(g(x),r(x)):
所以
我们设(k<frac{n}{2}),代入(w_n^k)得:
我们再代入(w_n^{k+frac{n}{2}}):
以上两式常被称为蝴蝶操作
我们于是得到了一个结论:
如果我们知道(g(x))和(r(x))在(w_{frac{n}{2}}^0,w_{frac{n}{2}}^1,...,w_{frac{n}{2}}^{n-1})处的值,那么我们就可以在线性时间内求出(f(x))在(w_n^0,w_n^1,...,w_n^{n-1})处的值。
我们再对(g(x))和(r(x))进行相同的操作,于是我们就可以这么递归分治向下求解了。
注意:我们只能对长度为(2)的次幂的数列进行分治FFT,否则递归向下的话两边长度就不相等了。
一般对于这种情况,我们可以在第一次DFT之前把序列长度补成2的次幂。对于新补的高次项,我们令他们的系数为0。
这样,算法的时间复杂度就是(O(nlog n))了。
但是,我相信大家都知道,递归过程是很耗内存的。
我们能不能不递归就进行FFT呢?
当然可以!
我们完全可以在原序列里把这些系数拆分了,然后在“倍增”地合并。
现在的关键问题就是我们如何拆分这些系数了。
(这里我盗张图)
有啥规律呢?
对于原序列中的系数(a_i),令(i)做二进制位翻转操作后的结果为(rev(i))(比如说:在长度为8的情况下,1就是001,翻转之后就是100,即4),那么(a_i)最后的位置就是(rev(i))。
第二步:离散反傅里叶变换(IDFT)
IDFT的过程的作用我们先前说过了,就是要完成插值过程。
我们把单位根代入,就得到下面的线性方程组的等效矩阵:
我们仍然记为(XA=Y)。
相当于我们现在知道了(X,Y)去求(A)了。
我们前面在探究多项式插值的时候,我们已经说明了多项式插值的存在性与唯一性(忘了可以重新看一眼)。
现在,(A=X^{-1}Y),我们前面已经说明(X)存在逆矩阵,于是问题的关键就是求这个逆矩阵了。
我们知道,(X)是范德蒙矩阵,它的行列式有它的特殊计算方式,而它的逆矩阵也有他独特的性质,就是每一项取倒数,再除以(n)(为什么是这样,我不会证)
我们在先前探讨单位根的时候,我们知道:
我们现在取倒数,即-1次方:
于是我们只要把(pi)取成(-3.1415926...),然后其他过程和DFT是一样的!
所以,我们只需要一个函数,就可以完成DFT与IDFT两个操作了。
好了,放上一道模板题:P3803 【模板】多项式乘法(FFT)
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const double Pi=acos(-1.0);
struct Complex{
double x,y;
Complex(double a=0,double b=0){
x=a; y=b;
}
Complex operator +(const Complex a)const{
return Complex(x+a.x,y+a.y);
}
Complex operator *(const Complex a)const{
return Complex(x*a.x-y*a.y,x*a.y+y*a.x);
}
Complex operator -(const Complex a)const{
return Complex(x-a.x,y-a.y);
}
}a[maxn<<1],b[maxn<<1];
int n,m;
int len;
int r[maxn<<1];
inline int read(){
char ch=getchar(); int sum=0; int f=1;
while(!isdigit(ch)) f=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch)) sum=sum*10+ch-'0',ch=getchar();
return sum*f;
}
inline void FFT(Complex *a,int len,int on){
for(int i=0;i<len;i++)
if(i<r[i]) swap(a[i],a[r[i]]);
for(int i=2;i<=len;i<<=1){//一次合并的长度
Complex wn(double(cos(2*Pi/i)),double(sin(on*2*Pi/i)));//当前单位根
for(int j=0;j<len;j+=i){//对每一块分别计算
Complex w(1,0);
for(int k=j;k<j+i/2;k++){//蝴蝶操作,计算当前多项式的点值
Complex u=a[k];
Complex t=w*a[k+i/2];
a[k]=u+t;
a[k+i/2]=u-t;
w=wn*w;
}
}
}
if(on==-1) for(int i=0;i<len;i++) a[i].x/=len;
}
int main(){
n=read(); m=read();
for(int i=0;i<=n;i++) a[i].x=read();
for(int i=0;i<=m;i++) b[i].x=read();
len=1; int cnt=0;
while(len<=n+m) len<<=1,cnt++;//补成2^k长度
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(cnt-1));//找到对应位置
FFT(a,len,1);//DFT
FFT(b,len,1);
for(int i=0;i<len;i++) a[i]=a[i]*b[i];//多项式乘法(用点值计算)
FFT(a,len,-1);//IDFT
for(int i=0;i<=n+m;i++) printf("%d ",int(a[i].x+0.5));//注意加0.5,防止被卡精度
printf("
");
return 0;
}
我们这是“多项式入门”,也就入门到这了!
多项式还有很多内容,包括:
- 快速数论变换(NTT)
- 快速沃尔什变换(FWT)
- 多项式求逆
- 多项式开方
- 多项式除法
- 多项式取模
- 多项式的对数函数与指数函数
- 多项式的三角函数与反三角函数
- 多项式牛顿迭代
- 多项式快速插值|多点求值
这些已经超过我的能力范围(我毕竟还只是一个初中的蒟蒻OIer)。
如果以后有机会的话,我会更新一个叫做多项式进阶的博客的!
如有不足之处请指正,谢谢!
参考资料:
1.苏科版七年级上册《数学》教科书
2.2002年国家集训队论文 张家琳《多项式乘法》
3.2016年国家集训队论文 毛啸 《再探快速傅里叶变换》
3.华东师范大学出版社《奥数教程》高中第三分册
4.百度百科 范德蒙行列式
5.OI wiki 快速傅里叶变换