多项式

系数表示法与点值表示法

系数表示法:\(f(x)=a_0x^0+a_1x^1+a_2x^2+a_3x^3+a_4x^4+a_5x^5\)

点值表示法:一个 \(n\) 次多项式由 \(n+1\) 个点 \((x_i,f(x_i))\) 表示。

为何 \(n+1\) 个点可以确定一个 \(n\) 次多项式?

写成矩阵形式:

\[\begin{bmatrix} a_0 & a_1 & a_2 & a_3 \end{bmatrix} \times \begin{bmatrix} {x_0}^0 & {x_1}^0 & {x_2}^0 & {x_3}^0\\ {x_0}^1 & {x_1}^1 & {x_2}^1 & {x_3}^1\\ {x_0}^2 & {x_1}^2 & {x_2}^2 & {x_3}^2\\ {x_0}^3 & {x_1}^3 & {x_2}^3 & {x_3}^3\\ \end{bmatrix} = \begin{bmatrix} f(x_0) & f(x_1) & f(x_2) & f(x_3) \end{bmatrix} \]

\(x_i\) 确定且互不相同时,中间那个矩阵是范德蒙德矩阵,矩阵可逆,所以点值表示法和系数表示法可以相互转换。

FFT

FFT 的基本思想

  • 系数表示法 \(\rightarrow\) 点值表示法(DFT)
  • 将两个多项式点值相乘(点积)
  • 点值表示法 \(\rightarrow\) 系数表示法(IDFT)

点的选择

我们当然可以随意选择 \(n+m+1\) 个点,然后代入求值,但是那样是 \(n^2\) 的,我们需要寻找一些特殊的点来简化计算。

我们选取 \(k=2^p\) 个点,满足 \(2^p\geq n+m+1\),点 \(x_i={w_k}^i\),其中 \({w_k}^1\) 表示复平面上将单位圆 \(k\) 等分后逆时针数第 \(i\) 个的向量。

一些需要了解的数学知识,可以通过复数的运算或者在坐标系上旋转证明(下面公式中所有 \(i\) 均为虚数单位)

\(e^{i\pi}=-1\)(欧拉公式)

\(e^{\theta\pi}=\cos\theta+i\sin\theta\)

\({w_k}^p=\cos({p\over k}\times 2\pi)+i\sin({p\over k}\times 2\pi)\)

\({w_k}^{p+{k\over 2}}=-{w_k}^p\)

\(w_k^p=\cos\theta+i\sin\theta\),那么\({1\over w_k^p}=\cos\theta-i\sin\theta\)

求出点值

先看看一个点 \(x\) 如何求值:

\[f(x)=a_0x^0+a_1x^1+a_2x^2+a_3x^3 \]

奇偶分离

\[=a_0x^0+a_2x^2+a_1x^1+a_3x^3 \]

\[=a_0x^0+a_2x^2+x(a_1x^0+a_3x^2) \]

\(g(x)=a_0x^0+a_2x^1\)\(h(x)=a_1x^0+a_3x^1\)

\[f(x)=g(x^2)+xh(x^2) \]

对于多个点的情况,有:

\[f(w_k^i)=g(w_{k\over 2}^i)+w_k^i\times h(w_{k\over 2}^i) \]

\[f(w_k^{i+{k\over 2}})=g(w_{k\over 2}^i)-w_k^i\times h(w_{k\over 2}^i) \]

所以我们可以通过计算 \(g(x),h(x)\) 来得到 \(f(x)\),因此复杂度优化为 \(O(n\log n)\)

非递归实现

逐层处理,递推出 rev 数组表示反转二进制位后的结果。

IDFT

\(f(x)=g(x^2)+xh(x^2)\) 改为 \(f(x)=g(x^2)+{1\over x}h(x^2)\),最后把结果除以 \(2^p\) 即可。

NTT

原根

