多项式乘法(FFT)学习笔记

------------------------------------------
本文只探讨多项式乘法(FFT)在信息学中的应用
如有错误或不明欢迎指出或提问,在此不胜感激

多项式

1.系数表示法
     一般应用最广泛的表示方式
     用A(x)表示一个x-1次多项式,a[i]为$ x^i$的系数,则A(x)=$ \sum_0^{n-1}$ a[i] * $ x^i$

仅利用这种方式求多项式乘法复杂度为O($ n^2$),不够优秀
2.点值表示法
     将n个互不相同的值$ x_0$...$ x_{n-1}$带入多项式,可以得到
     对于一个n-1次多项式,可以被n个点所唯一对应.
     因而对于A(x)*B(x),只需要得知A的2n个点值和对应B的点值即可O(n)求出多项式的乘积
     然而得知这些点值的复杂度依然在平方级别,达不到要求

考虑优化
先引一些要用到的名词

复数
复数即为表示成a+bi的数,其中i为-1的平方根
表示:
     可以通过平面直角坐标系上的一条向量(0,0)到(x,y)表示x+yi
     其中x轴为实数轴,y轴为虚数轴
运算:
     复数运算符合四则运算,即:
     (a+bi) + (c+di) = (a+c) + (b+d)i;
     (a+bi) * (c+di) = ac + adi + bci - bd$ i^2$ =(ac - bd) + (bc + ad)i
几何意义:
     定义模长为向量长度,幅角为从x轴正半轴逆时针转动到向量的角
     复数相加等同于向量加法
     复数相乘,模长相乘,幅角相加
//建议complex类手写,速度优于STL

单位根
以下默认n为$ 2^x$ 且x为非负整数
在复数平面,以原点为圆心,以1为半径作圆
以x轴正半轴到其与原交点(0,0)到(1,0)的这条向量为起点n等分圆,圆心到每个n等分点的向量均称为n次单位根
对于一个单位根,其标号为幅角/(360°/n),特别的,(0,0)到(1,0)的向量标号为0
以下用$ w_n^k$ 表示标号为k的n次单位根

单位根的性质
     1. $ w_n^k$ = cos($ \frac{2π}{n}$)k + sin($ \frac{2π}{n}$)ki
     证明:欧拉公式

2. $ w_n^k$ = $ w_{2n}^{2k}$
     证明:带入式1,等价于分子分母同乘2,

3. $ w_n^k$ * $ w_n^k$=$ w_n^{2k}$
     证明:根据(复数相乘,模长相乘,幅角相加)
     又因为模长均为1,所以相当于只把转动角度乘2

4. $ w_n^{k+\frac{n}{2}}$ = -$ w_n^k$
     证明:$ w_n^{k+\frac{n}{2}}$相当于在$ w_n^k$的基础上逆时针方向再旋转180° (复数相乘,模长相乘,幅角相加)
     因而等价于取负

进入正题
递归完成快速傅里叶变换

设多项式A(x)的系数为$ a_0$...$ a_{n-1}$

A(x)=$ a_0$ + $ a_1$*x + $ a_2$*$ x^2$ + $ a_3$*$ x^3$ + $ a_4$*$ x^4$ + ... + $ a_{n-2}$*$ x^{n-2}$ + $ a_{n-1}$*$ x^{n-1}$

按下标奇偶性分成两组,在这里设
        A0(x) = $ a_0$ + $ a_2$ * $ x$ + $ a_4$ * $ x^2$ + ... + $ a_{n-2}$ * $ x^\frac{n-2}{2}$
        A1(x) = $ a_1$ + $ a_3$ * $ x$ + $ a_5$ * $ x^2$ + ... + $ a_{n-1}$ * $ x^\frac{n-2}{2}$

显然得到A(x) = A0($ x^2$) + xA1($ x^2$)
我们代单位根$ w_n^k$(0<=k<$ \frac{n}{2}$)入式得

A($ w_n^k$)=A0($ w_n^{2k}$)+$ w_n^k$A1($ w_n^{2k}$)//性质3

同理代$ w_n^{k+\frac{n}{2}}$入式得

A($ w_n^{k+\frac{n}{2}}$)=A0($ w_n^{2k+n}$)+$ w_n^{k+\frac{n}{2}}$A1($ w_n^{2k+n}$)
 = A0($ w_n^{2k}$) - $ w_n^k$ * A1($ w_n^{2k}$)//性质4&&$ w_n^n$=1

