【总结】多项式相关的各种算法

版权声明:侵删,转载请附带链接或评论 https://blog.csdn.net/corsica6/article/details/81951788

目录


基本概念

定义形如这样的函数为多项式:

f ( x ) = i = 0 n 1 a i x i

其中 n 为多项式的项数(系数为0的项也是项), n 1 为多项式的最高次幂, a i 为多项式中 i 次项的系数。


符号约定&说明

  • 一般情况下,函数自变量均用 x 表示。
  • f ( x ) 表示 f 这个多项式
  • f ( a ) 表示自变量 x = a ( a ) f 的函数值
  • f [ n ] 表示多项式 f x n 一项的系数
  • d e g ( f ) 表示多项式 f 的项数,也即最高次幂 1
  • 求积分时,常数项默认为 0

求导&积分

求导:

对于某一项 x n ,求导为 n x n 1

h ( x ) = f ( x ) ,则 h [ n ] = ( n + 1 ) f [ n + 1 ] ( 0 n < d e g ( h ) ) )

inline void getderivative(int n,int *f,int *h)
{
    for(int i=0;i<n;++i) h[i]=mul(f[i+1],i+1);
    h[n]=0;
}

积分:

h ( x ) = f ( x ) d x ,则 h [ n ] = f [ n 1 ] n ( 0 < n < d e g ( h ) , h [ 0 ] = 0 )

inline void getintegration(int n,int *f,int *h)
{
    for(int i=1;i<n;++i) h[i]=mul(f[i-1],inv[i]);//inv[i] i的逆元
    h[0]=0;
}

点值表示&拉格朗日插值

拉格朗日插值讲解

多项式 f ( x ) 不仅可以用每一项系数表示,由拉格朗日插值法,得到也可以用 d e g ( f ) 个二元组 ( x i , f ( x i ) ) ( x i x j , i j ) 来表示,也就是用 n 个点来描述一个 n 项多项式(即为点值表达式):

f ( x ) { d e g ( f ) < n 0 i < n , f ( x i ) = y i , x i x j , i j }

这种用点描述多项式的形式称为多项式的点值表示,由点值转化为系数式的过程称为插值。


卷积(Karatsuba算法&FTT&NTT)

多项式之间的乘积称为卷积。

h ( x ) = f ( x ) g ( x ) ( ) ,则 h [ n ] = i = 0 n f [ i ] g [ n i ] ( 0 n < d e g ( h ) )

d e g ( h ) = N 。直接求多项式卷积,复杂度是 N 2 的,使用分治的 K a r a t s u b a 算法可复杂度为 O ( n l o g 2 3 ) = O ( n 1.585 ) ,使用 F F T / N T T 可以将复杂度降到 N l o g N

K a r a t s u b a 算法原理是利用

        ( A x 2 n + B ) ( C x 2 n + D ) = A C x n + ( ( A + B ) ( C + D ) A C B D ) x 2 n + B D

不断递归减少乘法次数。

F F T / N T T 的基本思想都是利用单位复根/单位原根并采用分治策略先将 f ( x ) , g ( x ) 转化为点值表示,在点值上处理为 h ( x ) 的点值表示(显然 f ( a ) g ( a ) = h ( a ) ),再转化为 h ( x ) ,因为 F F T 中涉及大量虚数实数运算,精度和常数都很感人,所以常用单模数 N T T ,但限制模数为质数。

这里假设大家都已经会 F F T , N T T ,就不详细展开了。

这里先给出 N T T 代码(下文省略),以及习惯性模运算优化:

inline void NTT(int *e,int ptr,int len)//ptr=1 正变换 ptr=0 逆变换
{
    int i,j,k,ori,G,pd,ix,iy;G= ptr? g:inv[g];//g:原根 inv[g]:g的逆元 
    for(i=1;i<len;++i) if(i<rv[i]) swap(e[i],e[rv[i]]);
    for(i=1;i<len;i<<=1){
        ori=fp(G,(mod-1)/(i<<1));
        for(j=0;j<len;j+=(i<<1)){
            pd=1;
            for(k=0;k<i;++k,pd=mul(pd,ori)){
                ix=e[j+k];iy=mul(e[i+j+k],pd);
                e[j+k]=ad(ix,iy);e[i+j+k]=dc(ix,iy);
            }
        }
    }
    if(ptr) return;
    for(i=0;i<len;++i) e[i]=mul(e[i],inv[len]);
}
inline int ad(int x,int y) {x+=y;if(x>=mod) x-=mod;return x;}
inline int dc(int x,int y) {x-=y;if(x<0) x+=mod;return x;}
inline int mul(int x,int y) {return 1ll*x*y%mod;}

牛顿迭代

解近似方程根 h ( x ) = 0 的一种方法。

h ( x ) 当前近似解为 x n ,进一步迭代:

x n + 1 = x n h ( x n ) h ( x n ) x n

引申:当 h ( x ) 中嵌套一个 f ( x ) 构成复合函数 h ( f ( x ) ) = 0 ,同样可以牛顿迭代倍增求解近似的 f ( x ) ,满足 g ( x ) h ( f n ( x ) ) 0 ( m o d   x 2 n )

f n + 1 ( x ) f n ( x ) g ( x ) g ( x ) f n ( x ) ( m o d   x 2 n + 1 )

注: g ( x ) f n ( x ) 即对 g h 的偏导 h ( f n ( x ) ) g ( x ) = h ( f n ( x ) ) f n ( x )

上式可以用泰勒展开证明当 f n ( x ) x 2 n 项满足条件, f n + 1 ( x ) x 2 n + 1 项满足条件。

考虑 g ( x ) f n x 处的泰勒展开式:

g ( x ) = i = 0 i h ( x ) ( x ) i ( f n ( x ) ) i ! ( f ( x ) f n ( x ) ) i

这里令 0 ! = 1

因为 f n ( x ) 存在前 x 2 n 项满足条件,当 i 2 ( f ( x ) f n ( x ) ) 最小次幂不小于 x 2 n + 1 次方。

所以牛顿迭代求得就是泰勒展开式的前两项:

g ( x ) h ( f n ( x ) ) + h ( f n ( x ) ) ( f n + 1 ( x ) f n ( x ) ) ( m o d   x 2 n + 1 )

由因为目标函数 g ( x ) = 0 ,故:

f n + 1 ( x ) = f n x g ( x ) g ( x ) f n ( x )


基本运算&初等函数


求逆

给定 f ( x ) ,求 g ( x ) 满足 f ( x ) g ( x ) 1 ( m o d   x n )

倍增(递归分治)求解。

法1:

观察 f ( x ) g n ( x ) 1 0 ( m o d   x 2 n )

平方后得:

f 2 ( x ) g n 2 ( x ) 2 f ( x ) g n ( x ) + 1 0 ( m o d   x 2 n + 1 )

f ( x ) g n + 1 ( x ) 1 ( m o d   x 2 n + 1 ) 代入:

f ( x ) g n + 1 ( x ) 2 f ( x ) g n ( x ) f 2 ( x ) g n 2 ( x ) ( m o d   x 2 n + 1 )

最终得到:

g n + 1 ( x ) 2 g n ( x ) f ( x ) g n 2 ( x ) ( m o d   x 2 n + 1 )

法2:

考虑牛顿迭代求解:

底层为 g [ 0 ] = i n v ( f [ 0 ] )

h ( g ( x ) ) = f ( x ) g ( x ) 1 = 0

g n + 1 ( x ) g n ( x ) f ( x ) g n ( x ) 1 f ( x ) ( m o d   x 2 n + 1 )

其中 h ( g n ( x ) ) = 1 f ( x ) g n ( x ) ( m o d   x 2 n + 1 )

g n + 1 ( x ) g n ( x ) ( f ( x ) g n ( x ) 1 ) g n ( x ) ( m o d   x 2 n + 1 )

化简得到法1中的式子。

代码:

inline void getinverse(int n,int *f,int *g)
{
    if(n==1){g[0]=fastpow(f[0],mod-2);return;}
    getinv((n+1)>>1,f,g);
    int i,j,len=1,L=0;
    static int cont[N];
    for(;len<n+n;len<<=1) L++;
    for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|((i&1)<<(L-1)));
    for(i=0;i<n;++i) cont[i]=f[i];
    for(i=n;i<len;++i) cont[i]=0;
    NTT(cont,1,len);NTT(g,1,len);
    for(i=0;i<len;++i) g[i]=mul(g[i],dc(2,mul(cont[i],g[i])));
    NTT(g,0,len);
    for(i=n;i<len;++i) g[i]=0;
}

开根

给定 f ( x ) ,求 g ( x ) 满足 f ( x ) g 2 ( x ) ( m o d   x n )

倍增(递归分治)求解。

法1:

观察 g n 2 ( x ) f ( x ) 0 ( m o d   x 2 n )

平方后得:

g n 4 ( x ) 2 g n 2 ( x ) f ( x ) + f 2 ( x ) 0 ( m o d   x 2 n + 1 )

巧妙地移项:

( g n 2 ( x ) + f ( x ) ) 2 4 g n 2 ( x ) f ( x )

化归得:

( g n 2 ( x ) + f ( x ) 2 g n 2 ( x ) ) 2 f ( x ) ( m o d   x 2 n + 1 )

f ( x ) g n + 1 2 ( x ) ( m o d   x 2 n + 1 ) ,故:

g n + 1 ( x ) g n 2 ( x ) + f ( x ) 2 g n 2 ( x ) ( m o d   x 2 n + 1 )

法2:

考虑牛顿迭代求解:

底层为 g [ 0 ] = s q r t ( f [ 0 ] )

h ( g ( x ) ) = g 2 ( x ) f ( x ) = 0

g n + 1 ( x ) g n ( x ) g n 2 ( x ) f ( x ) 2 g n ( x ) ( m o d   x 2 n + 1 )

化简得到法1中的式子。

代码:

inline void getsqrt(int n,int *f,int *g)
{
    if(n==1) {g[0]=sqrt(f[0]);return;}
    getsqrt((n+1)>>1,f,g);
    int i,j,L=0,len=1;
    for(;len<n+n;len<<=1) L++;
    static int inv[N],tp[N];
    for(i=0;i<len;++i) inv[i]=0;
    getinv(n,g,inv);
    for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|((i&1)<<(L-1)));
    for(i=0;i<n;++i) tp[i]=f[i];
    for(i=n;i<len;++i) tp[i]=0;
    NTT(inv,1,len);NTT(g,1,len); NTT(tp,1,len);
    for(i=0;i<len;++i) g[i]=mul(ad(mul(g[i],g[i]),tp[i]),mul(iv[2],inv[i]));
    NTT(g,0,len);
    for(i=n;i<len;++i) g[i]=0;
}

ln

给定 f ( x ) ,求 g ( x ) l n ( f ( x ) ) ( m o d   x n )

l n 不能用牛顿迭代求解,因为 h ( g ( x ) ) g ( x ) 的系数为1,迭代会产生恒等式。

对等式两侧求导:

g ( x ) f ( x ) f ( x )

g ( x ) f ( x ) f ( x ) d x + C ( m o d   x n )

注意求积分 C = 0 后默认 f [ 0 ] = 1 ,所以 f [ 0 ] 1 时要特殊处理 C (代入任意值计算处理)。

代码:

inline void getln(int n,int *f,int *g)
{
    int i,j,len=1,L=0;
    for(;len<n+n;len<<=1) L++;
    static int der[N],nv[N];
    for(i=0;i<len;++i) der[i]=nv[i]=0;
    getderivative(n,f,der);
    getinverse(n,f,nv);
    for(i=1;i<len;++i) rv[i]=((rv[i>>1]>>1)|(i&1)<<(L-1));
    NTT(nv,1,len);NTT(der,1,len);
    for(i=0;i<len;++i) der[i]=mul(der[i],nv[i]);
    NTT(der,0,len);
    getintegration(n,der,g);
}

