浅析卷积
-1.先赞后看,养成习惯
本文最早发布于2019年2月17日,经格式美化后于2021年8月6日同步重发在CSDN、洛谷博客及Gitee网页,转载请注明 出处
0. 写在前面
从粗斜体到分界线中的内容可以跳过。
本文中一些算法的简历:
| 简写 | 全称 | 中文 | |
|---|---|---|---|
| FWT | Fast Walsh Transformation | 快速沃尔什变换 | Fast Wonderful TLE |
| FFT | Fast Fourier Transformation | 快速傅里叶变换 | Fantastic Fluency TLE |
| NTT | Number Theory Transformation | 快速数论变换 | Natural Talented TLE |
1. 卷积的定义
可以通过简单的前/后缀和计算,以下是
#include<stdio.h>
int N,a[1000005],b[1000005],c[1000005],A[1000005],B[1000005],C[1000005];
int main(){
scanf("%d",&N);
for(int i=1;i<=N;i++)
scanf("%d",a+i);
for(int i=1;i<=N;i++)
scanf("%d",b+i);
for(int i=1;i<=N;i++){
A[i]=a[i]+A[i-1];
B[i]=b[i]+B[i-1];
}
for(int i=1;i<=N;i++)
C[i]=A[i]*B[i];
for(int i=N;i>=1;i--)
c[i]=C[i]-C[i-1];
for(int i=1;i<=N-1;i++)
printf("%d ",c[i]);
printf("%d\n",c[N]);
return 0;
}
-
C_i=A_i\times B_i -
用其逆变换将
C_i 变成c_i 得到答案
3. 当 * 指 \vee/\wedge
在这篇文章中,如果涉及到带
3.1 用向量表示数
一个
| 数值 | 向量表示 | 数值 | 向量表示 |
|---|---|---|---|
| 0 | 4 | ||
| 1 | 5 | ||
| 2 | 6 | ||
| 3 | 7 |
3.2 \vee 的实质
我们把用向量表示的数字
由此可知
#include<stdio.h>
int n,k,N,a[100005],b[100005],c[100005];
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",a+i);
for(int i=0;i<n;i++)
scanf("%d",b+i);
for(N=1;N<n;N<<=1,k++);
for(int i=0;i<k;i++)
for(int j=0;j<N;j++)
if((j&(1<<i))==0)
a[j+(1<<i)]+=a[j];
for(int i=0;i<k;i++)
for(int j=0;j<N;j++)
if((j&(1<<i))==0)
b[j+(1<<i)]+=b[j];
for(int i=0;i<N;i++)
c[i]=a[i]*b[i];
for(int i=k-1;i>=0;i--)
for(int j=N-1;j>=0;j--)
if((j&(1<<i))==0)
c[j+(1<<i)]-=c[j];
for(int i=0;i<=N-2;i++)
printf("%d ",c[i]);
printf("%d\n",c[N-1]);
return 0;
}
4.1 千里之行,始于足下:N=2
由小学知识可知:
我们将
听说是正确的,于是拓展到高维
4.2 一句不是废话的废话
虽然我们已经发现了正解,我们还是从向量的角度看一下。比如
再写一遍
发现
4.3 具体实现
公式恐惧症患者请果断按下Ctrl+W以发起正当防卫
5.1 求多项式乘法的新方法
定义
不幸的是,带入多项式需要
5.2 选择带入的数
显然,瓶颈在带入的数的选择上。那我们需要带入怎样的数带入呢?
5.2.1 复数
我们知道,
复数是所有能够写成
把 1 看做 i 看做 人称 也就是复平面,复平面上的点和
5.2.2 单位复根
单位圆指在复平面上以原点为圆心,以
5.2.3 单位复根的性质
- 对于任意整数
n\geqslant0,k\geqslant0,d\geqslant0 有
- 对于任意整数
n\geqslant0,k\geqslant0 有
- 对于任意整数
n\geqslant0 有
- 对于任意整数
n\geqslant0,i\geqslant0,j\geqslant0 有
- 对于任意整数
n\geqslant0,i\geqslant0,j\geqslant0 有
5.3 开始带入!
我们定义将
根据定义得
由性质5得
我们把
奇偶分类
于是我们惊喜地发现,这可以分治做。
为什么需要奇偶分类呢?也许这就是傅里叶的伟大之处吧。
5.4 逆变换
那么FFT的逆变换怎么写呢?
令人惊讶的是,恰有
为什么它是正确的?
5.5 蝴蝶变换
我们将
| 区间大小 | ||||||||
|---|---|---|---|---|---|---|---|---|
| 8 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 4 | 0 | 2 | 4 | 6 | 1 | 3 | 5 | 7 |
| 2 | 0 | 4 | 2 | 6 | 1 | 5 | 3 | 7 |
| 1 | 0 | 4 | 2 | 6 | 1 | 5 | 3 | 7 |
把这个表格用二进制描述
| 区间大小 | ||||||||
|---|---|---|---|---|---|---|---|---|
| 8 | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
| 4 | 000 | 010 | 100 | 110 | 001 | 011 | 101 | 111 |
| 2 | 000 | 100 | 010 | 110 | 001 | 101 | 011 | 111 |
| 1 | 000 | 100 | 010 | 110 | 001 | 101 | 011 | 111 |
我们发现表格的第一行和最后一行二进制是反转的,这样我们就发现了
#include<math.h>
#include<stdio.h>
#include<algorithm>
using namespace std;
const double pi=acos(-1.0);
int n,m,res=0,N=1,len,revers[2097160];
long long ans[2097160];
int i,j,k,l;
struct node{
double x,y;
node(double x=0,double y=0):x(x),y(y){}
node operator*(const node &b){
return node(x*b.x-y*b.y,x*b.y+y*b.x);
}
node operator+(const node &b){
return node(x+b.x,y+b.y);
}
node operator-(const node &b){
return node(x-b.x,y-b.y);
}
}a[2097160],b[2097160],T,t,x,y;
void FFT(node *a,double flag){
for(i=0;i<N;i++)
if(i<revers[i])
swap(a[i],a[revers[i]]);
for(j=1;j<N;j<<=1){
T=node(cos(pi/j),flag*sin(pi/j));
for(k=0;k<N;k+=(j<<1)){
t=node(1,0);
for(l=0;l<j;l++,t=t*T){
x=a[k+l],y=t*a[k+j+l];
a[k+l]=x+y;
a[k+j+l]=x-y;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
n++;
m++;
for(i=0;i<n;i++)
scanf("%lf",&a[i].x);
for(i=0;i<m;i++)
scanf("%lf",&b[i].x);
for(;N<max(n,m)<<1;N<<=1,len++);
for(i=0;i<=N;i++)
revers[i]=(revers[i>>1]>>1)|((i&1)<<(len-1));
FFT(a,1);
FFT(b,1);
for(i=0;i<=N;i++)
a[i]=a[i]*b[i];
FFT(a,-1);
for(i=0;i<=N;i++)
ans[i]+=(long long)(a[i].x/N+0.5);
for(;!ans[N]&&N;N--);
N++;
for(i=0;i<n+m-2;i++)
printf("%lld ",ans[i]);
printf("%lld\n",ans[n+m-2]);
return 0;
}
5.6 精度问题
把上面的代码加入模操作后提交到 洛谷P4245【模板】任意模数NTT 里去发现光荣WA。
然后就发现
5.6.1 原根
如果
当
- 对于任意整数
n\geqslant0,k\geqslant0,d\geqslant0 有
- 对于任意整数
n\geqslant0,k\geqslant0 有
- 对于任意整数
n\geqslant0 有
- 对于任意整数
n\geqslant0,i\geqslant0,j\geqslant0 有
- 对于任意整数
n\geqslant0,i\geqslant0,j\geqslant0 有
5.6.2 能选的质数
一般情况下有三个质数可选:
-
469762049=7\times 2^{26}+1 -
998244353=119\times 2^{23}+1 -
1004535809=749\times 2^{21}+1
当
代码如下:
#include<stdio.h>
#include<algorithm>
using namespace std;
const long long p=998244353,g=3,invg=332748118;
int n,m,res=0,N=1,len,revers[2097160];
long long ans[2097160],a[2097160],b[2097160],T,t,x,y;
int i,j,k,l;
inline long long power(long long a,long long k,long long p){
long long ans=1,t=a;
for(;k;k>>=1,t=t*t%p)
if(k&1)
ans=ans*t%p;
return ans;
}
void NTT(long long *a,long long flag){
for(i=0;i<N;i++)
if(i<revers[i])
swap(a[i],a[revers[i]]);
for(j=1;j<N;j<<=1){
T=power(flag==1?g:invg,(p-1)/j/2,p);
for(k=0;k<N;k+=(j<<1)){
t=1;
for(l=0;l<j;l++,t=t*T%p){
x=a[k+l],y=t*a[k+j+l]%p;
a[k+l]=(x+y)%p;
a[k+j+l]=((x-y)%p+p)%p;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
n++;
m++;
for(i=0;i<n;i++){
scanf("%lld",a+i);
a[i]=a[i]%p;
}
for(i=0;i<m;i++){
scanf("%lld",b+i);
b[i]=b[i]%p;
}
for(;N<max(n,m)<<1;N<<=1,len++);
for(i=0;i<=N;i++)
revers[i]=(revers[i>>1]>>1)|((i&1)<<(len-1));
NTT(a,1);
NTT(b,1);
for(i=0;i<=N;i++)
a[i]=a[i]*b[i]%p;
NTT(a,-1);
for(i=0;i<=N;i++)
ans[i]=a[i]*power(N,p-2,p)%p;
for(i=0;i<n+m-2;i++)
printf("%lld ",ans[i]);
printf("%lld\n",ans[n+m-2]);
return 0;
}
5.7 换个角度看 \bigoplus 卷积
我们再回忆一下4.2节的内容,我们在做 4.1节的结论了。
5.8 FFT再拓展
在洛谷上搜索FFT会发现还有一道 洛谷P4245【模板】任意模数NTT ,怎么lòng?
我们定三个质数做模数,一般是5.6.2节中的三个,分别进行 long long(对,你没看错,不是爆int),
贴代码:
#include<cstdio>
#include<string.h>
#include<algorithm>
using namespace std;
int mod;
inline int power(int a,int k,int p){
int ans;
for(ans=1;k;k>>=1,a=1LL*a*a%p)
if(k&1)
ans=1LL*ans*a%p;
return ans;
}
const int p[3]={469762049,998244353,1004535809},g=3;
class longint{
private:
int A,B,C;
static longint reduce(const longint &x){
return longint(x.A+(x.A>>31&p[0]),x.B+(x.B>>31&p[1]),x.C+(x.C>>31&p[2]));
}
public:
explicit longint(){}
explicit longint(int x){
A=B=C=x;
}
explicit longint(int x,int y,int z){
A=x;
B=y;
C=z;
}
friend longint operator+(const longint &lsh,const longint &rsh){
return reduce(longint(lsh.A+rsh.A-p[0],lsh.B+rsh.B-p[1],lsh.C+rsh.C-p[2]));
}
friend longint operator-(const longint &lsh,const longint &rsh){
return reduce(longint(lsh.A-rsh.A,lsh.B-rsh.B,lsh.C-rsh.C));
}
friend longint operator*(const longint &lsh,const longint &rsh){
return longint(1LL*lsh.A*rsh.A%p[0],1LL*lsh.B*rsh.B%p[1],1LL*lsh.C*rsh.C%p[2]);
}
int get(){
long long x=1LL*(B-A+p[1])%p[1]*power(p[0],p[1]-2,p[1])%p[1]*p[0]+A;
return (1LL*(C-x%p[2]+p[2])%p[2]*power(1LL*p[0]*p[1]%p[2],p[2]-2,p[2])%p[2]*(1LL*p[0]*p[1]%mod)+x)%mod;
}
};
const int Array_Limit=1<<18;
int N,len,revers[Array_Limit|5];
longint exp[Array_Limit|5];
void init(int n){
len=-1;
N=1;
while(N<n){
N<<=1;
len++;
}
for(int i=1;i<=N;i++)
revers[i]=revers[i>>1]>>1|(i&1)<<len;
longint t(power(g,(p[0]-1)/N,p[0]),power(g,(p[1]-1)/N,p[1]),power(g,(p[2]-1)/N,p[2]));
exp[0]=longint(1);
for(longint *i=exp;i!=exp+N;i++)
*(i+1)=*i*t;
}
void NTT(longint *a,int flag){
for(int i=1;i<Array_Limit;i++)
if(i<revers[i])
swap(a[i],a[revers[i]]);
for(int i=1;i<N;i<<=1){
int t=N/i>>1;
for(int j=0;j<N;j+=i<<1)
for(int k=0;k<i;k++){
longint x=flag==1?exp[t*k]:exp[N-t*k],y=a[i+j+k]*x;
x=a[j+k];
a[j+k]=x+y;
a[i+j+k]=x-y;
}
}
if(flag==-1){
longint inv_N(power(N,p[0]-2,p[0]),power(N,p[1]-2,p[1]),power(N,p[2]-2,p[2]));
for(longint *i=a;i!=a+N;i++)
*i=(*i)*inv_N;
}
}
int n,m;
longint a[Array_Limit|5],b[Array_Limit|5];
int main(){
scanf("%d%d%d",&n,&m,&mod);
n++;
m++;
for(int i=0,x;i<n;i++){
scanf("%d",&x);
a[i]=longint(x%mod);
}
for(int i=0,x;i<m;i++){
scanf("%d",&x);
b[i]=longint(x%mod);
}
init(n+m);
NTT(a,1);
NTT(b,1);
for(int i=0;i<N;i++)
a[i]=a[i]*b[i];
NTT(a,-1);
for(int i=0;i<n+m-2;i++)
printf("%d ",a[i].get());
printf("%d\n",a[n+m-2].get());
return 0;
}
6. 写在最后
初稿成型时Up只有初一,如今重整时可能会有一些没发现的错误,如有欢迎指正。