都应该知道多项式是什么对吧(否则学什么多项式乘法)

我们用\(A(x)\)表示一个\(n-1\)次多项式,即\(A(x)=\sum_{i=0}^{n-1} {a_i}*x^i\)

例如\(A(x)=x^2+3x+1\)就是一个\(x\)的二次多项式

多项式的加减都接触过,就是合并同类项的过程

乘法也很简单,就是初中的化简式子题

那么计算两个多项式相乘的时间复杂度是什么呢?

如果我们用初中学到的暴力拆括号法,第一个括号里的每一项都要与第二个括号了的项相乘,时间复杂度显然是\(O(n^2)\)的,看上去就十分糟糕

于是人们发明了FFT

那么又有新问题了,如果所有的系数对一个数取余呢?(这一点在生成函数中尤为关键)

于是人们又发明了NTT

我们先来了解一下FFT,不过在此之前,我们最好了解一下以下内容——

前置芝士

多项式的点值表示

先看这样一个问题:对于两个未知数\(x,y\),我至少要给你多少个方程你才能解出\(x,y\)的值?很明显至少要两个

这又是一个初中的结论的:\(n\)个未知数我们至少需要\(n\)个方程来求值

这给了我们一个新的表示一个多项式的方法,对于任意一个\(n-1\)次的多项式\(A(x)\)而言,加入我们知道了对于\(n\)个不同的\(x\)的取值的多项式的取值\(y\),即已知了函数图像上的\(n\)个点\((x_1,y_1),(x_2,y_2),\cdots ,(x_n,y_n)\),那么这个\(A(x)\)的解析式就应该是确定的了

很明显的发现,对于两个多项式\(A(x)\)和\(B(x)\),令\(C(x)=A(x)*B(x)\),假设最终的\(C(x)\)是\(n-1\)次的,那么我们可以任取\(n\)个不相同的\(x\)的值代入\(A(x)\)和\(B(x)\)中,得到它们的点值表示,对于它们的某一组\(x\)值相同的点值\((x_i,A_i),(x_i,B_i)\),会有\((x_i,A_iB_i)\)成立,因此我们可以在\(O(n)\)的时间内求得两个多项式的乘积

但这又一个前提——开始的\(A(x)\)和\(B(x)\)均为点值表达式。如果是最为人熟悉的系数表达式的话就要进行转化,将一个\(x\)代入求值需要\(O(n)\)的时间,所以朴素的求出某个多项式的点值表达式又是\(O(n)\)的

诶这和最开始的做法有什么区别啊QAQ

但这里至少有一个可以优化的地方——我们如何快速的将一个系数表达式转化成点值多项式。和明显代入值是无法避免的,所以只能对代入的\(x\)进行优化了

所以丧心病狂的学者们找到了下面这个好东西——单位根,但在了解单位根之前,我们先要知道

复数

日常打成负数,两者是完全不一样的

最直观的理解复数可以从这个问题入手:我们知道\(\sqrt 9=3\),那么\(\sqrt {-9}\)等于什么呢?很明显答案不是3或-3

于是无所事事的数学家发明了一个新的数——虚数,写作\(i\),这个数满足\(i^2=-1\)

然后将它和实数写在一起即\(a+bi\)(a,b均为实数),并将这一坨东西叫做复数

后来,人们又魔改了笛卡尔坐标系,将原来的\(x\)轴作为实轴,原来的\(y\)轴作为虚轴,于是虚数就被表示成了这个坐标轴上的一个点(也可以理解成向量),这个坐标轴也被叫做复平面。我们之后要提到的单位根,就和这个复平面有关

但在这之前,我们先来开一下与复数有关的运算

在c++中定义了虚数类型的库

#include<complex>

不过由于这玩意自带大常数,所以建议还是自己手打复数+重载运算符

复数的加减十分简单:

\[(a+bi)\pm(c+di)=(a\pm c)+(b\pm d)i
\]

乘法也是一样的,直接拆开即可

\[(a+bi)*(c+di)=ac+adi+bci-bd=(ac-bd)+(ad+bc)i
\]

这个东西在坐标系中表现为:模长相乘,幅角相加

把上面的东西整理一下得到如下的代码