容易发现上下两式只有常数项的符号不同
因而只需求前一般即可得到后一般
递归形式程序结构:
对于长度为n的A(x)
分割成长度为$ \frac{n}{2}$的A0(x)和A1(x)
求A0(x)和A1(x)
通过A0(x)和A1(x)计算带入$ w_n^k$(0<=k<$ \frac{n}{2}$)时的值
变号计算代$ w_n^{k+\frac{n}{2}}$入式的值

我们可以用数组A[i]表示某一多项式(不一定是初始多项式)代入$ w_n^i$}时的值
由于许多奥妙重重的性质,我们不需要对于每个多项式都维护整个数组A,只需要维护它需要返回的值即可
绘图可得,当某一多项式递归到只有一项的时候,要返回的一定只是$ w_n^0$(1),即直接返回原多项式该项系数即可
递归代码:

void FFT(const int lim,cp *A)//cp即为complex类型,lim为2^n的整型
{
    if(lim==1)return;//直接返回对应常数项
    cp A0[lim>>1],A1[lim>>1];
    for(rt i=0;i<lim;i+=2)A0[i>>1]=A[i],A1[i>>1]=A[i+1];
    FFT(lim>>1,A0,fla);FFT(lim>>1,A1,fla);//递归求解
    cp w={cos(PI*2.0/lim),sin(PI*2.0/lim)},k={1,0};//w为1号单位根
    for(rt i=0;i<lim>>1;i++,k=k*w)//k即为第i个单位根
    {
        A[i]=A0[i]+k*A1[i];//A0,A1数组中的i号本相当于A数组中的2i号,乘2再除2后相当于没有
        A[i+(lim>>1)]=A0[i]-k*A1[i];
    }
}

逆变换
以上求得的均为点值表示法的结果
需要将其转回系数表示法
实际操作相当于再进行FFT时单位根逆向(顺时针)计算
递归全代码

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rt register int
#define ll long long
#define r read()
using namespace std;
ll read()
{
    ll x = 0; int zf = 1; char ch;
    while (ch != ‘-‘ && (ch < ‘0‘ || ch > ‘9‘)) ch = getchar();
    if (ch == ‘-‘) zf = -1, ch = getchar();
    while (ch >= ‘0‘ && ch <= ‘9‘) x = x * 10 + ch - ‘0‘, ch = getchar(); return x * zf;
}
void write(ll y)
{
    if (y < 0) putchar(‘-‘), y = -y;
    if (y > 9) write(y / 10);
    putchar(y % 10 + ‘0‘);
}
inline void writeln(ll x)
{
    write(x);putchar(‘\n‘);
}
int i,j,k,m,n,x,y,z,cnt,all,num;
struct cp{
    double x,y;
}a[2200010],b[2200010];
inline cp operator +(const cp x,const cp y){return {x.x + y.x, x.y + y.y};}
inline cp operator *(const cp x,const cp y){return {x.x * y.x - x.y * y.y, x.x * y.y + x.y * y.x};}
inline cp operator -(const cp x,const cp y){return {x.x - y.x, x.y - y.y};}
const double PI=acos(-1.0);
void FFT(const int lim,cp *A,const int fla)//fla为-1表示逆变换
{
    if(lim==1)return;cp A1[lim>>1],A2[lim>>1];
    for(rt i=0;i<lim;i+=2)A1[i>>1]=A[i],A2[i>>1]=A[i+1];
    FFT(lim>>1,A1,fla);FFT(lim>>1,A2,fla);
    cp w={cos(PI*2.0/lim),sin(PI*2.0/lim)*fla},k={1,0};
    for(rt i=0;i<lim>>1;i++,k=k*w)
    {
        A[i]=A1[i]+k*A2[i];
        A[i+(lim>>1)]=A1[i]-k*A2[i];
    }

}
int main()
{
    n=r;m=r;
    for(rt i=0;i<=n;i++)a[i].x=r,a[i].y=0;
    for(rt i=0;i<=m;i++)b[i].x=r,b[i].y=0;
    int limit=1;while(limit<=n+m)limit<<=1;//将多项式长度凑到2^n
    FFT(limit,a,1);FFT(limit,b,1);
    for(rt i=0;i<=limit;i++)a[i]=a[i]*b[i];//a为点值表示的多项式
    FFT(limit,a,-1);//逆变换
    for(rt i=0;i<=n+m;i++)write(a[i].x/limit+0.5),putchar(‘ ‘);
    //因为有limit个单位根因而答案需要除limit,0.5是四舍五入
    return 0;
}

