抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

未完待续……

upd: 虽然未完,但是不想续了。

听说现在全人类都会 FFT 了?

这里做个小小结。

多项式

大概就是这样的一个东西:

A(x)=i=0n1aixiA(x) = \sum_{i=0}^{n-1}a_ix^i

系数a0,a1,a2,,an1a_0, a_1, a_2,\cdots, a_{n-1}

次数 :如果最高位非00 系数是 aka_k,那就称A(x)A(x) 的次数为kk

多项式运算

多项式加法:若A(x)=i=0n1aixi,B(x)=i=0n1bixi,C(x)=i=0n1cixiA(x) = \sum_{i=0}^{n-1}a_ix^i, B(x) = \sum_{i=0}^{n-1}b_ix^i, C(x) = \sum_{i=0}^{n-1}c_ix^iA(x)+B(x)=C(x)A(x) + B(x) = C(x),那么ci=ai+bic_i=a_i+b_i

多项式乘法:用乘法结合率算一下即可。如A(x)=10x+9,B(x)=4x5A(x) =-10x+9,B(x) = 4x-5,则

A(x)B(x)=(10x+9)(4x5)=40x2+86x45A(x)\cdot B(x)=(-10x+9)\cdot(4x-5)=-40x^2+86x-45

离散卷积 :我们常说“A(x)A(x)B(x)B(x)的卷积”其实可以 简单理解为 A(x)A(x)B(x)B(x)的乘积,记作aba\otimes b[1]

系数表达

就是把 A(x)=i=0n1aixiA(x) = \sum_{i=0}^{n-1}a_ix^i 记录为 (a1,a2,,an1)(a_1,a_2,\cdots,a_{n-1})。朴素算法需要O(n2)\mathcal{O}(n^2) 进行乘法。

点值表达

随便代 nn 个不同的值进去,可以得到{(x0,y0),(x1,y1),,(xn1,yn1)}\{(x_0,y_0),(x_1,y_1),\cdots,(x_{n-1},y_{n-1})\}。这就是点值表达。

如果两个多项式有 2n2n 个不同值的答案一直,那么把它们的 yy 乘起来,就可以的到这两个多项式相乘后的多项式点值表达,复杂度O(n)\mathcal{O}(n)

两种表达方式的转化