struct complex{
double x,y;
complex(double x1=0.0,double y1=0.0) {x=x1,y=y1;}
};
complex operator +(complex a,complex b)
{
return complex(a.x+b.x,a.y+b.y);
} complex operator -(complex a,complex b)
{
return complex(a.x-b.x,a.y-b.y);
} complex operator *(complex a,complex b)
{
return complex(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);
}

单位根

(从这里开始的所有论述中,均保证\(n=2^k(k\in Z)\),你往下看就会发现这极大地简化了运算)

定义

考虑一些数\(z\)满足\(z^n=1\),那么这些\(z\)就被叫做单位根,它有一些很神奇的性质

我们考虑在复平面中的单位圆,并将其分成\(n\)等分(如下图\(n=4\))

得到的这些点就是上面提到的单位根,记做\(w_n^0,w_n^1,\cdots,w_n^{n-1}\),且\(w_n^0=1\)

我们来考虑一下它们的通项公式,由下面的图应该会有

将其用弧度制表示便有

\[w_n^k=cosk*\frac{2\pi}{n}+isink*\frac{2\pi}{n}
\]

我们把这样的\(w_n^0,w_n^1,\cdots,w_n^{n-1}\)代入到多项式\(A(x)\)中得到对应的值,这个过程叫做DFT(离散傅里叶变换)。为了更好的了解这样做的优越性,我们还需要知道单位根的性质

性质

1、\(w_n^n=w_n^0=1\)

这个不就是定义吗

2、\(w_{2n}^{2k}=w_n^k\)

证明:

\[w_{2n}^{2k}=cos2k*\frac{2\pi}{2n}+isin2k*\frac{2\pi}{2n}=cosk\frac{2\pi}{n}+isink\frac{2\pi}{n}=w_n^k
\]

3、\(w_n^{k+\frac{1}{2}n}=-w_n^k\)

证明:

\[w_n^{k+\frac{1}{2}n}=w_n^k*w_n^{\frac{1}{2}n}=w_n^k*(cos\frac{1}{2}n*\frac{2\pi}{n}+isin\frac{1}{2}n*\frac{2\pi}{n})=w_n^k*(cos\pi+isin\pi)=-w_n^k
\]

正文

前置芝士好长啊

在前置芝士中介绍到了DFT,那么为了配套我们在这里补充这个

IDFT(逆离散傅里叶变换)

这一块内容充分解释了为什么要用单位根来进行代入求值

假设一个\(n-1\)的多项式\(A(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}\)的DFT为\((y_0,y_1,\cdots,y_{n-1})\)。我们用一种类似于生成函数的方式再构造一个函数\(B(x)=y_0+y_1x+y_2x^2+\cdots+y_{n-1}x^{n-1}\),这时,我们不将单位根带入,取而代之的将他们的相反数(即\(-w_n^0,-w_n^1,\cdots,-w_n^{n-1}\))代入,得到了一组新数记做\((z_n^0,z_n^1,\cdots,z_n^{n-1})\)来化简一波式子

\[\begin{align}
z_k &= \sum_{i=0}^{n-1}y_i(w_n^{-k})^i\\
&= \sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j*(w_n^i)^j)(w_n^{-k})^i\\
&= \sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}({w_n^{j-k}})^i)
\end{align}
\]

我们注意到里面的和式是一个等比数列,当\(j=k\)的时候,里面就是\(\sum_{i=0}^{n-1}=n\)。而如果\(j\neq k\)时,根据等比数列的求和公式,这个式子就是酱紫的

\[\sum_{i=0}^{n-1}({w_n^{j-k}})^i=\frac{(w^{j-k}_n)^n-1}{w^{j-k}_n-1}=\frac{(w^n_n)^{j-k}-1}{w^{j-k}_n-1}=\frac{1-1}{w_n^{j-k}-1}=0
\]

所以上面的式子只有在\(j=k\)的时候的值才是非零的,且为\(n\),我们进一步化简就有

\[z_k=na_k
\]

\[a_k=\frac{z_k}{n}
\]

这也就为我们在点值表示法和系数表示法之间连接了一座桥梁——我们将单位根代入,可以得到多项式的点值表示;再将单位根的相反数代入由点值表示法得到的点值作为系数的多项式,可以得到这个多项式的系数表示法

实现相关

至此,我们已经解决了点值和系数的互相转化,它的时间复杂度仍然是将单位根代入求值的复杂度,也就是\(O(n^2)\),仍然不够优秀,有没有更好的方法?