exp

给定 f ( x ) ,求 g ( x ) 满足 e f ( x ) g ( x ) ( m o d   x n )

倍增(递归分治)求解。求exp只能用牛顿迭代了。

考虑取 l n 变形:

l n ( g ( x ) ) f ( x ) ( m o d   x n )

底层为 g [ 0 ] = e f [ 0 ] (一般情况下默认 f ( x ) 常数项为0)。

h ( g ( x ) ) = l n ( g ( x ) ) f ( x ) = 0

g n + 1 ( x ) g n x l n ( g n ( x ) ) f ( x ) 1 g n ( x ) ( m o d   x 2 n + 1 ) )

g n + 1 ( x ) g n ( x ) ( 1 l n ( g n ( x ) ) + f ( x ) ) ( m o d   x 2 n + 1 )

代码:

inline void getexp(int n,int *f,int *g)
{
    if(n==1) {g[0]=1;return;}
    getexp((n+1)>>1,f,g);
    int i,j,L=0,len=1;
    for(;len<n+n;len<<=1) L++;
    static int ln[N];
    for(i=0;i<len;++i) ln[i]=0;
    getln(n,g,ln); 
    for(i=0;i<n;++i) ln[i]=dc(f[i],ln[i]);
    ln[0]=ad(ln[0],1);
    NTT(ln,1,len);NTT(g,1,len);
    for(i=0;i<len;++i) g[i]=mul(g[i],ln[i]); 
    NTT(g,0,len);
    for(i=n;i<len;++i) g[i]=0;
}

快速幂

给定 f ( x ) , k ,求 g ( x ) 满足 g ( x ) f k ( x ) ( m o d   x n )

考虑取 l n 变形,再次进行降幂:

l n ( g ( x ) ) k l n ( f ( x ) ) ( m o d   x n )

故:

g ( x ) e k l n ( f ( x ) ) ( m o d   x n )

代码:

inline void getpow(int n,int *f,int *g,int K)
{
    int i,j,L=0,len=1;
    getln(n,f,g);
    for(;len<n;len<<=1) L++;
    for(i=0;i<len;++i) g[i]=mul(g[i],K);
    for(i=0;i<n;++i) f[i]=0;
    getexp(n,g,f);
}

三角函数

给定 P ( x ) ,求 X ( x ) , Y ( x ) 满足

cos P ( x ) X ( x ) 0 ( mod x n )

sin P ( x ) Y ( x ) 0 ( mod x n )

由欧拉公式:

e i P ( x ) = c o s P ( x ) + i s i n P ( x )

所以可以用复数二元组,实部表示 X ( x ) ,虚部表示 Y ( x )

X ( x ) + i Y ( x ) = e i P ( x )

求一下 e x p 分别对应。

貌似可以 F F T 直接算,但应用程度不高。

所以是理论可行。代码?唔,不存在的(留坑)。


除法&取模

多项式除法与取模


形式幂级数&拉格朗日反演

给定 f ( x ) , k ,求 g ( x ) k 次项系数 g [ k ] ,满足 f ( g ( x ) ) = x ( m o d   x n )

形式幂级数:

对于一个多项式,仅有有限项的系数是非零的。而对于形式幂级数,则去掉这一限制,将项数与系数推广到无穷大:

F ( x ) = i = 0 a i x i

其中系数 a i 均为环 R [ x ] (可以是实数/复数/整数/模意义下剩余类)中的元素,为方便表示,设 [ x n ] 表示 F ( x ) x n 的系数。所有的形式幂级数构成形式幂级数环 R [ [ x ] ]

拉格朗日反演:

F ( x ) , G ( x ) R [ [ x ] ] F ( G ( x ) ) = x ,则

[ x n ] G ( x ) = 1 n [ x 1 ] 1 F n ( x )

F ( x ) G ( x ) 的复合逆。