从系数表达到点值表达,我们常使用 秦九韶算法 (或称为 霍纳法则

A(x0)=a0+x0(a1+x0(a2++(x0(an2+x0an1))))A(x_0)=a_0+x_0(a_1+x_0(a_2+\cdots+(x_0(a_{n-2}+x_0a_{n-1}))\cdots))

从点值表达到系数表达,我们叫做 插值 ,常使用 拉格朗日公式

A(x)=k=0n1ykjk(xxj)(xkxj)A(x)=\sum_{k=0}^{n-1}y_k\prod_{j\neq k}\frac{(x-x_j)}{(x_k-x_j)}

如果你把 xix_i 代进去,就会发现仅有当 kk 枚举到 iijk(xxj)(xkxj)=1\prod_{j\neq k}\frac{(x-x_j)}{(x_k-x_j)}=1,其余情况都为jk(xxj)(xkxj)=0\prod_{j\neq k}\frac{(x-x_j)}{(x_k-x_j)}=0(因为分子的xjxj=0x_j-x_j=0)。[2]

那就假装它是对的吧。[3]

几道有趣的题

Codeforces 933B A Determined Cleanup

给定两个整数 p,k(1p1018,2k2000)p, k (1\le p\le 10^{18},2\le k\le 2000),求构造多项式f(x)=i=0d1aixif(x)=\sum_{i=0}^{d-1}a_ix^i,使得存在多项式p(x)p(x) 满足f(x)=q(x)(x+k)+pf(x)=q(x)\cdot(x+k)+p,且ai[0,k),i[0,d)a_i\in[0,k),i\in[0,d)[4]

题解

假设q(x)=i=0d2bixiq(x)=\sum_{i=0}^{d-2}b_ix^i,那么有:

ai={p+kbii=0bi1+kbii1a_i = \begin{cases} p + kb_i && i=0\\ b_{i-1} + kb_i && i \ge 1 \end{cases}

我们把上式反过来:

{p=a0kb0bi1=aikbi\begin{cases} p=a_0-kb_0\\ b_{i-1}=a_i-kb_i \end{cases}

往上迭代上去,我们就发现这个式子很像上面秦九韶算法的那个式子,所以我们得到:p=anan1a1(k)p=\overline{a_na_{n-1}\cdots a_{1}}_{(-k)}

所以我们只要对 pp 进行 k-k 进制分解即可。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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;
}

复数

为解决复数开方问题,我们引入新数 ii,叫做 虚数单位 ,定义i2+1=0i^2+1=0。又把形如a+bi(a,bR)a+bi(a, b\in \mathbb{R}) 的数叫做 复数 ,复数集用C\mathbb{C} 表示。

复平面

先把坐标轴画出来,横的叫实轴,竖的叫虚轴,然后确定 00 的位置,z=a+biz=a+bi可以用二维空间来表示出来。

大概就是这样[5]

模长:上图中的rr

辐角:上图中的φ\varphi

复数运算

加减法(a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)+(c+di)=(a+c)+(b+d)i。与向量一样,可以使用三角形定则。

乘法(a+bi)(c+di)=ac+bci+adi+bdi2=(acbd)+(bc+ad)i(a+bi)(c+di)=ac+bci+adi+bdi^{2}=(ac-bd)+(bc+ad)i。复数相乘,模长相乘,幅角相加。

考虑模长为 11 的复数 ω\omega。一个复数xx 与它相乘,得到的答案 yyxx模长相等,辐角是 xx 加上了 ω\omega 的辐角。想到什么?旋转

欧拉公式

eiu=cosu+isinue^{iu}=\cos u+i\sin u

我才不会告诉你我不会证。[6]

单位根

考虑方程 xn1=0(nN+)x^n-1=0\,(n\in \mathbb{N_+}),考虑到复数旋转的性质,我们发现就是将单位圆划分成nn 等分。所得的解就叫 单位根,记为ωn\omega_n

画成图大概就是这样的[7]

显然,l=1,θ=2πnl=1, \theta=\frac{2\pi}{n}

所以ωn=cosθ+isinθ=cos2πn+isin2πn=e2πn\omega_n=\cos \theta+i\sin\theta=\cos\frac{2\pi}{n}+i\sin\frac{2\pi}{n}=e^{\frac{2\pi}{n}}

消去引理

ωdndk=(e2πdn)dk=(e2πn)k=ωnk\omega_{dn}^{dk}={(e^{\frac{2\pi}{dn}})}^{dk}=(e^{\frac{2\pi}{n}})^k=\omega_n^k

折半引理

ωnk+n2=ωnkωnn2=ωnk\omega_n^{k+\frac{n}{2}}=\omega_n^k\cdot\omega_n^\frac{n}{2}=-\omega_n^k

所以:

(ωnk+n2)2=(ωnk)2(\omega_n^{k+\frac{n}{2}})^2=(\omega_n^k)^2

也就是说,如果我们要算 f(ωnk)f(\omega^k_n) 的值(kZ,k[0,n)k\in\mathbb{Z},k\in[0, n)),我们只需要计算 k[0,n2)k\in[0,\frac{n}{2}) 时的值即可。

求和引理

由:

xi=0n1xi=i=1nxix\cdot\sum_{i=0}^{n-1}x^i=\sum_{i=1}^{n}x^i

两边同减去 i=0n1xi\sum_{i=0}^{n-1}x^i 得:

(x1)i=0n1xi=xnx0=xn1(x-1)\sum_{i=0}^{n-1}x^i=x^n-x^0=x^n-1

故当 x1x\neq 1 时:

i=0n1xi=xn1x1\sum_{i=0}^{n-1}x^i=\frac{x^n-1}{x-1}

所以当 ωnk1\omega_n^k\neq 1,即nkn\nmid k 时:

i=0n1(ωnk)i=(ωnk)n1ωnk1=0\sum_{i=0}^{n-1}(\omega_n^k)^i=\frac{(\omega_n^k)^n-1}{\omega_n^k-1}=0

而当 ωnk=1\omega_n^k=1,即nkn\mid k 时:

i=0n1(ωnk)i=n\sum_{i=0}^{n-1}(\omega_n^k)^i=n

综上:

i=0n1(ωnk)i={0nknnk\sum_{i=0}^{n-1}(\omega_n^k)^i = \begin{cases} 0& n\nmid k\\ n& n\mid k \end{cases}

或者我们可以写成:

i=0n1(ωnk)i=[nk]×n\sum_{i=0}^{n-1}(\omega_n^k)^i = [n\mid k]\times n

其中 [命题 a][\text{命题 a}] 表示如果命题 a 成立,那么该值为11,否则为00

板子

1
2
3
4
5
6
7
8
9
10
11
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); }
};

多项式乘法的基本原理

首先,我们所说的多项式乘法显然是两个用系数表达的多项式之间的算法。

我们发现朴素算法的复杂度是O(n2)\mathcal{O}(n^2)

然后我们发现用系数表达好像很难优化,但多项式乘法有一个天然的优化方案,也就是如果多项式是用点值表示的,那么乘起来是 O(n)\mathcal{O}(n) 的。