在这里单位根的另一大优越性就体现出来了

对于一个多项式\(A(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^{n-1}\),我们考虑下面的两个新多项式

\[A_1(x)=a_0+a_2x+a_4x^2+\cdots+a_{n-2}x^{\frac{n}{2}-1}
A_2(x)=a_1+a_3x+a_5x^2+\cdots+a_{n-1}x^{\frac{n}{2}-1}
\]

那么应该有

\[A(x)=A_1(x^2)+xA_2(x^2)
\]

对于这个式子,我们将单位根代入,看看会有什么诡异的事情

我们保证\(k<\frac{n}{2}\),先将\(w_n^k\)代入

\[A(w_n^k)=A_1(w_n^{2k})+w_n^kA_2(w^{2k}_n)
\]

接下来是\(w_n^{k+\frac{n}{2}}\)

\[\begin{align}
A(w_n^{k+\frac{n}{2}}) &=A_1(w_n^{2k+n})+w_n^{k+\frac{n}{2}}A_2(w^{2k+n}_n)\\
&=A_1(w_n^{2k}*w_n^{n})-w_n^kA_2(w^{2k}_n*w^n_n)\\
&=A_!(w_n^{2k})-w_n^kA_2(w^{2k}_n)
\end{align}
\]

我们惊奇的发现,这两者之间只是一个符号的差别

这也就是说我们可以将原问题分治成两个子问题——求\(A_1(x)\)与\(A_2(x)\)的点值表示,然后再\(O(n)\)合并

总时间为\(T(n)=2T(\frac{n}{2})+O(n)\),由主定理知为\(O(nlogn)\)

好了最大的瓶颈已经被突破了

这是一道板子题的连接

黏贴一波代码

#include<iostream>
#include<string>
#include<string.h>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<vector>
#include<queue>
#include<map>
using namespace std;
#define rep(i,a,b) for (i=a;i<=b;i++)
typedef long long ll;
#define maxd 1e9+7
#define pi acos(-1.0)
struct complex{
double x,y;
complex(double x1=0.0,double y1=0.0) {x=x1,y=y1;}
};
complex operator +(complex a,complex b)
{
return complex(a.x+b.x,a.y+b.y);
} complex operator -(complex a,complex b)
{
return complex(a.x-b.x,a.y-b.y);
} complex operator *(complex a,complex b)
{
return complex(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);
}
int n,m;
complex a[8000100],b[8000100]; int read()
{
int x=0,f=1;char ch=getchar();
while ((ch<'0') || (ch>'9')) {if (ch=='-') f=-1;ch=getchar();}
while ((ch>='0') && (ch<='9')) {x=x*10+(ch-'0');ch=getchar();}
return x*f;
} void fft(int lim,complex *a,int typ)
{
//cout << lim << endl;
if (lim==1) return;//cout << 'a' << endl;
complex a1[(lim>>1)+10],a2[(lim>>1)+10];int i;
for (i=0;i<=lim;i+=2)
{
a1[i>>1]=a[i];a2[i>>1]=a[i+1];
}
fft(lim>>1,a1,typ);
fft(lim>>1,a2,typ);
complex wn=complex(cos(pi*2.0/lim),sin(pi*2.0/lim)*typ),w=complex(1.0,0.0);
for (i=0;i<(lim>>1);i++,w=w*wn)
{
a[i]=a1[i]+w*a2[i];
a[i+(lim>>1)]=a1[i]-w*a2[i];
}
} int main()
{
n=read();m=read();int i,lim=1;
for (i=0;i<=n;i++) a[i].x=read();
for (i=0;i<=m;i++) b[i].x=read();
while (lim<=n+m) lim<<=1;
//cout << lim << endl;
fft(lim,a,1);//cout << 'a' << endl;
fft(lim,b,1);
for (i=0;i<=lim;i++) a[i]=a[i]*b[i];
fft(lim,a,-1);
for (i=0;i<=n+m;i++) printf("%d ",(int)(a[i].x/lim+0.5));
return 0;
}

然后你就惊喜的发现了TLE,我们发现luogu的机子足够快让你卡过去了,但时间及其不美观

于是我们考虑优化

优化相关

所以为什么时间大?元凶很明显就是每一次的递归耗费了大量的时间

于是又有人开始思考了——能不能不用递归?

我们考虑原多项式的系数在递归过程中都被分在了哪个数组里

(借网上一张精美的图)

我们发现了原序列与后序列之间每个位置上的数的对应关系就是原序列的数的二进制翻转

证明的话直接考虑每一次分组的话就是根据当前位(从低到高依次考虑每一位)是0还是1,是0的话就分在前一组,是1的话就分在后一组,于是在最后统计的时候就反过来了(从高到低)

我们可以考虑从下面(最底层开始)一层一层的向上合并,最后得到我们想要的结果

于是我们就可以写出下面的有三个循环的嵌套的常数小一点的丑陋的FFT

#include<iostream>
#include<string>
#include<string.h>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<vector>
#include<queue>
#include<map>
using namespace std;
#define rep(i,a,b) for (i=a;i<=b;i++)
typedef long long ll;
#define maxd 1e9+7
#define pi acos(-1.0)
struct complex{
double x,y;
complex(double xx=0.0,double yy=0.0) {x=xx;y=yy;}
}; complex operator +(complex a,complex b)
{
return complex(a.x+b.x,a.y+b.y);
} complex operator -(complex a,complex b)
{
return complex(a.x-b.x,a.y-b.y);
} complex operator *(complex a,complex b)
{
return complex(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);
}
complex a[10001000],b[10001000];
int n,m,r[10001000]; int read()
{
int x=0,f=1;char ch=getchar();
while ((ch<'0') || (ch>'9')) {if (ch=='-') f=-1;ch=getchar();}
while ((ch>='0') && (ch<='9')) {x=x*10+(ch-'0');ch=getchar();}
return x*f;
} void fft(int lim,complex *a,int typ)
{
int i,mid;
for (i=0;i<lim;i++)
if (i<r[i]) swap(a[i],a[r[i]]);
for (mid=1;mid<lim;mid<<=1)
{
complex wn=complex(cos(pi/mid),sin(pi/mid)*typ);//也就是w_n^1,方便枚举所有的单位根
int len=mid<<1,sta,j;
for (sta=0;sta<lim;sta+=len)
{
complex w=complex(1,0);
for (j=0;j<mid;j++,w=wn*w)
{
complex x1=a[sta+j],x2=w*a[sta+j+mid];
a[sta+j]=x1+x2;
a[sta+j+mid]=x1-x2;
}
}
}
} int main()
{
n=read();m=read();int i,lim=1,cnt=0;
for (i=0;i<=n;i++) scanf("%lf",&a[i].x);
for (i=0;i<=m;i++) scanf("%lf",&b[i].x);
while (lim<=n+m) {cnt++;lim<<=1;}
for (i=0;i<=lim;i++)
r[i]=((r[i>>1]>>1)|((i&1)<<(cnt-1)));//处理每一个数的二进制逆序转化,大体思路就是将一个数拆成前n-1位和最后1位然后分别转换
//for (i=0;i<=lim;i++) cout << r[i] << endl;
fft(lim,a,1);
fft(lim,b,1);
for (i=0;i<=lim;i++) a[i]=a[i]*b[i];
fft(lim,a,-1);
for (i=0;i<=n+m;i++)
printf("%d ",(int)(a[i].x/lim+0.5));
return 0;
}

结语

好吧FFT是很强大的,在很多组合的题目经常出现

但是它还是有一个缺陷的——多次调用了C++中的三角函数,我们知道这样的话常数巨大且有可能产生精度误差

在某些我们保证了系数均为整数的情况下显得十分多于(这在某些生成函数题中尤为明显)

更重要的是,它并没有对取模这种运算进行很好的兼容

所以又有人才们搞出了一个新东西——NTT

我会在不就之后继续介绍NTT(前提是作业少)

所以想看NTT的话就找我的老师让他少布置点作业吧

完结撒花(雾)

FFT算法小结的更多相关文章

  1. C#排序算法小结

    前言 算法这个东西其实在开发中很少用到,特别是web开发中,但是算法也很重要,因为任何的程序,任何的软件,都是由很多的算法和数据结构组成的.但是这不意味着算法对于每个软件设计人员的实际工作都是很重要的 ...

  2. [图论]Dijkstra 算法小结

    Dijkstra 算法小结  By Wine93 2013.11 1. Dijkstra 算法相关介绍 算法阐述:Dijkstra是解决单源最短路径的算法,它可以在O(n^2)内计算出源点(s)到图中 ...

  3. [图论]Floyd 算法小结

    Floyd 算法小结  By Wine93 2013.11 1. Floyd算法简介 Floyd算法利用动态规划思想可以求出任意2点间的最短路径,时间复杂度为O(n^3),对于稠密图, 效率要高于执行 ...

  4. Paxos算法小结

    转自不正直的绅士,因百度空间迁移,无法注明出处,我从其google搜索引擎中的cache进行的copy. 不正直的绅士 是跟我一起工作过的非常有才的一个青年才俊. Paxos的使用非常广泛.sanlo ...

  5. FFT算法

    FFT算法的完整DSP实现 傅里叶变换或者FFT的理论参考: [1] http://www.dspguide.com/ch12/2.htm The Scientist and Engineer's G ...

  6. FFT算法的完整DSP实现(转)

    源:FFT算法的完整DSP实现 傅里叶变换或者FFT的理论参考: [1] http://www.dspguide.com/ch12/2.htm The Scientist and Engineer's ...

  7. 剑指Offer--排序算法小结

    剑指Offer--排序算法小结 前言 毕业季转眼即到,工作成为毕业季的头等大事,必须得认认真真进行知识储备,迎战笔试.电面.面试. 许久未接触排序算法了.平时偶尔接触到时自己会不假思索的百度,然后就是 ...

  8. 快速傅立叶变换(FFT)算法

    已知多项式f(x)=a0+a1x+a2x2+...+am-1xm-1, g(x)=b0+b1x+b2x2+...+bn-1xn-1.利用卷积的蛮力算法,得到h(x)=f(x)g(x),这一过程的时间复 ...

  9. msp430学习笔记-实现开方log等计算及FFT算法(待续)

    MSP430 FFT算法实现 http://bbs.21ic.com/icview-391532-1-1.html http://blog.sina.com.cn/s/blog_6cd2030b010 ...

随机推荐

  1. mariadb(第五章)视图、事物、索引、外键

    视图 对于复杂的查询,在多个地方被使用,如果需求发生了改变,需要更改sql语句,则需要在多个地方进行修改,维护起来非常麻烦 假如因为某种需求,需要将user拆房表usera和表userb,该两张表的结 ...

  2. Yii1.1框架实现PHP极光推送消息通知

    一.下载极光推送PHP SDK,解压后放在/protected/components/目录下,如下图所示: 二.完善修改下官方的demo例子,我这里复制一份demo,改为NotifyPush.php, ...

  3. Daily Scrum 12.19

    Member Task on 12.19 Task on 12.20 仇栋民 请假 完成Task972 : 完成活动评分基础功能 康家华 完成 Task1004 : 百度map UI优化 完成Task ...

  4. 提取PPT文件中的Vba ProjectStg Compressed Atom。Extract PPT VBA Compress Stream

    http://msdn.microsoft.com/en-us/library/cc313106(v=office.12).aspx  微软文档 PartI ********************* ...

  5. javascript重定向页面并用post方法传递消息

    javascript中重定向页面得方法很多,同时能传递消息的也不少:但可用post方法传递的我只找到两种: 第一种方法:用document.write在 JavaScript函数中,用document ...

  6. Vue之子组件

    全局组件 <script src="./static/vue.min.js"></script> // 导入vue <body> <div ...

  7. python3 selenium webdriver 元素定位xpath定位骚操作

    源文http://www.cnblogs.com/qingchunjun/p/4208159.html By.xpath() 这个方法是非常强大的元素查找方式,使用这种方法几乎可以定位到页面上的任意元 ...

  8. [转帖]windows+xshell+xming访问非桌面版Linux服务器

    windows+xshell+xming访问非桌面版Linux服务器 2016年06月05日 00:09:11 jxxiaohou 阅读数:11996 标签: Linux 更多 个人分类: Linux ...

  9. Angular 自定义指令传参

    <!DOCTYPE html><html ng-app="myApp"><head lang="en"> <meta ...

  10. mysql 如何查看sql语句执行时间和效率

    查看执行时间 1 show profiles; 2 show variables;查看profiling 是否是on状态: 3 如果是off,则 set profiling = 1: 4 执行自己的s ...