多项式

1.FFT

复数

复数乘法的运算法则是模长相乘,幅角相加。
在单位圆中(模长为1),复数相乘只会幅角转动。
定义单位根 \(w_n\),表示转 \(\frac{1}{n}\) 圆的角。
满足 \((w_n)^n=1\)

单位根的 \(0\sim n-1\) 次幂均分整个圆,即满足 \((w_n^i)^n=1\).
它们互不相同。

引入

我们现在要求多项式乘法。
\(C=A\times B\).

1.取若干个数 \(1,w_n,w_n^2....,w_n^{n-1}\).
2.对这些数求出 \(A(w_n^i),B(w_n^i)\),这一步称为 DFT.
3.求出 \(C(w_n^i)=A(w_n^i)\cdot B(w_n^i)\).
4.插值求出 \(C\) 的系数,这一步称为 IDFT.

DFT

给出 \(A\) 函数,求 \(A(w_n^i)\).
\(n\) 补成 \(2\) 的整数次幂。

分治:将 \(A\) 拆成奇偶两部分
\(A(x)=A_0(x^2)+xA_1(x^2)\)
我们往下分治,假设我们已知 \(A_0(w_{n/2}^i),A_1(w_{n/2}^i)\) 的值。
我们可以发现,\(A(w_n^i)=A_0(w_n^{2i})+w_n^i A_1(w_n^{2i})\).
\(A(w_n^i)=A_0(w_{n/2}^{i})+w_n^i A_1(w_{n/2}^{i})\)
就变成子问题求解。

IDFT

给出 \(C\) 函数在 \(w_n^i\) 的取值,求 \(C\) 的系数。
只需要把 \(w_n^i\) 的取值看成一个多项式,再求出 \(w_n^{-i}\) 的取值,最后除以 \(n\) 即为答案。
笔者尚且无法证明。
上述过程和 DFT 并无二致,只是一个符号的区别。
这样我们求出了多项式乘法。

卡常

我们设一个函数 \(f=A+Bi\).
DFT,再求出 \((f(w_n^i))^2\).
再 IDFT,求出 \(G=(A+Bi)^2=A^2-B^2+2ABi\)
只需取出 \(G\) 的虚部再除以 \(2\) 就是答案。
这样只需一次 DFT 和 IDFT,比原来少了一次 DFT.

实现

正常的写法,由于数组拷贝,会比较慢。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const double pi=acos(-1);
int read() {
  char ch=0;
  while(ch<48||ch>57) ch=getchar();
  return ch-'0';
}
struct CP {
	double a,b;
	CP operator + (const CP x) const {return (CP){a+x.a,b+x.b};}
	CP operator - (const CP x) const {return (CP){a-x.a,b-x.b};}
	CP operator * (const CP x) const {return (CP){a*x.a-b*x.b,x.b*a+b*x.a};}
};
void dft(vector<CP> &F) {
	if(F.size()==1) return ;
	int n=F.size();
	vector<CP> F0,F1;
	F0.resize(n/2); F1.resize(n/2);
	for(int i=0; i<n; i+=2) F0[i/2]=F[i];
	for(int i=1; i<n; i+=2) F1[i/2]=F[i];
	dft(F0); dft(F1);
	CP w=(CP){cos(2*pi/n),sin(2*pi/n)},buf=(CP){1,0};
	for(int i=0; i<n/2; i++) {
		CP tmp=buf*F1[i];
		F[i]=F0[i]+tmp;
		F[i+n/2]=F0[i]-tmp;
		buf=buf*w;
	}
	return ;
}
void idft(vector<CP> &F) {
	int n=F.size();
	dft(F);
	reverse(&F[1],&F[n]);
	for(int i=0; i<n; i++) F[i].a/=n,F[i].b/=n;
}
int n,m,lim;
int main() {
	scanf("%d%d",&n,&m);
	lim=1; while(lim<n+m+1) lim<<=1;
	vector<CP> F; F.resize(lim);
	for(int i=0; i<=n; i++) F[i].a=read();
	for(int i=0; i<=m; i++) F[i].b=read();
	dft(F);
	for(int i=0; i<lim; i++) F[i]=F[i]*F[i];
	idft(F);
	for(int i=0; i<=n+m; i++) {
		int ret=(int)(F[i].b*0.5+0.49);
		printf("%d ",ret);
	}
	return 0;
} 