所以能不能先把它们转化为点值表达,然后乘起来之后把它们插值回去呢?

很高兴的是我们有 FFT。

所以,多项式乘法就是这样完成的。[8]

DFT 与 FFT

对于多项式 A(x)=i=0naixiA(x)=\sum_{i=0}^na_ix^i,我们记a=(a0,a1,,an1)a=(a_0, a_1, \cdots, a_{n-1})y=(A(ωn0),A(ωn1),,A(ωnn1))y=(A(\omega_n^0), A(\omega_n^1), \cdots, A(\omega_n^{n-1}))。则称yyaa离散傅里叶变换 DFT),记作y=DFTn(a)y=\operatorname{DFT}_n(a)。求一个多项式的DFT\operatorname{DFT},朴素算法的复杂度为O(n2)\mathcal{O}(n^2),而如果用一种称为 快速傅里叶变换FFT)的方法,利用上面所介绍的几个单位根的性质,可以将该过程优化到O(nlogn)\mathcal{O}(n\log n)(感觉终于进入正题了)

分治!

折半引理使单位有了其他数没有的优势——它可以分治。

为了让分治能顺利进行,下面所涉及到的多项式的长度都可以表示为 2k(kZ)2^k(k\in \mathbb{Z})。如果多项式长度不足2k2^k,我们就用00 将它补齐。

我们把需要计算的多项式 A(x)=i=0naixiA(x)=\sum_{i=0}^na_ix^i 按系数编号的奇偶分成两部分,即 A0(x)=i=0n/2a2ixiA_0(x)=\sum_{i=0}^{n/2}a_{2i}x^iA1(x)=i=0n/2a2i+1xiA_1(x)=\sum_{i=0}^{n/2}a_{2i+1}x^i。我们不难发现:

A(x)=A0(x2)+xA1(x2)A(x) = A_0(x^2) + xA_1(x^2)

平方出现了!

我们仅需计算x=ωnk(k=0n2)x=\omega_n^k(k=0\sim \frac{n}{2}),然后我们就会发现:

A(ωnk)={A0((ωnk)2)+ωnkA1((ωnk)2)k<n2A0((ωnkn/2)2)ωnk/2A1((ωnk/2)2)kn2A(\omega_n^k) = \begin{cases} A_0((\omega_n^k)^2) + \omega_n^kA_1((\omega_n^k)^2) && k<\frac{n}{2}\\ A_0((\omega_n^{k-n/2})^2) - \omega_n^{k/2}A_1((\omega_n^{k/2})^2) && k \ge \frac{n}{2} \end{cases}

x2=(ωnk)2=ωn/2kx^2=(\omega_n^k)^2=\omega_{n/2}^k,所以递归下去是我们使用的单位根其实是ωn/2\omega_{n/2}

具体可以看代码。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)。

拉格朗日公式?O(n2)\mathcal{O}(n^2)

我们假设得到y=DFT(a)y=\operatorname{DFT}(a),我们构造向量cc

ck=i=0n1yi(ωnk)i=i=0n1j=0n1ωnkiajωnij=i=0n1j=0n1ωni(jk)aj=j=0n1i=0n1ωni(jk)aj=j=0n1aji=0n1(ωn(jk))i\begin{aligned} c_k&=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}\omega_n^{-ki}a_j\omega_n^{ij}\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}\omega_n^{i(j-k)}a_j\\ &=\sum_{j=0}^{n-1}\sum_{i=0}^{n-1}\omega_n^{i(j-k)}a_j\\ &=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}(\omega_n^{(j-k)})^i \end{aligned}

然后根据求和引理我们就可以得到:

ck=j=0n1aji=0n1(ωn(jk))i=j=0n1[jkn]naj\begin{aligned} c_k&=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}(\omega_n^{(j-k)})^i\\ &=\sum_{j=0}^{n-1}[j-k\mid n]na_j \end{aligned}

由于 0j,k<n0\le j,k<n,所以jknj-k\mid n 当且仅当 jk=0j-k=0j=kj=k

故我们可以把等式写成

ck=j=0n1[j=k]naj=nakc_k=\sum_{j=0}^{n-1}[j=k]na_j=na_k

所以:

ak=1ncka_k=\frac{1}{n}c_k

所以我们只需计算 ckc_k 即可。[9]

我们发现我们就是把普通的 FFT 单位根 ωn\omega_n 改成了ωn1\omega_n^{-1}

回顾上面那幅图:

我们发现让 θ\theta 到这转,也就是把图上下翻转一下就可以了。