问题:常数巨大
解决方案:改成非递归(迭代)形式
观察下图


发现底层序列的值相当于原序列的值的二进制反转
因而我们可以预处理底层的值然后迭代向上推

如何预处理
1.x为偶数
     x=(x>>1)<<1;
     在反转序列中reverse[x]=reverse[x>>1]>>1;
2.x为奇数
     相当于x-1的结果再在最高位补1

这样我们得到底层结果之后,一层一层向上迭代合并即可
迭代代码:

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rt register int
#define ll long long
#define r read()
using namespace std;
ll read()
{
    ll x = 0; int zf = 1; char ch;
    while (ch != ‘-‘ && (ch < ‘0‘ || ch > ‘9‘)) ch = getchar();
    if (ch == ‘-‘) zf = -1, ch = getchar();
    while (ch >= ‘0‘ && ch <= ‘9‘) x = x * 10 + ch - ‘0‘, ch = getchar(); return x * zf;
}
void write(ll y)
{
    if (y < 0) putchar(‘-‘), y = -y;
    if (y > 9) write(y / 10);
    putchar(y % 10 + ‘0‘);
}
inline void writeln(ll x)
{
    write(x);putchar(‘\n‘);
}
int i,j,k,m,n,x,y,z,cnt,all,num,li,lim;
int low[2200010];//low数组就是底层的reverse序列
struct cp{
    double x,y;
}a[2200010],b[2200010];
inline cp operator + (const cp x,const cp y){return {x.x+y.x, x.y+y.y};}
inline cp operator - (const cp x,const cp y){return {x.x-y.x, x.y-y.y};}
inline cp operator * (const cp x,const cp y){return {x.x*y.x-x.y*y.y, x.x*y.y+x.y*y.x};}
double PI=acos(-1.0);
void FFT(cp *A,const int fla)
{
    for(rt i=0;i<lim;i++)if(i<low[i])swap(A[i],A[low[i]]);//得到底层数列
    for(rt i=1;i<lim;i<<=1)//i表示当前层的下面每个区间大小为i
    {
        cp w={cos(PI/i),fla*sin(PI/i)};//本应为2PI/2i
        for(rt j=0;j<lim;j+=(i<<1))//j为当前层的每个起始端点
        {
            cp K={1,0};
            for(rt k=0;k<i;k++,K=K*w)//k枚举当前层的j所在区间的元素
            {
                const cp x=A[j+k],y=K*A[j+k+i];//求解,如果对递归理解深刻应该能理解这段
                A[j+k]=x+y;
                A[j+k+i]=x-y;
            }
        }
    }
}
int main()
{
    n=r;m=r;
    for(rt i=0;i<=n;i++)a[i].x=r;
    for(rt i=0;i<=m;i++)b[i].x=r;
    lim=1;while(lim<=n+m)lim*=2,li++;
    for(rt i=0;i<lim;i++)
    low[i]=(low[i>>1]>>1)+((i&1)<<(li-1));
    FFT(a,1);FFT(b,1);
    for(rt i=0;i<lim;i++)a[i]=a[i]*b[i];
    FFT(a,-1);
    for(rt i=0;i<=(n+m);i++)printf("%d ",(int)(a[i].x/lim+0.5));
    return 0;
}

原文地址:https://www.cnblogs.com/DreamlessDreams/p/8697931.html

时间: 2024-10-08 01:29:08

多项式乘法(FFT)学习笔记的相关文章

Servlet乘法表学习笔记

一.控制台实现乘法表 package com.shanrengo; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServle

【笔记篇】(理论向)快速傅里叶变换(FFT)学习笔记w

现在真是一碰电脑就很颓废啊... 于是早晨把电脑锁上然后在旁边啃了一节课多的算导, 把FFT的基本原理整明白了.. 但是我并不觉得自己能讲明白... Fast Fourier Transformation, 快速傅里叶变换, 是DFT(Discrete Fourier Transform, 离散傅里叶变换)的快速实现版本. 据说在信号处理领域广泛的应用, 而且在OI中也有广泛的应用(比如SDOI2017 R2至少考了两道), 所以有必要学习一波.. 划重点: 其实学习FFT最好的教材是<算法导论