迭代写法:
我们发现原来的写法需要很多数组拷贝。
如果我们把一个位置的数奇偶变换的最后位置找出,
那么我们就无须拷贝新的数组了。
我们发现一个位置最后在它的二进制反转的位置。
看代码。

code
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+10;
const double pi=acos(-1);
int read() {
	char ch=0;
	while(ch<48||ch>57) ch=getchar();
	return ch-'0';
}
struct CP {
	double a,b;
	CP operator + (const CP x) const {return (CP){a+x.a,b+x.b};}
	CP operator - (const CP x) const {return (CP){a-x.a,b-x.b};}
	CP operator * (const CP x) const {return (CP){a*x.a-b*x.b,x.b*a+b*x.a};}
} F[N];
void dft(CP *F,int n) {
	if(n==1) return ;
	dft(&F[0],n/2); dft(&F[n/2],n/2);
	CP w=(CP){cos(2*pi/n),sin(2*pi/n)},buf=(CP){1,0};
	for(int i=0; i<n/2; i++) {
		CP tmp=buf*F[i+n/2];
		F[i+n/2]=F[i]-tmp;
		F[i]=F[i]+tmp;
		buf=buf*w;
	}
	return ;
}
void idft(CP *F,int n) {
	dft(F,n);
	reverse(&F[1],&F[n]);
	for(int i=0; i<n; i++) F[i].a/=n,F[i].b/=n;
}
int n,m,lim;
int tr[N];
int main() {
	scanf("%d%d",&n,&m);
	lim=1; while(lim<n+m+1) lim<<=1;
	for(int i=0; i<=n; i++) F[i].a=read();
	for(int i=0; i<=m; i++) F[i].b=read();
	for(int i=1; i<lim; i++)
		tr[i]=(tr[i>>1]>>1)|((i&1)?lim>>1:0);
	for(int i=1; i<lim; i++)
		if(i<tr[i]) swap(F[i],F[tr[i]]);
	dft(F,lim);
	for(int i=0; i<lim; i++) F[i]=F[i]*F[i];
	for(int i=1; i<lim; i++)
		if(i<tr[i]) swap(F[i],F[tr[i]]);
	idft(F,lim);
	for(int i=0; i<=n+m; i++) {
		int ret=(int)(F[i].b*0.5+0.49);
		printf("%d ",ret);
	}
	return 0;
} 

2.NTT

原根

如果我们想要提升精度,那么我们可以用取模计算代替复数运算。
对于取模 \(p\),我们发现了原根 \(g\),满足 \(g\)\(1\sim p-1\) 次幂互不相同。
原根的性质与单位根的性质是相似的。
我们可以构造 \(w_n=g^{(p-1)/n}\).
但这要求 \(p-1\) 含有很多 \(2\) 的因子。

常见的原根:\(g_{998244353}=3\).

引入

大体与 FFT 没什么不同。

实现

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int p=998244353,g=3;
int qpow(int a,int b) {
	int res=1;
	for(; b; b>>=1) {
		if(b&1) res=1ll*res*a%p;
		a=1ll*a*a%p;
	}
	return res;
}
void ntt(vector<int> &F,int op) {
	if(F.size()==1) return ;
	int n=F.size();
	vector<int> F0,F1;
	F0.resize(n/2); F1.resize(n/2);
	for(int i=0; i<n; i+=2) F0[i/2]=F[i];
	for(int i=1; i<n; i+=2) F1[i/2]=F[i];
	ntt(F0,op); ntt(F1,op);
	int w=qpow(g,(p-1)/n),buf=1;
	if(op==-1) w=qpow(w,p-2);
	for(int i=0; i<n/2; i++) {
		int tmp=1ll*F1[i]*buf%p;
		F[i]=(tmp+F0[i])%p;
		F[i+n/2]=(-tmp+p+F0[i])%p;
		buf=1ll*buf*w%p;
	}
	return ;
}
int n,m,lim;
int main() {
	scanf("%d%d",&n,&m);
	lim=1; while(lim<n+m+1) lim<<=1;
	vector<int> F,G;
	F.resize(lim); G.resize(lim);
	for(int i=0; i<lim; i++) F[i]=G[i]=0;
	for(int i=0; i<=n; i++) scanf("%d",&F[i]);
	for(int i=0; i<=m; i++) scanf("%d",&G[i]);
	ntt(F,1); ntt(G,1);
	for(int i=0; i<lim; i++) F[i]=1ll*F[i]*G[i]%p;
	ntt(F,-1);
	int inv=qpow(lim,p-2);
	for(int i=0; i<=n+m; i++) printf("%d ",(int)(1ll*F[i]*inv%p));
	return 0;
} 

