多项式:从什么都不知道到门都没入
多项式
单项式:由数字和字母组成的积的代数式称为单项式
多项式:由若干个单项式相加组成的代数式叫做多项式
(废话,这上过初中的人都知道)
一、一元多项式
设\(n\in N^*\),则我们称多项式
为一元多项式
其中,我们称\(a_ix^i\)为\(i\)次项,\(a_i\)为\(i\)次项系数。
两个多项式\(f(x)\)和\(g(x)\)相等,当且仅当它们同次项的系数相等。
若\(a_n \not= 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\not=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\not=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\not=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\n",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,b\in R)\)的数称为复数,其中\(i\)为虚数单位,规定
其中\(a\)被称为实部,\(b\)被称为虚部。
知道这玩意有啥用?(我还是个初中的蒟蒻)
一会你就知道了。
我们大家都知道笛卡尔发明的万恶的平面直角坐标系。
对于复数,我们有一个类似的东西:复平面
复平面的横轴是实数轴,纵轴是虚数轴。
于是对于每一个复数,我们都有一个唯一对应的向量了。
我们可以用\((r,\theta)\)来表示一个虚数,其中\(r\)为虚数的长度,\(\theta\)为该向量与\(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\not=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{2\pi}{n}\)得
所以
欧拉公式中的\(x\)取\(2\pi\)得
所以
证毕。
单位根还有以下两个性质:
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的幂次,且这里\(n\ge2\)):
我们现在根据奇偶性把这个多项式分为两半:
我们再令:
把\(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(n\log 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("\n");
return 0;
}
我们这是“多项式入门”,也就入门到这了!
多项式还有很多内容,包括:
- 快速数论变换(NTT)
- 快速沃尔什变换(FWT)
- 多项式求逆
- 多项式开方
- 多项式除法
- 多项式取模
- 多项式的对数函数与指数函数
- 多项式的三角函数与反三角函数
- 多项式牛顿迭代
- 多项式快速插值|多点求值
这些已经超过我的能力范围(我毕竟还只是一个初中的蒟蒻OIer)。
如果以后有机会的话,我会更新一个叫做多项式进阶的博客的!
如有不足之处请指正,谢谢!
参考资料:
1.苏科版七年级上册《数学》教科书
2.2002年国家集训队论文 张家琳《多项式乘法》
3.2016年国家集训队论文 毛啸 《再探快速傅里叶变换》
3.华东师范大学出版社《奥数教程》高中第三分册
4.百度百科 范德蒙行列式
5.OI wiki 快速傅里叶变换