特别的,若 F ( x ) = x ϕ ( x ) ,则:

[ x n ] G ( x ) = 1 n [ x n 1 ] ϕ n ( x )

F ( x ) = x ϕ ( x ) ,得 ϕ ( x ) = x F ( x ) ,代入上式:

[ x n ] G ( x ) = 1 n [ x n 1 ] ( x F ( x ) ) n

进一步拓展:

[ x n ] h ( g ( x ) ) = 1 n [ x n 1 ] h ( x ) ( x f ( x ) ) n (三层嵌套)

证明详见拉格朗日反演- hjuy134

代码:

inline int lagrange(int n,int *F,int *G,int k)
{
    static int res[N];
    getinverse(n,F,res);
    for(i=n-1;i>=1;--i) res[i]=res[i-1];
    res[0]=0; 
    getpow(n,res,G,k);
    return mul(G[k-1],fastpow(k,mod-2)); 
}

时间复杂度总结

原文戳这里

内容 时间复杂度 常数
卷积 O ( n l o g n ) 3
求逆 O ( n l o g n ) 6
开根 O ( n l o g n ) 18
求导 O ( n ) 1
积分 O ( n ) 1
ln O ( n l o g n ) 9
exp O ( n l o g n ) 24
快速幂 O ( n l o g n ) 33

求值&插值


多点求值

给定 f ( x ) 以及点集 X = { x 1 , x 2 , . . . , x n } ,求 f ( x 1 ) , f ( x 2 ) , . . . , f ( x n )

倍增(递归分治)求解。

考虑底层:由拉格朗日插值法式,得到 F ( x i ) = F ( x ) ( m o d   ( x x i ) ) ( x i )

观察到 f ( x ) = f ( x ) m o d ( i = 1 n ( x x i ) ) ,满足 f ( x ) = f ( x ) ( 1 i n )

因为 f ( x ) 在可取范围 d e g ( f ( x ) ) n 1 内的答案都是唯一的,所以可以保留到这 n 个点的

最简拉格朗日插值式化出的多项式( d e g ( f ) = n 1 ),而不会影响答案,而这样的一个式子必然仅

存在于 i = 1 n ( x x i ) (最高项次幂为 n > d e g ( f ) )的剩余类中,故等式成立。

那么考虑分治处理,设:

X 0 = { x 0 , x 1 , . . . , x n 2 } X 1 = { x n 2 + 1 , x n 2 + 2 , . . . , x n }

g 0 ( x ) = i = 1 n 2 ( x x i ) g 1 ( x ) = i = n 2 + 1 n ( x x i )

显然对于 x i X 0 g 0 ( x i ) = 0 ;对于 x i X 1 g 1 ( x i ) = 0

于是每次二分递归,将两边连乘积合并即可,先预处理出所有递归中取模后的多项式,再递归处理。

靠谱的做法是小于一定数量时暴力算,因为多点求值的常数太大了(复杂度 O ( n l o o o o o o g 2 n ) )。

代码:

#define mid (((l)+(r))>>1)
inline void prework(int dep,int l,int r,int *x,int *consmul)//预处理连乘积
{
    if(l==r){
        consmul[dep][l<<1]=mod-x[l];
        consmul[dep][l<<1|1]=1;//递归底部:x-xi
        return;
    }
    prework(dep+1,l,mid,x,consmul);
    prework(dep+1,mid+1,r,x,consmul);
    getmul(consmul[dep+1]+(l<<1),mid-l+1,consmul[dep+1]+((mid+1)<<1),r-mid,consmul[dep]+(l<<1),r-l+1);
}

inline void getval(int dep,int l,int r,int *f,int *y,int *consmul)
{
    static int modulo[D][N];//记录取模后多项式
    getmod(f,r-l,consmul[dep]+(l<<1),r-l+1,modulo[dep]+l);
    if(l==r){y[l]=modulo[dep][l];return;}
    getval(dep+1,l,mid,modulo[dep]+l,y,consmul);
    getval(dep+1,mid+1,r,modulo[dep]+mid+1,y,consmul);
}