设原根为 \(g\),模数为 \(p\)

  • 原根的性质

    对于 \(i\in [0,p),g^i \bmod p\) 互不相同

  • 原根的判定

    \(\gcd(g,p)=1\),且 \(g^i\equiv 1\) 的最小解为 \(\varphi(p)\)(欧拉定理)

  • 原根存在定理

    原根存在当且仅当 \(p=2,4,c^a,2c^a\),其中 \(c\) 为奇质数,\(a\in N^{*}\)

  • 判定方法

    找出 \(\varphi(p)\) 的质因数 \(p_i\),如果所有 \(p_i\) 都满足 \(g^{\varphi(p)\over p_i}\not\equiv 1\),那么 \(g\) 是原根。

  • 寻找最小原根

    最小的原根在 \(O(n^{0.25})\) 级别,直接暴力即可。

单位根的改变

和 FFT 类似,NTT 也需要寻找单位根。

设质数 \(p=a\times 2^b+1\),原根为 \(g\)

我们发现 \(g^{(p-1)\over k}\) 具有和 \(w_k\) 相同的性质,可以拿来做单位根。

将 FFT 中所有运算在剩余系下进行即可。

常见模数

\(998244353=479\times 2^{21}+1,g=3\)注意:该模数能够处理的最大数据范围是 \(n+m\leq 2\times 10^6\)

\(1004535809=7\times 17\times 2^{23}+1,g=3\)

多项式求逆

采用倍增实现,递归公式为:

\[g\prime(x)\equiv 2g(x)-f(x)g^2(x)\quad (\bmod \ x^n) \]

\(g\prime(x)\) 为新的答案数组,次数为 \(n-1\)\(g(x)\) 为原来的答案数组。

常数

计算常数时应当把 \(n\) 视为 \(2^{\lceil \log n\rceil}\),常数按照取模次数算。

FFT \(n=10^6,m=10^6\) 用时 \(1s\)

NTT 折算 \(4.5\) 倍常数(取 \((n+m)\log(n+m)\) 的倍数计算),\(n=2\times 10^6,m=2\times 10^6\) 用时 \(1s\)

多项式求逆 折合 \(27\) 倍常数,\(n=10^6\) 用时 \(1s\)

NTT 和爆乘比较

\(n\times n \rightarrow 2n\)\(n=180\) 时二者相当。

\(n\times n \rightarrow n\)\(n=360\) 时二者相当。(使用最新的优化,可以做到 \(n=500\) 时相当)

\(n\times n \rightarrow 2n\) 和 三模 NTT,\(n=450\) 时二者相当。

\(n^{1.584}\) 乘法 和 三模 NTT,\(n=2048\) 时二者相当。

\(n^2\) 的多项式操作

https://www.cnblogs.com/tzcwk/p/dxs-sqr.html

代码

  • FFT