洛谷.3803.[模板]多项式乘法(FFT)

题目链接:洛谷.LOJ. FFT相关:快速傅里叶变换(FFT)详解.FFT总结.从多项式乘法到快速傅里叶变换. #include <cmath> #include <cctype> #include <cstdio> #include <algorithm> #define gc() getchar() const int N=1e6+5; const double PI=acos(-1); int n,m; struct Complex { double

[uoj#34] [洛谷P3803] 多项式乘法(FFT)

新技能--FFT. 可在 \(O(nlogn)\) 时间内完成多项式在系数表达与点值表达之间的转换. 其中最关键的一点便为单位复数根,有神奇的折半性质. 多项式乘法(即为卷积)的常见形式: \[ C_n=\sum\limits_{i=0}^n A_iB_{n-i} \] 基本思路为先将系数表达 -> 点值表达 \(O(nlogn)\) 随后点值 \(O(n)\) 进行乘法运算 最后将点值表达 -> 系数表达 \(O(nlogn)\) 代码 #include<cstdio> #inc

luogu3803 多项式乘法 (FFT)

FFT讲解传送门 简单记一下做法: 1.算法流程:两式的系数表达转化为点值表达(O(nlogn))->利用点值表达使两式相乘(O(n))->将结果的点值表达转化回系数表达(O(nlogn)) 2.做法: $$目标:把一个n项多项式F(x)=\sum_{i=0}^{n-1}a_ix^i转化为\{(w^k_n,y_k)\}的点值表达,其中w^k_n为n次单位根的k次方$$ 不妨设n为2的幂次,如果不是,则可以补上系数为0的高次项 $$将a_ix^i按照幂次奇偶性分组,得到F(x)=(a_0x^0+

多项式相关——FFT(学习中……持续更新)

FFT 参考blog: 十分简明易懂的FFT(快速傅里叶变换) 快速傅里叶变换(FFT)详解 系数表示法 一个一元\(n\)次多项式\(f(x)\)可以被表示为:\[f(x) = \sum_{i = 0}^{n}a_{i}x^{i}\] 即用\(i\)次项的系数来表示\(f(x)\),展开就是\(f(x) = {a_{0}, a_{1}...a_{n}}\) 点值表示法 把多项式看做一个函数,然后带入\(n\)个不同的\(x\),可以得到\(n\)个不同的\(y\),每对\((x, y)\)就组

位向量 补码与无符号 加法与乘法 CSAPP学习笔记

计算机中用位来表示整数,一种方式只能表示非负数,一种可以表示有符号数. 无符号数编码: 补码编码: 由上面的定义可以知道补码与无符号之间的对应关系(见下式),最高位为0时,补码与无符号表示是一样的,而最高位为1时,举个例子,补码表示的-1对应于无符号数的4294967295(这里指的是32位数) . 在整数运算之前必须先了解 整数的扩展和截断 扩展分为零扩展和符号扩展,零扩展是简单的在表示的开头添加0,适用于无符号数的扩展.而符号扩展在表示中添加最高有效位值的副本,适用于补码的扩展.比如4位的1

UOJ #34 多项式乘法 FFT快速傅立叶变换

题目大意:这是一道模板题. CODE: #include <cmath> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #define MAX 1000010 using namespace std; const double PI = acos(-1.0); struct Complex{ double x,y; Complex(dou

整数表示 补码与无符号 加法与乘法 CSAPP学习笔记

计算机中用位来表示整数,一种方式只能表示非负数,一种可以表示有符号数. 无符号数编码: 补码编码: 由上面的定义可以知道补码与无符号之间的对应关系(见下式),最高位为0时,补码与无符号表示是一样的,而最高位为1时,举个例子,补码表示的-1对应于无符号数的4294967295 . 在整数运算之前必须先了解 整数的扩展和截断 扩展分为零扩展和符号扩展,零扩展是简单的在表示的开头添加0,适用于无符号数的扩展.而符号扩展在表示中添加最高有效位值的副本,适用于补码的扩展.比如4位的1101扩展成8位,即1