其中的单位根ωn1=cos2πnisin2πn\omega_n^{-1}=\cos\frac{2\pi}{n}-i\sin\frac{2\pi}{n}

具体可以看代码实现。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#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;
}

卡常大法好

蝴蝶操作

我们来看这段代码:

1
2
3
4
5
6
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;
}

由于复杂度主要集中在复数运算上,所以我们可以把它优化:

1
2
3
4
5
6
7
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]

第一次递归时,我们把下标二进制最后一位为 00 的放在左边,最后一位为 11 的放在右边。接着,我们把下标倒数第二位为 00 的放在坐标,倒数第二位为 11 的放在右边……

我们把下标二进制分解来看看:

不知道大家有没有发现一个特点:这棵树从下往上好像做了一个二进制下的基数排序:先比较最后一位,在比较倒数第二位……也就是说,如果我们把这些数的二进制串倒过来,比如 110 就变成了 011001 就变成了100,我们其实是对它进行了一遍排序。

于是我们把一个数的下标倒过来就是递归树底层的位置了。

O(nlogn)\mathcal{O}(n\log n)暴力?

当然我们还可以 O(n)\mathcal{O}(n) 解决,有一个很显然的 dp:rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << cs)rev[i]表示 ii 翻转后的数字,cs表示这个二进制数字的长度(含前导 00)。(i & 1) << cs 表示 i 最后一位翻转后的数字,rev[i >> 1] >> 1i 出最后一位外翻转得到的数字。比如 0101101(i & 1) << cs1000000,而 i >> 10010110,翻转之后得到 0110100,右移1 之后是0011010,在或上前面的1000000,得到的是1011010

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#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,就直接上代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#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 的:

题目大意

有一副扑克,对于每个正合数 pp,恰好有44 张不同花色的牌。其中有一些牌丢失(题目会提供已经丢失牌的列表,四种花色分别用 S,H,C,D\text{S,H,C,D} 表示)。从四种不同花色牌中分别取出一张牌,求这四张牌构成点数为 nn 的方案数。

多组数据,每组数据输入 a,b,ca, b, ccc 为丢失牌的数量,分别输出 n=a,n=a+1,,n=bn=a,n=a+1,\cdots,n=b 时的答案。[8:1]

题解

我们令 f[i]f[i] 表示用现有的牌组成 ii 的方案数。正常的 dpdp 加入每种花色的复杂度都是 O(n2)\mathcal{O}(n^2),但是我们发现对两种花色的合并,如果我们有f1[i],f2[i]f_1[i],f_2[i] 两种花色,那么得到的 f[i]f[i] 其实就是 f1f_1f2f_2的卷积,因为根据乘法原理与加法原理,我们可以得到f[i]=j+k=if1[j]+f2[k]f[i] = \sum_{j+k=i} f_1[j]+f_2[k]

然后大力 FFT。

对了这题要开long double

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#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 的复杂度很优秀,然而不能取模。

那我们有没有什么办法让它能取模呢?

我们考虑从单位根下手。

我们看单位根的定义:

ωnn=1\omega_n^n=1

有没有关于取模等于 11 的式子?

相信大家都想到了费马小定理:

ap11(modp)a^{p-1}\equiv 1\pmod p

当然 pp 得是素数。

于是我们可以联立方程:

ap1ωnn(modp)a^{p-1}\equiv\omega^n_n\pmod p

两边同开 nn 次根号:

ap1nωn(modp)a^{\frac{p-1}{n}}\equiv \omega_n\pmod p

如果 np1n\mid p-1 的话,那这式子就有趣了:我们可以把 ωn\omega_n 换成ap1na^{\frac{p-1}{n}},就可以完成取模操作了。

我们发现在做 FFT 时,nn一定是 22 的幂次,所以 pp 只要表示成 2k×t1(k,tN)2^k\times t-1(k,t\in \mathbb{N}),且2kn2^k\ge n 就可以了。

主要用的是998244353=119×223+1998244353=119\times2^{23}+1[11]


  1. 才是真的定义。 ↩︎

  2. 感谢 majun 大佬 的口胡证明。 ↩︎

  3. 这里 看真的证明。 ↩︎

  4. 翻译来自 NaVi_Awson 大佬的blog,略有改动。 ↩︎

  5. 图片来自Wikipedia↩︎

  6. 具体可以去看看luojinyao 大佬的 blog↩︎

  7. 图片来自《算法导论》,有改动。 ↩︎

  8. 翻译摘自《算法竞赛入门经典 训练指南》,有改动。 ↩︎ ↩︎

  9. 感谢 sxd666 巨佬 的口胡。 ↩︎

  10. 图片来自《算法导论》。 ↩︎

  11. 其它的数可以参考 这里↩︎

评论