3.几个模板

任意模数多项式乘法

如果按照原来的值域计算,那么可能会导致运算过程中值超过了 long long 范围。
可以取几个模数来 NTT,最后 CRT,但是需要用到一些科技,不推荐。

我们考虑将一个数拆成 \(a\times base+b\) 的形式,\(base\) 可以取根号值域,取 \(2^{15}\) 即可。
这样最大的值域是可接受的。

对于 \(A,B\) 两个多项式,
最后结果是 \(A_1B_1base^2+(A_1B_2+A_2B_1)base+A_2B_2\).
其中 \(A=A_1base+A_2\)\(B=B_1base+B_2\).

这样要用到总共 8 次 DFT 和 IDFT,常数过大。
我们设 \(P_1=A_1+A_2 i,P_2=A_1-A_2 i,Q=B_1+B_2 i\)
考虑求 \(P_1 Q\)\(P_2 Q\).

\(P_1 Q =A_1B_1-A_2B_2+(A_1B_2+A_2B_1)i\).
\(P_2 Q =A_1B_1+A_2B_2+(A_1B_2-A_2B_1)i\).

两式相加,相减,分离实,虚部,得解。
这样只需 5 次。

code
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+10;
const long double pi=acos(-1);
struct CP {
	long double a,b;
	CP operator + (const CP x) const {return (CP){a+x.a,b+x.b};}
	CP operator - (const CP x) const {return (CP){a-x.a,b-x.b};}
	CP operator * (const CP x) const {return (CP){a*x.a-b*x.b,x.b*a+b*x.a};}
} F1[N],F2[N],Q[N];
void dft(CP *F,int n) {
	if(n==1) return ;
	dft(&F[0],n/2); dft(&F[n/2],n/2);
	CP w=(CP){cos(2*pi/n),sin(2*pi/n)},buf=(CP){1,0};
	for(int i=0; i<n/2; i++) {
		CP tmp=buf*F[i+n/2];
		F[i+n/2]=F[i]-tmp;
		F[i]=F[i]+tmp;
		buf=buf*w;
	}
	return ;
}
void idft(CP *F,int n) {
	dft(F,n);
	reverse(&F[1],&F[n]);
}
int n,m,p,lim;
int tr[N];
const int base=32000;
int main() {
	scanf("%d%d%d",&n,&m,&p);
	lim=1; while(lim<n+m+1) lim<<=1;
	for(int i=0; i<=n; i++) {
		int x; scanf("%d",&x);
		F1[i]=(CP){1.0*(x/base),1.0*(x%base)};
		F2[i]=(CP){1.0*(x/base),1.0*(-(x%base))};
	}
	for(int i=0,x; i<=m; i++) {
		scanf("%d",&x);
		Q[i]=(CP){1.0*(x/base),1.0*(x%base)};
	}
	for(int i=1; i<lim; i++)
		tr[i]=(tr[i>>1]>>1)|((i&1)?lim>>1:0);
	for(int i=1; i<lim; i++)
		if(i<tr[i]) {
			swap(F1[i],F1[tr[i]]);
			swap(F2[i],F2[tr[i]]);
			swap(Q[i],Q[tr[i]]);
		}
	dft(F1,lim); dft(F2,lim); dft(Q,lim);
	for(int i=0; i<lim; i++) Q[i].a=Q[i].a/lim,Q[i].b=Q[i].b/lim;
	for(int i=0; i<lim; i++) {
		F1[i]=F1[i]*Q[i];
		F2[i]=F2[i]*Q[i];
	}
	for(int i=1; i<lim; i++)
		if(i<tr[i]) {
			swap(F1[i],F1[tr[i]]);
			swap(F2[i],F2[tr[i]]);
		}
	idft(F1,lim); idft(F2,lim);
	for(int i=0; i<=n+m; i++) {
		long long a1b1,a2b1,a1b2,a2b2;
		a1b1=((long long)floor((F1[i].a+F2[i].a)*0.5+0.49))%p;
		a1b2=((long long)floor((F1[i].b+F2[i].b)*0.5+0.49))%p;
		a2b1=((long long)floor(F1[i].b+0.49)-a1b2)%p;
		a2b2=((long long)floor(F2[i].a+0.49)-a1b1)%p;
		printf("%lld ",((1ll*base*base%p*a1b1%p+1ll*base*(a1b2+a2b1)%p+a2b2)%p+p)%p);
	}
	return 0;
} 