#include<bits/stdc++.h>
using namespace std;
#define int long long
struct cp{
	double x,y;
};
cp operator +(const cp&n1,const cp&n2){return {n1.x+n2.x,n1.y+n2.y};}
cp operator -(const cp&n1,const cp&n2){return {n1.x-n2.x,n1.y-n2.y};}
cp operator *(const cp&n1,const cp&n2){return {n1.x*n2.x-n1.y*n2.y,n1.y*n2.x+n1.x*n2.y};}
cp operator /(const cp&n1,const int&x){return {n1.x/x,n1.y/x};}
const int N=2.2e6+5;
const double pi=acos(-1);
int n,m;
cp a[N],b[N],c[N];
cp pw[N];
int rev[N];
void dft(cp *a,int op,int n){
	for(int i=0;i<n;i++){
		if(i<rev[i]) swap(a[i],a[rev[i]]);
	}
	for(int k=1;k*2<=n;k<<=1){
		for(int i=0;i<k;i++){
			pw[i]={cos(i*pi/k),sin(i*pi/k)};
			if(op) pw[i].y=-pw[i].y;
		}
		for(int i=0;i<n;i+=k*2){
			for(int j=0;j<k;j++){
				cp g=a[i+j],h=pw[j]*a[i+j+k];
				a[i+j]=g+h;
                a[i+j+k]=g-h;
			}
		}
	}
	if(op){
		for(int i=0;i<n;i++) a[i]=a[i]/n;
	}
}
void fft(cp *a,cp *b,cp *c,int n,int m){
	int P=1,D=0;
	while(P<n+m+1) P<<=1,D++;
	for(int i=0;i<P;i++){
		rev[i]=(rev[i>>1]>>1)|((i&1)<<(D-1));
	}
	dft(a,0,P);dft(b,0,P);
	for(int i=0;i<P;i++) c[i]=a[i]*b[i];
	dft(c,1,P);
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=0;i<=n;i++){
		int x;cin>>x;a[i]={x,0};
	}
	for(int i=0;i<=m;i++){
		int x;cin>>x;b[i]={x,0};
	}
	fft(a,b,c,n,m);
	for(int i=0;i<=n+m;i++) cout<<(int)(c[i].x+0.5)<<' ';
	return 0;
}
  • NTT
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=998244353;
const int N=1.1e6+5;
int qpow(int a,int x){
	int sum=1;
	while(x){
		if(x&1) sum=sum*a%mod;
		a=a*a%mod;
		x>>=1;
	}return sum;
}
inline int Add(int x){return x>=mod? x-mod: x;}
inline int Dec(int x){return x<0? x+mod: x;}
inline int Ba(int x){
	const static __int128 bar=(__int128(1)<<64)/mod;
	return x-(x*bar>>64)*mod;
}
namespace NTT{
	const int g[2]={3,qpow(3,mod-2)};
	int h[N],rev[N];
	int P,D;
	void dft(int a[],int op,int n){
		for(int i=0;i<n;i++){
			if(i<rev[i]) swap(a[i],a[rev[i]]);
		}
		for(int k=1;k*2<=n;k<<=1){
			int bs=qpow(g[op],(mod-1)/k/2);
			h[0]=1;
			for(int i=1;i<k;i++) h[i]=h[i-1]*bs%mod;
			for(int i=0;i<n;i+=k*2){
				for(int j=i;j<i+k;j++){
					int A=a[j],B=Ba(h[j-i]*a[j+k]);
					a[j]=Add(A+B);
					a[j+k]=Dec(A-B);
				}
			}
		}
		if(op){
			int iv=qpow(n,mod-2);
			for(int i=0;i<n;i++) a[i]=(a[i]*iv%mod+mod)%mod;
		}
	}
	void ntt(int a[],int b[],int c[],int n,int m){
		for(int i=0;i<=n;i++) a[i]=(a[i]%mod+mod)%mod;
		for(int i=0;i<=m;i++) b[i]=(b[i]%mod+mod)%mod;
		P=1;D=0;
		while(P<=n+m+1) P<<=1,D++;
		for(int i=0;i<P;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<(D-1));
		dft(a,0,P);dft(b,0,P);
		for(int i=0;i<P;i++) c[i]=a[i]*b[i]%mod;
		dft(c,1,P);
	}
}
int a[N],b[N],c[N];
int n;
signed main(){
	NTT::ntt(a,b,c,n,n);
	return 0;
}
  • \(n^{1.584}\) 乘法
const int S=4e7+5;
int PP[S];
int *make(int n){
	static int now=0;
	if(n<0){now=0;return 0;}
	memset(PP+now,0,sizeof(int)*n);
	now+=n;
	assert(now<S);
	return PP+now-n;
}
void cdq(int *a,int *b,int *c,int n){
	static unsigned long long cc[100];
	if(n<=16){
		for(int i=0;i<n*2;i++) cc[i]=0;
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++) cc[i+j]+=a[i]*b[j];
		}
		for(int i=0;i<n*2;i++) c[i]+=cc[i]%mod;
		return;
	}
	int m=n/2;
	int *f3=make(m),*f4=make(m);
	int *g1=make(n),*g3=make(n);
	for(int i=0;i<m;i++) f3[i]=Add(a[i]+a[i+m]),f4[i]=Add(b[i]+b[i+m]);
	cdq(a,b,g1,m);cdq(f3,f4,c+m,m);cdq(a+m,b+m,g3,m);
	for(int i=0;i<n;i++) c[i]+=g1[i],c[i+n]+=g3[i],c[i+m]+=-g1[i]-g3[i];
}
void cdq(){
	for(int i=0;i<n*2;i++) c[i]=0;
	make(-1);
	cdq(a,b,c,P);
	for(int i=0;i<n*2;i++) c[i]=(c[i]%mod+mod)%mod;
}
posted @ 2025-05-09 11:41  born_to_sun  阅读(40)  评论(0)    收藏  举报