@总结 - 1@ 多项式乘法 —— FFT
@0 - 参考资料@
@1 - 一些概念@
多项式的系数表示法:形如 \(A(x)=a_0+a_1x+...+a_{n-1}x^{n-1}\)。
多项式的点值表示法:对于 n-1 次多项式 \(A(x)\),我们选取 n 个不同的值 \(x_0, x_1, ... , x_{n-1}\) 代入 \(A(x)\),得到 \(y_i=A(x_i)\)。则 \((x_0, y_0),...,(x_{n-1},y_{n-1})\) 称为多项式的点值表示。
把多项式系数当作 n 个变量,n 个点当作 n 个线性方程,可以用高斯消元求得唯一解。因此,我们可以用这 n 个点唯一表示 A(x) 。
注意,一个多项式的点值表示法并不是唯一的。
如果用点值表示法的多项式作乘法,可以直接把纵坐标相乘,在 O(n) 的时间实现多项式乘法。
FFT(快速傅里叶变换)可以实现 O(nlog n) 的 点值表示 与 系数表示 之间的转换。
一个解释多项式乘法原理的图:
复数:复数简单来说就是 \(a + bi\),其中\(i^2=-1\)。可以发现复数 \(a + bi\) 与二维平面上的向量 \((a, b)\) 一一对应。
复数的乘法可以直接(a + bi)(c + di)展开相乘。但是几何上复数乘法还有另一种解释:
这样定义下,复数的乘法为模长相乘,幅角相加。
单位根:定义 n 次单位根为使得 \(z^n=1\) 成立的复数 \(z\)。一共 n 个,在单位圆上且 n 等分单位圆。
可以发现 n 次单位根模长都为 1,幅角依次为\(0*\frac{2\pi}{n},1*\frac{2\pi}{n},...,(n-1)*\frac{2\pi}{n}\)。
我们记 n 次单位根依次为\(w_n^0,w_n^1,...w_n^{n-1}\)。
有以下几个性质:
(1)\(w_{n}^{i}*w_{n}^{j}=w_{n}^{i+j}\)
(2)\(w_{dn}^{dk}=w_n^k\),有点类似于分数的约分。
(3)\(w_{2n}^k=-w_{2n}^{k+n}\)。
以上几个性质都可以从幅角的方面去理解。
@2 - 傅里叶正变换@
FFT 的正变换实现,是基于对多项式进行奇偶项分开递归再合并的分治进行的。
对于 n-1 次多项式,我们选择插入 n 次单位根求出其点值表达式。
记多项式\(A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}\)。
再记\(A_o(x)=a_1+a_3x+a_5x^2+...\)。
再记\(A_e(x)=a_0+a_2x+a_4x^2+...\)。
有\(A(x)=x*A_o(x^2)+A_e(x^2)\)。
令 \(n = 2*p\)。则有:
\(A(w_n^k)=w_n^k*A_o[(w_{n/2}^{k/2})^2]+A_e[(w_{n/2}^{k/2})^2]=w_n^k*A_o(w_p^k)+A_e(w_p^k)\);
\(A(w_n^{k+p})=w_n^{k+p}*A_o(w_p^{k+p})+A_e(w_p^{k+p})=-w_n^k*A_o(w_p^k)+A_e(w_p^k)\)
在已知 \(A_o(w_p^k)\) 与 \(A_e(w_p^k)\) 的前提下,可以 O(1) 算出 \(A(w_n^k)\) 与 \(A(w_n^{k+p})\)。
因此,假如我们递归求解 \(A_o(x),A_e(x)\) 两个多项式 p 次单位根的插值,就可以在O(n)的时间内算出 \(A(x)\) n 次单位根的插值。
时间复杂度是经典的 \(T(n)=2*T(n/2)+O(n)=O(n\log n)\)。
@3 - 傅里叶逆变换@
观察我们刚刚的插值过程,实际上就是进行了如下的矩阵乘法。
\(\begin{bmatrix} (w_n^0)^0 & (w_n^0)^1 & \cdots & (w_n^0)^{n-1} \\ (w_n^1)^0 & (w_n^1)^1 & \cdots & (w_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{n-1})^0 & (w_n^{n-1})^1 & \cdots & (w_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A(w_n^0) \\ A(w_n^1) \\ \vdots \\ A(w_n^{n-1}) \end{bmatrix}\)
我们记上面的系数矩阵为 \(V\)。
对于下面定义的 \(D\):
\(D = \begin{bmatrix} (w_n^{-0})^0 & (w_n^{-0})^1 & \cdots & (w_n^{-0})^{n-1} \\ (w_n^{-1})^0 & (w_n^{-1})^1 & \cdots & (w_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{-(n-1)})^0 & (w_n^{-(n-1)})^1 & \cdots & (w_n^{-(n-1)})^{n-1} \end{bmatrix}\)
考虑 \(D*V\)的结果:
\((D*V)_{ij}=\sum_{k=0}^{k<n}d_{ik}*v_{kj}=\sum_{k=0}^{k<n}w_n^{-ik}*w_{n}^{kj}=\sum_{k=0}^{k<n}w_n^{k(j-i)}\)
当 i = j 时,\((D*V)_{ij}=n\);
当 i ≠ j 时,\((D*V)_{ij}=1+w_n^{j-i}+(w_n^{j-i})^2+...=\frac{1-(w_n^{j-i})^n}{1-w_n^{j-i}}\)=0;
【根据定义,n 次单位根的 n 次方都等于 1】
所以:\(\frac1n*D=V^{-1}\)
因此将这个结果代入最上面那个公式里面,有:
\(\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \frac1n \begin{bmatrix} (w_n^{-0})^0 & (w_n^{-0})^1 & \cdots & (w_n^{-0})^{n-1} \\ (w_n^{-1})^0 & (w_n^{-1})^1 & \cdots & (w_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{-(n-1)})^0 & (w_n^{-(n-1)})^1 & \cdots & (w_n^{-(n-1)})^{n-1} \end{bmatrix}\begin{bmatrix} A(w_n^0) \\ A(w_n^1) \\ \vdots \\ A(w_n^{n-1}) \end{bmatrix}\)
“这样,逆变换 就相当于把 正变换 过程中的 \(w_n^k\) 换成 \(w_n^{-k}\),之后结果除以 n 就可以了。”——摘自某博客。
……
还是有点难理解。比如为什么我们不直接把\(w_n^k\) 换成 \(\frac1n*w_n^{-k}\) 算了。
实际上,因为\(w_n^{-k}=w_n^{n-k}\),也就是说它 TM 还是一个 n 次单位根。所以我们插值还是正常的该怎么插怎么插。如果换成 \(\frac1n*w_n^{-k}\) 它就不是一个单位根,以上性质就不满足了。
@4 - 迭代实现 FFT@
递归版本的 FFT 虽好,可奈何常数太大。
我们考虑怎么迭代实现 FFT。观察奇偶分组后各数的位置。
原序列:0,1,2,3,4,5,6,7。
终序列:0,4,2,6,1,5,3,7。
转换为二进制再来看看。
原序列:000,001,010,011,100,101,110,111。
终序列:000,100,010,110,001,101,011,111。
可以发现终序列是原序列每个元素的翻转。
于是我们可以先把要变换的系数排在相邻位置,从下往上迭代。
这个二进制翻转过程可以自己脑补方法,只要保证时间复杂度O(nlog n),代码简洁就可以了。
在这里给出一个参考的方法:
我们对于每个 i,假设已知 i-1 的翻转为 j。考虑不进行翻转的二进制加法怎么进行:从最低位开始,找到第一个为 0 的二进制位,将它之前的 1 变为 0,将它自己变为 1。因此我们可以从 j 的最高位开始,倒过来进行这个过程。
@5 - 参考代码实现@
本代码为 uoj#34 的AC代码。在代码中有些细节可以关注一下。
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 400000;
const double PI = acos(-1);
struct complex{
double r, i;
complex(double _r=0, double _i=0):r(_r), i(_i){}
};//C++ 有自带的复数模板库,但很显然我并不会。
typedef complex cplx;
cplx operator +(cplx a, cplx b){return cplx(a.r+b.r, a.i+b.i);}
cplx operator -(cplx a, cplx b){return cplx(a.r-b.r, a.i-b.i);}
cplx operator *(cplx a, cplx b){return cplx(a.r*b.r-a.i*b.i, a.r*b.i+b.r*a.i);}
void FFT(cplx *A, int n, int type) {
for(int i=0,j=0;i<n;i++) {
if( i < j ) swap(A[i], A[j]);
for(int k=(n>>1);(j^=k)<k;k>>=1);//这个地方读不懂就背吧。。。
}
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
cplx u = cplx(cos(type*PI/t), sin(type*PI/t));
//这个地方需要注意一点:如果题目中需要反复用到 FFT,则可以预处理出所有单位根以减小常数。
for(int i=0;i<n;i+=s) {
cplx r = cplx(1, 0);
for(int j=0;j<t;j++,r=r*u) {
cplx e = A[i+j], o = A[i+j+t];
A[i+j] = e + r*o; A[i+j+t] = e - r*o;
}
}
}
}
cplx A[MAXN + 5], B[MAXN + 5], C[MAXN + 5];
int main() {
int n, m;
scanf("%d%d", &n, &m); n++, m++;
for(int i=0;i<n;i++)
scanf("%lf", &A[i].r);
for(int i=0;i<m;i++)
scanf("%lf", &B[i].r);
int len = 1;
while( len < (n+m-1) ) len <<= 1;
//此处重点:由于我们每一次都要奇偶分组,所以长度必须为2的整数次幂,高位补0就好了。
//还有一点:A 与 B 的乘积最高次数为 n+m-1,而不是 n 也不是 m。
FFT(A, len, 1); FFT(B, len, 1);
for(int i=0;i<len;i++)
C[i] = A[i] * B[i];
FFT(C, len, -1);
for(int i=0;i<n+m-1;i++)
printf("%d ", int(round(C[i].r/len)));//记得,一定要记得除以len。
}
@6 - 快速数论变换 NTT@
实际上这可以算是 FFT 的一个优化。
FFT虽然跑得快,但是因为是浮点数运算,终究还是有 精度、常数 等问题。
然而问题来了:我们多项式乘法都是整数在那里搞来搞去,为什么一定要扯到浮点数。是否存在一个在模意义下的,只使用整数的方法?
想一想我们用了哪些单位根的性质:
(1)\(w_{n}^{i}*w_{n}^{j}=w_{n}^{i+j}\)
(2)\(w_{dn}^{dk}=w_n^k\)
(3)\(w_{2n}^k=-w_{2n}^{k+n}\)
(4) n 个单位根互不相同,且 \(w_n^0=1\)
我们能否找到一个数,在模意义下也满足这些性质?
引入原根的概念:对于素数 p,p 的原根 G 定义为使得 \(G^0,G^1,...,G^{p−2}(\mod p)\) 互不相同的数。
再定义 \(g_n^k = (G^{\frac{p-1}{n}})^k\)。验证一下这个东西是否满足单位根的以上性质。
(1),由幂的运算立即可得。
(2),由幂的运算立即可得。
(3),\(g_{2n}^{k+n}=(G^{\frac{p-1}{2n}})^{k+n}=(G^{\frac{p-1}{2n}})^k*(G^{\frac{p-1}{2n}})^n=G^{\frac{p-1}{2}}*g_{2n}^k=-g_{2n}^k(\mod p)\)。
【因为\(G^{p-1}=1(\mod p)\)且由原根定义\(G^{\frac{p-1}{2}}\not=G^{p-1}(\mod p)\),故\(G^{\frac{p-1}{2}}=-1(\mod p)\)】
(4),由原根的定义立即可得。
所以我们就可以搞 NTT 了。直接把代码中涉及单位根的换成原根即可。
然而,可以发现 NTT 适用的模数 m 十分有限。它应该满足以下性质:
(1)令 \(m = 2^p*k+1\),k 为奇数,则多项式长度必须 \(n \le 2^p\)。
(2)方便记忆,方便记忆,与方便记忆。(其实我后来发现记不住可以直接现场暴力求。。。)
这里有一些合适的模数【来源:miskcoo】。
NTT 参考代码,一样是 uoj 的那道题。
#include<cstdio>
#include<algorithm>
using namespace std;
const int MOD = 998244353;
const int MAXN = 400000;
const int G = 3;
int pow_mod(int b, int p) {
int ret = 1;
while( p ) {
if( p & 1 ) ret = 1LL*ret*b%MOD;
b = 1LL*b*b%MOD;
p >>= 1;
}
return ret;
}
void NTT(int *A, int n, int type) {
for(int i=0,j=0;i<n;i++) {
if( i > j ) swap(A[i], A[j]);
for(int l=(n>>1);(j^=l)<l;l>>=1);
}
for(int s=2;s<=n;s<<=1) {
int t = (s>>1), u = (type == 1) ? pow_mod(G, (MOD-1)/s) : pow_mod(G, (MOD-1)-(MOD-1)/s);
for(int i=0;i<n;i+=s) {
for(int j=0,p=1;j<t;j++,p=1LL*p*u%MOD) {
int e = A[i+j], o = 1LL*A[i+j+t]*p%MOD;
A[i+j] = (e + o)%MOD, A[i+j+t] = (e + MOD - o)%MOD;
}
}
}
}
int A[MAXN + 5], B[MAXN + 5], C[MAXN + 5];
int main() {
int n, m;
scanf("%d%d", &n, &m); n++, m++;
for(int i=0;i<n;i++)
scanf("%d", &A[i]);
for(int i=0;i<m;i++)
scanf("%d", &B[i]);
int len = 1; while( len < n+m-1 ) len <<= 1;
NTT(A, len, 1); NTT(B, len, 1);
for(int i=0;i<len;i++)
C[i] = 1LL*A[i]*B[i]%MOD;
NTT(C, len, -1);
int inv = pow_mod(len, MOD-2);
for(int i=0;i<n+m-1;i++)
printf("%d ", 1LL*C[i]*inv%MOD);
}
@7 - 任意模数 NTT@
假如题目中规定了模数怎么办?还卡 FFT 的精度怎么办?
有两种方法:
@三模数 NTT@
我们可以选取三个适用于 NTT 的模数 M1,M2,M3 进行 NTT,用中国剩余定理合并得到 x mod (M1*M2*M3) 的值。只要保证 x < M1*M2*M3 就可以直接输出这个值。
之所以是三模数,因为用三个大小在 10^9 左右模数对于大部分题目来说就足够了。
但是 M1*M2*M3 可能非常大怎么办呢?难不成我还要写高精度?其实也可以。
我们列出同余方程组:
x \equiv a_1&\mod m_1\\
x \equiv a_2&\mod m_2\\
x \equiv a_3&\mod m_3\\
\end{cases}\]
先中国剩余定理(这个不会……我真的帮不了 qwq)合并前两个方程组:
x \equiv A&\mod M\\
x \equiv a_3&\mod m_3\\
\end{cases}
\]
其中 M = m1*m2 < 10^18。
然后将第一个方程变形得到 \(x = kM + A\) 代入第二个方程:
k \equiv (a_3-A)*M^{-1} \mod m_3\\
\]
令 $Q = (a_3-A)*M^{-1} $,则 \(k = Pm_3 + Q\)。
再将上式代入回 \(x = kM + A\),得 \(x = (Pm_3 + Q)M+ A = Pm_3M+QM+A\)。
又因为 \(M = m_1m_2\),所以 \(x = Pm_1m_2m_3 + QM + A\)。
也就是说 \(x \equiv QM + A \mod m_1m_2m_3\)。
然后……然后就这样啊。
一份 luoguP4243 的 AC 代码:
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll G = 3;
const int MAXN = 400000;
const ll MOD[3] = {469762049, 998244353, 1004535809};
//模数记不住怎么办?身为一名 OIer 啊,就要做自己最擅长的事情(指暴力打表)。
ll pow_mod(ll b, ll p, ll mod) {
ll ret = 1;
while( p ) {
if( p & 1 ) ret = ret*b%mod;
b = b*b%mod;
p >>= 1;
}
return ret;
}
ll mul_mod(ll a, ll b, ll mod) {
ll ret = 0;
while( a ) {
if( a & 1 ) ret = (ret + b)%mod;
b = (b + b)%mod;
a >>= 1;
}
return ret;
}
ll inv[3][3], M, k1, k2, Inv;
void init() {
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if( i != j ) inv[i][j] = pow_mod(MOD[i], MOD[j]-2, MOD[j]);
M = MOD[0]*MOD[1];
k1 = mul_mod(MOD[1], inv[1][0], M);
k2 = mul_mod(MOD[0], inv[0][1], M);
Inv = inv[0][2]*inv[1][2]%MOD[2];
}
ll CRT(ll a1, ll a2, ll a3, ll mod) {
ll A = (mul_mod(a1, k1, M) + mul_mod(a2, k2, M))%M;
ll K = (a3 + MOD[2] - A%MOD[2])%MOD[2]*Inv%MOD[2];
return ((M%mod)*K%mod + A)%mod;
}
//27 ~ 40 行,三模数 NTT 的精华 owo
ll f[3][MAXN + 5], g[3][MAXN + 5];
void ntt(ll *A, int n, int m, int type) {
for(int i=0, j=0;i<n;i++) {
if( i < j ) swap(A[i], A[j]);
for(int l=(n>>1);(j^=l)<l;l>>=1);
}
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
ll u = (type == -1) ? pow_mod(G, (MOD[m]-1)/s, MOD[m]) : pow_mod(G, (MOD[m]-1) - (MOD[m]-1)/s, MOD[m]);
for(int i=0;i<n;i+=s) {
ll p = 1;
for(int j=0;j<t;j++,p=p*u%MOD[m]) {
ll x = A[i+j], y = A[i+j+t]*p%MOD[m];
A[i+j] = (x + y)%MOD[m], A[i+j+t] = (x + MOD[m] - y)%MOD[m];
}
}
}
if( type == -1 ) {
ll inv = pow_mod(n, MOD[m]-2, MOD[m]);
for(int i=0;i<n;i++)
A[i] = A[i]*inv%MOD[m];
}
}
ll h[3][MAXN + 5];
int main() {
int n, m; ll p; init();
scanf("%d%d%lld", &n, &m, &p); n++, m++;
for(int i=0;i<n;i++) {
scanf("%lld", &f[2][i]);
f[1][i] = f[2][i] % MOD[1];
f[0][i] = f[2][i] % MOD[0];
}
for(int i=0;i<m;i++) {
scanf("%lld", &g[2][i]);
g[1][i] = g[2][i] % MOD[1];
g[0][i] = g[2][i] % MOD[0];
}
int len; for(len = 1;len < n+m-1;len <<= 1);
for(int i=0;i<3;i++) {
ntt(f[i], len, i, 1); ntt(g[i], len, i, 1);
for(int j=0;j<len;j++)
h[i][j] = f[i][j]*g[i][j]%MOD[i];
ntt(h[i], len, i, -1);
}
for(int i=0;i<n+m-1;i++)
printf("%lld ", CRT(h[0][i], h[1][i], h[2][i], p));
}
@拆系数 fft (mtt)@
对于题目所给定的模数 M,我们求出其平方根 \(S = \lfloor\sqrt{M}\rfloor\)。如果 M 在 10^9 范围内,则 S 在 3*10^4 左右。
然后我们将需要进行 fft 的系数 ai 拆成 \(a_i = \lfloor\frac{a_i}{S}\rfloor*S + (a_i\mod S) = b_i*S + c_i\) 的形式。
于是将两个多项式 A1, A2 作卷积可以转化为 B1, C1 与 B2, C2 之间分别作卷积,再通过上面的式子合并起来。
因为 B 和 C 的系数值域都在 3*10^4 的范围以内,所以 double 产生的误差不大会影响结果(看运气,建议还是用 long double)。
这样一共需要 7 次 fft(比上面那个三模数要少 2 次)
上面是拆系数 fft 的基本思路,但我们还可以进行进一步地优化。
这个优化的起始点在于观察到 fft 时我们只传了实数进去,而 fft 时我们运用的是复数进行运算,这样直观上就会产生一些“浪费”。
利用复数的虚数部分,我们可以将两个实系数多项式通过一次 fft 进行插值。
具体而言,假如我们要插值的是 n 项的多项式 A 和 B。
令 P = A + B*i, Q = A - B*i。再令 A'[k], B'[k], P'[k], Q'[k] 表示插值后第 k 位上的数。
然后我们开始推式子(其中 conj 是复数的共轭):
= \sum_{p=0}^{n-1}(a_p - b_p*i)*\omega_{n}^{k*p} \\
= \sum_{p=0}^{n-1}(a_p - b_p*i)*conj(\omega_{n}^{(n-k)*p}) (由单位根的性质)\\
= \sum_{p=0}^{n-1}conj(a_p + b_p*i)*conj(\omega_{n}^{(n-k)*p})\\
= \sum_{p=0}^{n-1}conj((a_p + b_p*i)*\omega_{n}^{(n-k)*p})\\
= conj(P'[n-k])
\]
又因为 A'[k] = (P + Q)/2, B'[k] = (P - Q)/(2*i) = i*(Q - P)/2,所以就实现了我们最初的目的。
由此可以将 7 次 fft 降为 5 次。
可以将还原的过程也两个合成一个,但是因为插完值过后的系数变为了复数,所以上述结论不再适用。
不过有更为简单的结论:令 R'[k] = A'[k] + B'[k]*i,则 R = A + B*i(但我好像不会证。。。打表试验搞出来的)(update in 2020/02/12:这个显然的。。。。)。
这样 5 次又少一次,变为了 4 次。
可以发现这是一个足够优秀的算法,因为我们的 ntt 至少也需要 3 次插值。
一样是 luoguP4243 的 AC 代码:
(double 误差过不了.jpg)
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN = 100000;
const long double PI = acos(-1);
typedef long long ll;
struct complex{
long double r, i;
complex(long double _r=0, long double _i=0):r(_r), i(_i){};
friend complex conj(complex x) {
return complex(x.r, -x.i);
}
friend complex operator +(complex x, complex y) {
return complex(x.r + y.r, x.i + y.i);
}
friend complex operator -(complex x, complex y) {
return complex(x.r - y.r, x.i - y.i);
}
friend complex operator *(complex x, complex y) {
return complex(x.r*y.r - x.i*y.i, x.r*y.i + y.r*x.i);
}
friend complex operator /(complex x, long double k) {
return complex(x.r/k, x.i/k);
}
};
void fft(complex *A, int n, int type) {
for(int i=0,j=0;i<n;i++) {
if( i < j ) swap(A[i], A[j]);
for(int k=(n>>1);(j^=k)<k;k>>=1);
}
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
complex u = complex(cos(PI/t), sin(type*PI/t));
for(int i=0;i<n;i+=s) {
complex p = complex(1, 0);
for(int j=0;j<t;j++,p=p*u) {
complex x = A[i+j], y = p*A[i+j+t];
A[i+j] = (x + y), A[i+j+t] = (x - y);
}
}
}
if( type == -1 ) {
for(int i=0;i<n;i++)
A[i] = A[i] / n;
}
}
int length(int x) {
int len; for(len = 1; len < x; len <<= 1);
return len;
}
complex a1[4*MAXN + 5], a2[4*MAXN + 5];
complex b1[4*MAXN + 5], b2[4*MAXN + 5];
complex Q[4*MAXN + 5], P[4*MAXN + 5];
int main() {
int n, m, p; scanf("%d%d%d", &n, &m, &p), n++, m++;
int len = length(n + m - 1), sq = sqrt(p);
for(int i=0;i<n;i++) {
int x; scanf("%d", &x), x %= p;
P[i] = complex(x/sq, x%sq);
}
for(int i=0;i<m;i++) {
int x; scanf("%d", &x), x %= p;
Q[i] = complex(x/sq, x%sq);
}
fft(P, len, 1), fft(Q, len, 1);
for(int i=0;i<len;i++) {
a1[i] = (conj(P[(len-i)%len]) + P[i])/2;
a2[i] = (conj(P[(len-i)%len]) - P[i])/2*complex(0, 1);
b1[i] = (conj(Q[(len-i)%len]) + Q[i])/2;
b2[i] = (conj(Q[(len-i)%len]) - Q[i])/2*complex(0, 1);
}
for(int i=0;i<len;i++) {
P[i] = a1[i]*b2[i] + a2[i]*b1[i];
Q[i] = a1[i]*b1[i] + complex(0, 1)*a2[i]*b2[i];
}
fft(Q, len, -1), fft(P, len, -1);
for(int i=0;i<len;i++)
if( i < n + m - 1 )
printf("%lld ", (ll(Q[i].r+0.5)%p*sq%p*sq%p + ll(P[i].r+0.5)%p*sq%p + ll(Q[i].i+0.5)%p)%p);
}
@8 - 例题与应用@
@分治 FFT@
算是 FFT 的一个简单的扩展吧。。。
其问题大致可以描述为:
\(C\) 是 \(A,B\) 的卷积,其中 \(B\) 是一开始就已知的。依次给出 \(a_0, a_1,\dots\) 的值,当给出 \(a_i\) 的值时,需要立即算出 \(c_i\) 的值。
解决方法就是使用 cdq 分治 + FFT。如果你不知道 cdq 分治是什么也没关系,只需要知道它是个分治就 OK。
假如我们已知 \([le, mid]\) 内所有的 \(A[i]\),则 \([le, mid]\) 对 \([mid+1, ri]\) 的贡献为:
\]
可以求出 k 的范围为 1 <= k <= ri-le。这是一个长度为 ri - le 的卷积,可以用 FFT 来优化。
我们递归时先递归左边,再卷积计算左边那一半对右边那一半的贡献,最后递归右边。
注意分治的区间长度不一定要是 2 的幂。
@例题@ : @codeforces - 553E@ Kyoya and Train
我写的题解(可能比较冗长……)
@多维卷积@
对于含有多个变量的多项式,比如 \(f(x, y) = a_{00}x^0y^0 + a_{01}x^0y^1 + ...+ a_{0m}x^0y^m + a_{10}x^1y^1 + ... + a_{nm}x^ny^m\)。
考虑怎么快速将它们相乘。
我们一样转为点值形式,通过代入单位根的二元组 \((w_n^i, w_m^j)\)(即令 \(x = w_n^i, y = w_m^j\) 得到的多项式的值)求出共 n * m 个点值,再对应位置相乘。
可以理解为将第一维相同的项放在一起,对第二维进行代入单位根;再将第二维相同的项放在一起,对第一位进行代入单位根。
逆变换同理。
也可以把 \(x^iy^j\) 看成 \(z^{i*(2*m+1)+j}\) 转为一维卷积来做。
一般运用在二维矩阵中的卷积问题,或是将二进制看成多维卷积 + 循环卷积来做。
@例题@ : @codechef - BUYLAND@ Buying Land。
这里有一份题解
这种卷积题居然在 5, 6 年前就有人研究了……真可怕。
@循环卷积@
有一类特殊的卷积长这样:
\]
我们将这类卷积称为循环卷积。
一般而言,循环卷积有两种解决方案。
一是使用正常的卷积,再把大于等于 n 的部分加到前面去。
二是将循环卷积转为点值表示,逐位相乘,再逆变换回去。
但是这个时候我们不能随便把卷积的长度变长变短,必须让它保持长度为 n。
注意到如果 \(x^n = 1\) 则 \(x^{n+i} = x^i\),相当于指数取模。这个取模和循环卷积的取模非常的相似。
因此,可以通过代入 n 次单位根将循环卷积转换为点值表示。但是如果 n 不是 2 的幂就无法使用分治解决了。
因为无法使用分治,这个方法是 \(O(n^2)\)。但是在多次乘法的时候它具有一定的优越性,比如矩阵快速幂。
第二种方案,说明循环卷积与正常卷积具有一定的统一性,或者说正常卷积本就是循环卷积的一种特例。
@例题@ : @codechef - BIKE@ Chef and Bike,是 2017 冬令营的题。
这里有一份题解
@多项式求逆,除法与取模@
@多点求值与快速插值@
@多项式开方,对数,指数,三角与幂函数@
从入门到进阶再到出门已经安排得明明白白了。
@总结 - 1@ 多项式乘法 —— FFT的更多相关文章
- 多项式乘法(FFT)学习笔记
------------------------------------------本文只探讨多项式乘法(FFT)在信息学中的应用如有错误或不明欢迎指出或提问,在此不胜感激 多项式 1.系数表示法 ...
- 【learning】多项式乘法&fft
[吐槽] 以前一直觉得这个东西十分高端完全不会qwq 但是向lyy.yxq.yww.dtz等dalao们学习之后发现这个东西的代码实现其实极其简洁 于是趁着还没有忘记赶紧来写一篇博 (说起来这篇东西的 ...
- 洛谷.3803.[模板]多项式乘法(FFT)
题目链接:洛谷.LOJ. FFT相关:快速傅里叶变换(FFT)详解.FFT总结.从多项式乘法到快速傅里叶变换. 5.4 又看了一遍,这个也不错. 2019.3.7 叕看了一遍,推荐这个. #inclu ...
- [uoj#34] [洛谷P3803] 多项式乘法(FFT)
新技能--FFT. 可在 \(O(nlogn)\) 时间内完成多项式在系数表达与点值表达之间的转换. 其中最关键的一点便为单位复数根,有神奇的折半性质. 多项式乘法(即为卷积)的常见形式: \[ C_ ...
- UOJ 34 多项式乘法 FFT 模板
这是一道模板题. 给你两个多项式,请输出乘起来后的多项式. 输入格式 第一行两个整数 nn 和 mm,分别表示两个多项式的次数. 第二行 n+1n+1 个整数,表示第一个多项式的 00 到 nn 次项 ...
- [HNOI2017] 礼物 - 多项式乘法FFT
题意:给定两个 \(n\) 元环,环上每个点有权值,分别为 \(x_i, y_i\).定义两个环的差值为 \[\sum_{i=0}^{n-1}{(x_i-y_i)^2}\] 可以旋转其中的一个环,或者 ...
- 【Luogu3808】多项式乘法FFT(FFT)
题目戳我 一道模板题 自己尝试证明了大部分... 剩下的还是没太证出来... 所以就是一个模板放在这里 以后再来补东西吧.... #include<iostream> #include&l ...
- 【模板】多项式乘法(FFT)
题目描述 给定一个n次多项式F(x),和一个m次多项式G(x). 请求出F(x)和G(x)的卷积. 输入输出格式 输入格式: 第一行2个正整数n,m. 接下来一行n+1个数字,从低到高表示F(x)的系 ...
- 【Luogu3803】多项式乘法FFT(FFT)
题目戳我 一道模板题 自己尝试证明了大部分... 剩下的还是没太证出来... 所以就是一个模板放在这里 以后再来补东西吧.... #include<iostream> #include&l ...
随机推荐
- jquery版本冲突问题
开发过程中,我们常常会遇到这种问题: 我们需要做一个新的功能,需要插入一个新的插件,使用新版的jquery,但原版的jquery已经被很多函数使用,不能直接修改.这时候我们可以现将$转移给$170($ ...
- HTML连载58-绝对定位的参考点以及注意事项
一.绝对定位参考点 1.规律: (1)默认情况下所有的绝对定位的元素,无论有没有祖先元素,都会以body作为参考点. <style> .box1{ width: 300px; height ...
- Spring_Hibernate整合准备
1,Spring整合Hibernate 整合什么? 1)由IOC容器来生成Hibernate的SessionFactory 2)让Hibernate使用上Spring的声明式事务 2,整合步骤 1 ...
- 直接在安装了redis的Linux机器上操作redis数据存储类型--set类型
一.概述: 在Redis中,我们可以将Set类型看作为没有排序的字符集合,和List类型一样,我们也可以在该类型的数据值上执行添加.删除或判断某一元素是否存在等操作.需要说明的是,这些操作的时间复 ...
- JAVA读取文件操作时路径的斜杠问题
java中的路径一般用"/"windows中的路径用"\"linux,unix中的路径一般用"/"其中java中"/"等 ...
- NOIP模拟17.9.21
NOIP模拟17.9.21 3 58 145 201 161.5 样例输出21.6 数据规模及约定对于40% 的数据,N <= 20对于60% 的数据,N <= 1000对于100% 的数 ...
- DOM 创建元素 删除元素(结点)
创建新的 HTML 元素 如需向 HTML DOM 添加新元素,您必须首先创建该元素(元素节点),然后向一个已存在的元素追加该元素. <script> var para=document. ...
- 几道面试题-考察JS的运用
1.定义一个方法,传入一个string类型的参数,然后将string的每个字符间加个空格返回,比如: spacify('hello world') // => 'h e l l o w o r ...
- Codeforces 449B
题目链接 B. Jzzhu and Cities time limit per test 2 seconds memory limit per test 256 megabytes input sta ...
- I / O流 类
一.概述 1 基本概念 I/O就是Input/Output的简写,也就是输入/输出的含义. I/O流就是指像流水一样源源不断地进行读写的过程. 2 基本分类 根据读写数据的单元分为:字节流 和 ...