未完待续……
upd: 虽然未完,但是不想续了。
听说现在全人类都会 FFT 了?
这里做个小小结。
多项式
大概就是这样的一个东西:
系数:
次数:如果最高位非
多项式运算
多项式加法:若
多项式乘法:用乘法结合率算一下即可。如
离散卷积:我们常说“
系数表达
就是把
点值表达
随便代
如果两个多项式有
两种表达方式的转化
从系数表达到点值表达,我们常使用秦九韶算法(或称为霍纳法则)
从点值表达到系数表达,我们叫做插值,常使用拉格朗日公式:
如果你把
那就假装它是对的吧。3。
几道有趣的题
Codeforces 933B A Determined Cleanup
给定两个整数
题解
假设
我们把上式反过来:
往上迭代上去,我们就发现这个式子很像上面秦九韶算法的那个式子,所以我们得到:
所以我们只要对
代码
#include <cstdio>
typedef long long LL;
const int maxn = 2333;
LL ans[maxn];
LL p, k;
int main()
{
scanf("%lld%lld", &p, &k);
int top = 0;
while(p)
{
ans[++top] = p % (-k);
p /= -k;
if(ans[top] < 0)
p++, ans[top] += k;
}
printf("%d\n", top);
for(int i = 1; i <= top; ++i)
printf("%d ", ans[i]);
return 0;
}
复数
为解决复数开方问题,我们引入新数
复平面
先把坐标轴画出来,横的叫实轴,竖的叫虚轴,然后确定
大概就是这样5:

模长:上图中的
辐角:上图中的
复数运算
加减法:
乘法:
考虑模长为
欧拉公式
我才不会告诉你我不会证。6
单位根
考虑方程
画成图大概就是这样的7:

显然,
所以
消去引理
折半引理
所以:
也就是说,如果我们要算
求和引理
由:
两边同减去
故当
所以当
而当
综上:
或者我们可以写成:
其中
板子
struct Complex
{
double x, y;
Complex(double x = 0, double y = 0) { this->x = x, this->y = y; }
friend Complex operator + (Complex a, Complex b)
{ return Complex(a.x + b.x, a.y + b.y); }
friend Complex operator - (Complex a, Complex b)
{ return Complex(a.x - b.x, a.y - b.y); }
friend Complex operator * (Complex a, Complex b)
{ return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
};
多项式乘法的基本原理
首先,我们所说的多项式乘法显然是两个用系数表达的多项式之间的算法。
我们发现朴素算法的复杂度是
然后我们发现用系数表达好像很难优化,但多项式乘法有一个天然的优化方案,也就是如果多项式是用点值表示的,那么乘起来是
所以能不能先把它们转化为点值表达,然后乘起来之后把它们插值回去呢?
很高兴的是我们有 FFT。
所以,多项式乘法就是这样完成的。8

DFT 与 FFT
对于多项式 (感觉终于进入正题了)
分治!
折半引理使单位有了其他数没有的优势——它可以分治。
为了让分治能顺利进行,下面所涉及到的多项式的长度都可以表示为
我们把需要计算的多项式
平方出现了!
我们仅需计算
又
具体可以看代码。
代码实现
inline void FFT(Complex* a, int n)
{
if(n == 1)
return;
Complex *a0 = new Complex[n >> 1];
Complex *a1 = new Complex[n >> 1];
for(int i = 0; i < n; i += 2) // 奇偶分类
{
a0[i >> 1] = a[i];
a1[i >> 1] = a[i ^ 1];
}
FFT(a0, n >> 1);
FFT(a1, n >> 1); // 分治
Complex w(1., 0.), w1(cos(pi2 / n), sin(pi2 / n)); // w1是1次单位根,w是k次单位根
for(int i = 0; (i << 1) < n; ++i)
{
a[i] = a0[i] + a1[i] * w;
a[i + (n >> 1)] = a0[i] - a1[i] * w;
w = w * w1;
}
}
IDFT?
这样就完了吗?不是的。
现在我们所的到的是点值表示,而我们所需要的是把系数表达。所以我们需要把它插值回去。
我们把这个操作叫做离散傅里叶变换的逆变换(IDFT)。
拉格朗日公式?
我们假设得到
然后根据求和引理我们就可以得到:
由于
故我们可以把等式写成
所以:
所以我们只需计算
我们发现我们就是把普通的 FFT 单位根
回顾上面那幅图:

我们发现让
其中的单位根
具体可以看代码实现。
代码实现
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const double pi = acos(-1.);
const double pi2 = 2. * pi;
struct Complex
{
double x, y;
Complex(double x = 0, double y = 0) { this->x = x, this->y = y; }
friend Complex operator + (Complex a, Complex b)
{ return Complex(a.x + b.x, a.y + b.y); }
friend Complex operator - (Complex a, Complex b)
{ return Complex(a.x - b.x, a.y - b.y); }
friend Complex operator * (Complex a, Complex b)
{ return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
};
inline void FFT(Complex* a, int n, int tp) // 加了个tp,表示单位根的幂次是否为正
{
if(n == 1)
return;
Complex *a0 = new Complex[n >> 1];
Complex *a1 = new Complex[n >> 1];
for(int i = 0; i < n; i += 2)
{
a0[i >> 1] = a[i];
a1[i >> 1] = a[i ^ 1];
}
FFT(a0, n >> 1, tp);
FFT(a1, n >> 1, tp);
Complex w(1., 0.), w1(cos(pi2 / n), tp * sin(pi2 / n)); // 如果单位根的幂次为负那么sin的值应取负
for(int i = 0; (i << 1) < n; ++i)
{
a[i] = a0[i] + a1[i] * w;
a[i + (n >> 1)] = a0[i] - a1[i] * w;
w = w * w1;
}
}
const int maxn = 1000005;
Complex aa[maxn << 2], bb[maxn << 2];
int main()
{
int n, m, k;
scanf("%d%d", &n, &m);
for(k = 1; k <= n + m; k <<= 1);
for(int i = 0; i <= n; ++i)
scanf("%lf", &aa[i].x);
for(int i = 0; i <= m; ++i)
scanf("%lf", &bb[i].x);
FFT(aa, k, 1);
FFT(bb, k, 1);
for(int i = 0; i < k; ++i)
aa[i] = aa[i] * bb[i];
FFT(aa, k, -1);
for(int i = 0; i <= n + m; ++i)
printf("%d ", (int) (aa[i].x / k + .5));
return 0;
}
卡常大法好
蝴蝶操作
我们来看这段代码:
for(int i = 0; (i << 1) < n; ++i)
{
a[i] = a0[i] + a1[i] * w;
a[i + (n >> 1)] = a0[i] - a1[i] * w;
w = w * w1;
}
由于复杂度主要集中在复数运算上,所以我们可以把它优化:
for(int i = 0; (i << 1) < n; ++i)
{
t = a1[i] * w;
a[i] = a0[i] + t;
a[i + (n >> 1)] = a0[i] - t;
w = w * w1;
}
虽然也不知道这有什么用。
我们把上面这段代码叫做蝴蝶操作。
从递归到迭代
我们来看一下 FFT 递归的全过程10:

第一次递归时,我们把下标二进制最后一位为
我们把下标二进制分解来看看:

不知道大家有没有发现一个特点:这棵树从下往上好像做了一个二进制下的基数排序:先比较最后一位,在比较倒数第二位……也就是说,如果我们把这些数的二进制串倒过来,比如 110 就变成了 011,001 就变成了 100,我们其实是对它进行了一遍排序。
于是我们把一个数的下标倒过来就是递归树底层的位置了。
当然我们还可以 rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << cs),rev[i] 表示 cs 表示这个二进制数字的长度(含前导 (i & 1) << cs 表示 i 最后一位翻转后的数字,rev[i >> 1] >> 1 是 i 出最后一位外翻转得到的数字。比如 0101101,(i & 1) << cs 是 1000000,而 i >> 1 是 0010110,翻转之后得到 0110100,右移 1 之后是 0011010,在或上前面的 1000000,得到的是 1011010。
代码实现
#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1000005;
const double pi2 = 2. * acos(-1.);
struct Complex
{
double x, y;
Complex(double x = 0, double y = 0) { this->x = x, this->y = y; }
friend Complex operator + (Complex a, Complex b)
{ return Complex(a.x + b.x, a.y + b.y); }
friend Complex operator - (Complex a, Complex b)
{ return Complex(a.x - b.x, a.y - b.y); }
friend 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 rev[maxn << 2];
Complex aa[maxn << 2], bb[maxn << 2];
inline void FFT(Complex* a, int n, int tp)
{
for(int i = 0; i < n; ++i)
if(i < rev[i])
swap(a[i], a[rev[i]]);
for(int s = 2; s <= n; s <<= 1)
{
Complex w1(cos(pi2 / s), tp * sin(pi2 / s)), w, tmp;
for(int i = 0; i < n; i += s)
{
w = Complex(1., 0.);
for(int j = 0; (j << 1) < s; ++j, w = w * w1)
{
tmp = w * a[i + j + (s >> 1)];
a[i + j + (s >> 1)] = a[i + j] - tmp;
a[i + j] = a[i + j] + tmp;
}
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i <= n; ++i)
scanf("%lf", &aa[i].x);
for(int i = 0; i <= m; ++i)
scanf("%lf", &bb[i].x);
int l = 1, cs = 0;
while(l <= n + m)
{
l <<= 1;
cs++;
}
for(int i = 0; i < l; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (cs - 1));
FFT(aa, l, 1);
FFT(bb, l, 1);
for(int i = 0; i < l; ++i)
aa[i] = aa[i] * bb[i];
FFT(aa, l, -1);
for(int i = 0; i <= n + m; ++i)
printf("%d ", (int) (aa[i].x / l + .5));
return 0;
}
代码长度差不多(上面的是迭代实现),但速度和内存上的差距是很大的。

FFT 优化高精乘法
高精乘法就是把两个大数当成多项式乘起来,其实就是普通 FFT,就直接上代码了。
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int maxn = 60005;
const double pi2 = 2. * acos(-1);
struct Complex
{
double x, y;
Complex(double x = 0, double y = 0) { this->x = x, this->y = y; }
friend Complex operator + (Complex a, Complex b)
{ return Complex(a.x + b.x, a.y + b.y); }
friend Complex operator - (Complex a, Complex b)
{ return Complex(a.x - b.x, a.y - b.y); }
friend 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 rev[maxn << 2];
inline void FFT(Complex* a, int n, int tp)
{
for(int i = 0; i < n; ++i)
if(i < rev[i])
swap(a[i], a[rev[i]]);
for(int s = 2; s <= n; s <<= 1)
{
Complex w1(cos(pi2 / s), tp * sin(pi2 / s)), w, t;
for(int i = 0; i < n; i += s)
{
w = Complex(1., 0.);
for(int j = 0; (j << 1) < s; ++j, w = w1 * w)
{
t = a[i + j + (s >> 1)] * w;
a[i + j + (s >> 1)] = a[i + j] - t;
a[i + j] = a[i + j] + t;
}
}
}
}
Complex aa[maxn << 2], bb[maxn << 2];
LL ans[maxn << 2];
char s[maxn];
int main()
{
int n;
scanf("%d", &n);
scanf("%s", s);
for(int i = 0; i < n; ++i)
aa[i].x = s[n - i - 1] ^ 48;
scanf("%s", s);
for(int i = 0; i < n; ++i)
bb[i].x = s[n - i - 1] ^ 48;
int l = 1, cs = 0;
while(l < (n << 1))
{
l <<= 1;
cs++;
}
for(int i = 0; i < l; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (cs - 1));
FFT(aa, l, 1);
FFT(bb, l, 1);
for(int i = 0; i < l; ++i)
aa[i] = aa[i] * bb[i];
FFT(aa, l, -1);
for(int i = 0; i < l; ++i)
{
ans[i] += (LL) (aa[i].x / l + .5);
ans[i + 1] += ans[i] / 10;
ans[i] %= 10;
}
while(!ans[l] && l)
--l;
while(~l)
printf("%lld", ans[l--]);
return 0;
}
一些简单的题
UVa 12298 Super Poker II
按照国际惯例,uva 的题是一定要放 pdf 的:
题目大意
有一副扑克,对于每个正合数
多组数据,每组数据输入
题解
我们令
然后大力 FFT。
对了这题要开 long double。
代码
#include <cmath>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <bitset>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long double LD;
typedef long long LL;
inline char gc()
{
static const int L = 23333;
static char sxd[L], *sss = sxd, *ttt = sxd;
if(sss == ttt)
{
ttt = (sss = sxd) + fread(sxd, 1, L, stdin);
if(sss == ttt)
return EOF;
}
return *sss++;
}
#define dd c = gc()
inline char read(int& x)
{
x = 0;
char dd;
for(; !isdigit(c); dd)
if(c == EOF)
return EOF;
for(; isdigit(c); dd)
x = (x << 1) + (x << 3) + (c ^ 48);
return c;
}
#undef dd
const int maxn = 50005 << 2;
const LD pi2 = acos(-1.) * (LD) 2.;
int pp[maxn << 2];
inline void pre()
{
int n = 50000;
for(int i = 2; i <= n; ++i)
{
if(!pp[i])
{
for(int j = i << 1; j <= n; j += i)
pp[j] = 1;
}
}
}
struct Complex
{
LD x, y;
Complex(LD a = 0., LD b = 0.) { x = a, y = b; }
friend Complex operator + (Complex a, Complex b)
{ return Complex(a.x + b.x, a.y + b.y); }
friend Complex operator - (Complex a, Complex b)
{ return Complex(a.x - b.x, a.y - b.y); }
friend Complex operator * (Complex a, Complex b)
{ return Complex(a.x * b.x - a.y * b.y, a.x * b.y + b.x * a.y); }
};
int rev[maxn << 2];
inline void FFT(Complex* a, int n, int tp)
{
for(int i = 0; i < n; ++i)
if(i < rev[i])
swap(a[i], a[rev[i]]);
for(int s = 2; s <= n; s <<= 1)
{
Complex w, w1 = Complex(cos(pi2 / s), tp * sin(pi2 / s)), tmp;
for(int i = 0; i < n; i += s)
{
w = Complex(1., 0.);
for(int l = 0; (l << 1) < s; ++l, w = w * w1)
{
tmp = a[i + l + (s >> 1)] * w;
a[i + l + (s >> 1)] = a[i + l] - tmp;
a[i + l] = a[i + l] + tmp;
}
}
}
}
Complex aa[maxn << 2], bb[maxn << 2], cc[maxn << 2], dd[maxn << 2];
inline void solve(int n, int l, int r)
{
int L = r + 1;
int len = 1, cs = -1;
while(len <= (L << 2)) // 4个多项式相乘,长度当然要乘4了
{
len <<= 1;
cs++;
}
memset(aa, 0, sizeof(aa));
memset(bb, 0, sizeof(bb));
memset(cc, 0, sizeof(cc));
memset(dd, 0, sizeof(dd));
for(int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << cs);
for(int i = 1; i < r; ++i)
aa[i] = bb[i] = cc[i] = dd[i] = Complex((LD) pp[i], 0.);
for(int i = 1, t; i <= n; ++i)
{
char c = read(t);
switch(c)
{
case 'S':
aa[t].x = 0.;
break;
case 'H':
bb[t].x = 0.;
break;
case 'C':
cc[t].x = 0.;
break;
case 'D':
dd[t].x = 0.;
break;
}
}
FFT(aa, len, 1), FFT(bb, len, 1), FFT(cc, len, 1), FFT(dd, len, 1);
for(int i = 0; i < len; ++i)
aa[i] = aa[i] * bb[i] * cc[i] * dd[i];
FFT(aa, len, -1);
for(int i = l; i <= r; ++i)
printf("%lld\n", (LL) ((aa[i].x / len) + .5));
puts("");
}
signed main()
{
pre();
int x, y, z;
while(read(x) != EOF && read(y) != EOF && read(z) != EOF && (x || y || z))
solve(z, x, y);
return 0;
}
NTT
我们发现 FFT 的复杂度很优秀,然而不能取模。
那我们有没有什么办法让它能取模呢?
我们考虑从单位根下手。
我们看单位根的定义:
有没有关于取模等于
相信大家都想到了费马小定理:
当然
于是我们可以联立方程:
两边同开
如果
我们发现在做 FFT 时,
主要用的是