inline void getval(int *f,int *x,int n,int *y)
{
    static int consmul[D][N<<1];//记录连乘积
    prework(0,1,n,x,dc);
    getval(0,1,n,f,y,dc);
}

注:此代码没有提交过,不保证正确性。


快速插值(牛顿插值&拉格朗日优化)

给定二元组集合 P = { ( x 1 , f ( x 1 ) ) , ( x 2 , f ( x 2 ) , . . . , ( x n , f ( x n ) ) } ,求 f ( x ) ( d e g ( f ) < n )

首先介绍一种不快速但很好手算的方法,牛顿插值(增量法):

g n ( x ) = i = 1 n ( x x i )

设前 n 个点插值得到 f n ( x ) ,考虑 f n + 1 ( x ) :

f n + 1 ( x ) f n ( x ) ( m o d   g ( n ) )

f n + 1 ( x ) = f n ( x ) + k g ( n )

代入 x = x n + 1 ,解出 k ,即可求出 f n + 1 ( x ) 。复杂度 O ( n 2 )

现在来考虑一种复杂度 O ( n l o o o o o o o o o g 2 n ) 快速插值:

观察拉格朗日插值式:

f ( x ) = i = 1 n f ( x i ) j = 1 , j i n x x j x i x j

通过化简优化一下这个 O ( n 2 ) 的式子:

f ( x ) = i = 1 n f ( x i ) j = 1 , j i n ( x i x j ) j = 1 , j i n ( x x j )

g ( x ) = i = 1 n ( x x i ) g i ( x ) = g ( x ) x x i

显然 j = 1 , j i n ( x i x j ) = g i ( x )

g ( x ) = ( x x i ) g i ( x ) 巧妙地求导(利用洛必达法则)后发现:

g ( x ) = g i ( x ) + ( x x i ) g ( x )

故: g i ( x i ) = g ( x i )

g i ( x i ) 变成了 g ( x ) 的多点求值。

那么 1 j = 1 , j i n ( x i x j ) 就可以快速求出了,而 j = 1 , j i n ( x x j ) = ( x x i ) j = 1 n ( x x j ) ,直接用分治 F F T 求出连乘积(类似多点求值中的预处理)即可。最后 O ( n ) 合并答案。

代码:

#define mid (((l)+(r))>>1)
inline void getinterpolation(int dep,int l,int r,int *val,int *consmul,int *f)
{
    if(l==r) {f[l]=val[l];return;}
    getinterpolation(dep+1,l,mid,val,consmul,f);
    getinterpolation(dep+1,mid+1,r,val,consmul,f);
    static int g[N],h[N];
    getmul(f+l,mid-l,consmul[dep+1]+((mid+1)<<1),r-mid,g,r-l);
    getmul(f+mid+1,r-mid-1,consmul(dep+1)+(l<<1),l-mid+1,h,r-l);
    //类似线段树与另一子节点合并过程,每一位乘上除自己位以外的所有连乘积
    //注意总次数是r-l,不是r-l+1,本区间向乘上另一区间时次数会少一位(少乘了自己)
    getadd(g,h,f+l,r-l);//合并
}

inline void getinterpolation(int *x,int *y,int n,int *f)
{
    static int consmul[D][N<<1],res[N],val[N];
    prework(0,1,n,x,consmul);//预处理连乘积
    getderivative(n,consmul[0],res);//处理g'(x)
    getval(res,x,n,val);//多点求值得到gi(xi)
    for(int i=1;i<=n;++i)
     val[i]=mul(y[i],inv[val[i]]);
    getinterpolation(0,1,n,val,consmul,f);//最后乘上式子右边一部分
}

注:此代码没有提交过,不保证正确性。


参考资料

膜膜膜众神犇。也非常感谢能有这么多这么好的资源。

如果有错误欢迎指出,有疑问欢迎评论。

猜你喜欢

转载自blog.csdn.net/corsica6/article/details/81951788