多项式乘法逆

\(f^{-1}(x)\times f(x)=1\pmod {x^n}\)

我们假设已经求出了 \(f_*^{-1}(x)\times f(x)=1\pmod {x^{n/2}}\)\(n/2\) 向上取整)
\(f^{-1}(x)\times f(x)=1\pmod {x^{n/2}}\)
\(f_*^{-1}(x)-f^{-1}(x)=0\pmod {x^{n/2}}\)
\(f_*^{-2}(x)+f^{-2}(x)-2f_*^{-1}(x)f^{-1}(x)=0\pmod {x^n}\)
\(f_*^{-2}(x)f(x)+f^{-1}(x)-2f_*^{-1}(x)=0\pmod {x^n}\)
\(f^{-1}(x)=(2-f_*^{-1}(x)\times f(x))f_*^{-1}(x)\)

递归 +NTT 即可。
边界: \(n=1\) 时,为 \(f(0)\) 的逆元。

code
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+10;
const int p=998244353,g=3,gi=332748118;
int qpow(int a,int b) {
	int res=1;
	for(; b; b>>=1) {
		if(b&1) res=1ll*res*a%p;
		a=1ll*a*a%p;
	}
	return res;
}
int n,m,lim,tr[N];
void ntt(int *F,int n,int op) {
	if(n==1) return ;
	ntt(&F[0],n/2,op); ntt(&F[n/2],n/2,op);
	int w=qpow(op==1?g:gi,(p-1)/n),buf=1;
	for(int i=0; i<n/2; i++) {
		int tmp=1ll*F[i+n/2]*buf%p;
		F[i+n/2]=(F[i]-tmp+p)%p;
		F[i]=(F[i]+tmp)%p;
		buf=1ll*buf*w%p;
	}
	return ;
}
int c[N];
void inv(int *F,int *G,int n) {
	if(n==1) {
		G[0]=qpow(F[0],p-2);
		return ;
	}
	inv(F,G,(n+1)/2);
	for(int i=0; i<n; i++) c[i]=F[i];
	for(int i=n; i<lim; i++) c[i]=0;
	lim=1;
	while(lim<n*2) lim<<=1;
	for(int i=1; i<lim; i++)
		tr[i]=(tr[i>>1]>>1)|(i&1?(lim>>1):0);
	for(int i=1; i<lim; i++) {
		if(i<tr[i]) swap(c[i],c[tr[i]]),swap(G[i],G[tr[i]]);
	}
	ntt(c,lim,1); ntt(G,lim,1);
	for(int i=0; i<lim; i++) G[i]=1ll*G[i]*(-1ll*c[i]*G[i]%p+2ll)%p;
	for(int i=1; i<lim; i++)
		if(i<tr[i]) swap(G[i],G[tr[i]]);
	ntt(G,lim,-1);
	int iv=qpow(lim,p-2);
	for(int i=0; i<lim; i++) G[i]=(1ll*G[i]*iv%p+p)%p;
	for(int i=n; i<lim; i++) G[i]=0;
}
int a[N],ans[N];
int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]),a[i]=(a[i]+p)%p;
	inv(a,ans,n);
	for(int i=0; i<n; i++) printf("%d ",ans[i]);
	return 0;
} 

3.一些典题

P3723 [AH2017/HNOI2017] 礼物

考虑求 \(\min{\sum (a_i-b_i+c)^2}\),\(b_i\) 可以循环移位。
考虑将 \(b\) 复制一遍放到 \(i+1 \sim 2n\) 处。
拆式子: \(\sum (a_i^2+b_i^2)+nc^2+\sum (a_i-b_i) c -2\sum a_ib_i\)
\(\sum (a_i^2+b_i^2)\) 是定值,\(nc^2+\sum (a_i-b_i) c\) 是二次函数。
最后求 \(\max{\sum a_i b_{i+k}}\),发现是“差卷积形式”。
\(a\) 翻转为 \(a'\)\(\max a'_{n-i+1} b_{i+k}\) 是标准卷积。
\(a',b\) 转为多项式卷积,最后 \(n+k+1\) 就是偏移 \(k\) 位的答案。

posted @ 2023-08-02 21:25  s1monG  阅读(36)  评论(0)    收藏  举报