多项式算法学习笔记
定义
形如 的式子被称为多项式。定义多项式中每个单项式叫做这个多项式的项,定义多项式的次数为项的最高次数。显然 个点可以唯一确定一个次数为 的多项式。
接下来我们首先来看一下多项式的基本运算怎么实现(要不然谁用啊)
多项式加减
直接对齐相同次数然后系数直接加减就可以了吧。。。时间复杂度 ,难度:普及组 T1(虽然这个级别已经不存在了233)
多项式乘法
首先我们考虑一下我们平时如何手算多项式算法。显然我们都是 暴力算的,所以就有了一个枚举所有可能的二元组然后统计答案的暴力做法。
当然在常规题目中我们会使用一种叫做 FFT(快速傅里叶变换) 的 的优秀算法来计算,详情请查看这篇文章。
哈哈你一定学完 FFT 然后回来了!我们来考虑一下如果多项式乘法在模意义下怎么做。
有一种叫做 NTT (快速数论变换)的东西,它把每次我们做快速傅里叶变换时用到的 重新定义了一下。大概是令 为模数 的原根,我们就可以取 。然后 IDFT 的时候直接乘逆元就可以了。
至于这个原根怎么找。。。一般你碰到模数为 的时候就想下可不可以 NTT,因为 是一个常用的 NTT 模数,一个原根是 。
中心代码大致如下:
inline void NTT(int *A,int limit,int opt){ FOR(i,0,limit-1) if(i < r[i]) std::swap(A[i],A[r[i]]); for(int mid = 1;mid < limit;mid <<= 1){ int W = qpow(3,(ha-1)/(mid<<1)); if(opt == -1) W = qpow(W,ha-2); for(int j = 0;j < limit;j += (mid<<1)){ for(int k = 0,w = 1;k < mid;k++,w = w*W%ha){ int x = A[j+k],y = w*A[j+mid+k]%ha; A[j+k] = (x+y)%ha;A[j+mid+k] = (x-y+ha)%ha; } } } if(opt == -1){ // IDFT int inv = qpow(limit,ha-2); FOR(i,0,limit-1) A[i] = A[i]*inv%ha; } }
好了接下来才是关于多项式的一些基本的稍微硬核一点的操作。
确定多项式
上面提到了 个点可以唯一确定一个次数为 的多项式。
现在给定 个点,请你确定这个多项式并且将 代入求值。
做法一:高斯消元
我们暴力建方程然后暴力高斯消元!。
什么你连高斯消元都不会?题目链接
其实就是暴力解方程啦~奉上高斯消元模板题的代码:
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define fi first #define lc (x<<1) #define se second #define U unsigned #define rc (x<<1|1) #define Re register #define LL long long #define MP std::make_pair #define CLR(i,a) memset(i,a,sizeof(i)) #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const double EPS = 1e-7; const int MAXN = 200+5; double d[MAXN][MAXN],ans[MAXN]; int n; int main(){ scanf("%d",&n); FOR(i,1,n) FOR(j,1,n+1) scanf("%lf",&d[i][j]); FOR(i,1,n){ int r = i; FOR(j,i+1,n) if(std::fabs(d[r][i]) < std::fabs(d[j][i])) r = j; if(std::fabs(d[r][i]) < EPS){ puts("No Solution"); return 0; } if(i != r) std::swap(d[r],d[i]); double div = d[i][i]; FOR(j,i,n+1) d[i][j] /= div; FOR(j,i+1,n){ div = d[j][i]; FOR(k,i,n+1) d[j][k] -= d[i][k]*div; } } ans[n] = d[n][n+1]; ROF(i,n-1,1){ ans[i] = d[i][n+1]; FOR(j,i+1,n) ans[i] -= (d[i][j]*ans[j]); } FOR(i,1,n) printf("%.2f\n",ans[i]); return 0; }
做法二:拉格朗日插值
背下式子:
设该多项式为 ,第 个点的坐标为 ,需要求在 点的取值。
证明显然:因为这个式子满足了如果代入 就肯定会返回 。根据 个点唯一确定一个多项式,就可以知道这个多项式等价于原多项式了。
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define fi first #define lc (x<<1) #define se second #define U unsigned #define rc (x<<1|1) #define Re register #define LL long long #define MP std::make_pair #define CLR(i,a) memset(i,a,sizeof(i)) #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 2000+5; const int ha = 998244353; inline int qpow(int a,int n=ha-2){ int res = 1; while(n){ if(n & 1) res = 1ll*res*a%ha; a = 1ll*a*a%ha; n >>= 1; } return res; } int n,k,x[MAXN],y[MAXN],ans; int main(){ scanf("%d%d",&n,&k); FOR(i,1,n) scanf("%d%d",x+i,y+i); FOR(i,1,n){ int s1 = 1,s2 = 1; FOR(j,1,n){ if(j == i) continue; s1 = 1ll*s1*((1ll*k-x[j]+ha)%ha)%ha;s2 = 1ll*s2*((1ll*x[i]-x[j]+ha)%ha)%ha; } s2 = qpow(s2);s1 = 1ll*s1*s2%ha*y[i]%ha; ans = (ans+s1)%ha; } printf("%d\n",ans); return 0; }
扩展阅读:维基百科-拉格朗日插值
多项式求逆
题目链接
给定一个多项式 ,让你求出一个多项式 ,满足
我们可以考虑倍增地构造多项式 。首先我们把这个式子进行一些化简。
不难发现最后一个式子的范围扩大到原来的 倍,并且又凑成了一开始的形式。
我们考虑迭代处理,首先当规模为 时显然答案系数就是对应系数的逆元。
我们设对于规模为 的问题得到的多项式为 ,我们想要推出扩大一倍规模的答案 ,根据上面的推导,可以有
所以我们直接两遍 NTT 就能扩大一倍的问题规模了。
时间复杂度:。
这里就只给迭代写法啦 qwq 貌似迭代确实比递归快很多呢。
inline void Inv(int *A,int *B,int n){ B[0] = qpow(A[0],ha-2); int len; for(len = 1;len < (n<<1);len <<= 1){ int d = len<<1; FOR(i,0,len-1) a[i] = A[i],b[i] = B[i]; FOR(i,0,d-1) pos[i] = (pos[i>>1]>>1)|((i&1)?len:0); NTT(a,d,1);NTT(b,d,1); FOR(i,0,d-1) B[i] = ((2-a[i]*b[i]%ha+ha)%ha*b[i]%ha)%ha; NTT(B,d,-1); FOR(i,len,d-1) B[i] = 0; } FOR(i,0,len-1) a[i] = b[i] = 0; FOR(i,n,len-1) B[i] = 0; }
多项式除法
题目链接
给定一个 次多项式 和一个 次多项式 ,求出两个多项式 ,满足:
- 次数为 , 次数小于 。
- 。
显然如果能整除的话我们直接求个逆就可以了。那现在有余数怎么办啊 Orz.......
别急,我们首先定义一种操作 R(reverse),满足:。
不难看出 操作的本质是多项式翻转。所以我们就可以 完成这个操作。
现在我们尝试对我们要构造的东西做一些改变。
我们考虑两边对 取模。
我们求一遍 的逆,然后用 NTT 乘起来,就可以得到 了,最后直接计算出来余数就可以了。
时间复杂度瓶颈在于多项式求逆,所以是 。
代码就不贴了。就是上面的一些简单运用。多项式求导/积分
都是非常简单的东西了。
根据求导的加法法则和幂函数可以轻松得到:
补充一些同样适用于多项式的求导基本法则:
写这些主要是为了介绍后面的牛顿迭代
多项式牛顿迭代
处理多项式复合的高级神器 Orz
问题大概是这样的:给出 ,求 满足 。
首先为了让像我这样萌萌哒的弱校初中选手也能看懂,我决定放一些前置芝士。
前置芝士1:Taylor 展开
这个大家都会 qwq。
Taylor 展开(泰勒展开)是一种用函数在某点的信息描述其附近取值的公式,也就是拟合某点及其周围的取值啦qwq。
具体来说我们从头开始构造这个拟合函数,设我们要使 及其周围一段区间拟合这个原函数。首先我们可以使得我们构造出的函数和待拟合的函数在 的取值相等。但是发现这只有一个值?一点也不像怎么办?我们可以使得他们的二阶倒数,三阶导数.....一直相等下去,然后我们就会发现这两个函数在那附近神奇的重合了!我们看个 GIF 来详细的了解这个过程。
所以我们就可以得到 Taylor 展开来拟合多项式的式子了:
其中 表示对原函数图像 这个点进行 阶求导。
如果描述听不懂的话建议大家去看一下上面的视频的...毕竟视频里有图还有可爱的 ~
回到多项式牛顿迭代上
我们考虑使用和多项式求逆一样的 trick:倍增来构造答案多项式。
首先当 时,,这个就是要单独求出来的。
现在我们假设已经求出了
考虑如何扩展到 下,我们把 放到 上做 Taylor 展开
因为倍增构造,我们可以发现 和 后 项相同,所以 的最低非 项次数大于 。所以可以得到
然后因为我们要求的 需要满足 ,所以可以推一波式子:
按照多项式求逆那样的套路迭代计算就可以了,我们来看一下一些经典的题目。
多项式开方
题目链接
给定一个多项式 ,要求求出一个多项式 ,满足
简单的变换:。
设 ,则 ,使用牛顿迭代法:
然后就可以直接迭代计算了。时间复杂度 。
多项式求 ln
题目链接
给定一个多项式 ,求一个多项式 ,满足 。
我们设 ,定义 。
那么显然 。
那么求逆就可以算出 。然后因为求导和不定积分互逆,所以对 求一个不定积分就可以了。
代码不贴了(主要是后面有大杂烩)
多项式求 exp
题目链接
多项式大杂烩,练习之前学的怎么样了qwq
给定一个多项式 ,要你求出一个 ,满足 。
我们考虑计算出
变形一下可得
设 ,然后直接套用牛顿迭代方法:
首先求导:得到
然后代入牛顿迭代公式,得到
因为 ,所以 的常数项是 。(有些牛顿迭代题目要用二次剩余算系数但是我还没接触过QAQ)
然后就是板子大全了qwq
放一下代码吧:
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define Re register #define LL long long #define U unsigned #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------\n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl #define int LL const int MAXN = 5e5 + 5; const int ha = 998244353; inline int qpow(int a,int n){ int res = 1; while(n){ if(n & 1) res = res*a%ha; a = a*a%ha; n >>= 1; } return res; } int a[MAXN],b[MAXN],c[MAXN],d[MAXN],r[MAXN],g[MAXN],f[MAXN],x[MAXN],n; inline void NTT(int *A,int limit,int opt){ FOR(i,0,limit-1) if(i < r[i]) std::swap(A[i],A[r[i]]); for(int mid = 1;mid < limit;mid <<= 1){ int W = qpow(3,(ha-1)/(mid<<1)); if(opt == -1) W = qpow(W,ha-2); for(int j = 0;j < limit;j += (mid<<1)){ for(int k = 0,w = 1;k < mid;k++,w = w*W%ha){ int x = A[j+k],y = w*A[j+mid+k]%ha; A[j+k] = (x+y)%ha;A[j+mid+k] = (x-y+ha)%ha; } } } if(opt == -1){ int inv = qpow(limit,ha-2); FOR(i,0,limit-1) A[i] = A[i]*inv%ha; } } inline void Inv(int *A,int *B,int n){ B[0] = qpow(A[0],ha-2); int len; for(len = 1;len < (n<<1);len <<= 1){ int d = len<<1; FOR(i,0,len-1) a[i] = A[i],b[i] = B[i]; FOR(i,0,d-1) r[i] = (r[i>>1]>>1)|((i&1)?len:0); NTT(a,d,1);NTT(b,d,1); FOR(i,0,d-1) B[i] = (2-a[i]*b[i]%ha+ha)%ha*b[i]%ha; NTT(B,d,-1);FOR(i,len,d-1) B[i] = 0; } FOR(i,0,len-1) a[i] = b[i] = 0; FOR(i,n,len-1) B[i] = 0; } inline void Direv(int *A,int *B,int n){ FOR(i,1,n-1) B[i-1] = i*A[i]%ha;B[n-1] = 0; } inline void Inter(int *A,int *B,int n){ FOR(i,1,n-1) B[i] = A[i-1]*qpow(i,ha-2)%ha;B[0] = 0; } inline void getln(int *A,int *B,int n){ int *C = c,*D = d; Direv(A,C,n);Inv(A,D,n); int len = 0,limit = 1; while(limit <= n) limit <<= 1,len++; FOR(i,1,limit-1) r[i] = (r[i>>1]>>1)|((i&1)<<(len-1)); NTT(C,limit,1);NTT(D,limit,1); FOR(i,0,limit-1) C[i] = C[i]*D[i]%ha; NTT(C,limit,-1);Inter(C,B,n); } inline void Exp(int *A,int *B,int n){ B[0] = 1;int len; for(len = 1;len < (n<<1);len <<= 1){ int d = len<<1; getln(B,f,len); FOR(i,0,len-1) f[i] = (A[i]+(i==0)-f[i]+ha)%ha; FOR(i,0,d-1) r[i] = (r[i>>1]>>1)|((i&1)?len:0); NTT(B,d,1);NTT(f,d,1); FOR(i,0,d-1) B[i] = B[i]*f[i]%ha; NTT(B,d,-1); FOR(i,len,d-1) B[i] = 0; } FOR(i,0,len-1) f[i] = 0;FOR(i,n,len-1) B[i] = 0; } signed main(){ scanf("%lld",&n); FOR(i,0,n-1) scanf("%lld",x+i); int len;for(len=1;len<=n;len<<=1);Exp(x,g,len); FOR(i,0,n-1) printf("%lld ",g[i]); return 0; }