多项式乘法(FTT、NTT)
诈尸。
摆了一个多月没写 boke 的 BE 是屑。
\(\mathbf{基础知识/模板}\)
因为太屑了所以就直接放巨佬们的博客力。
[学习笔记&教程] 信号, 集合, 多项式, 以及各种卷积性变换 (FFT,NTT,FWT,FMT)
多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/例题与常用套路【入门】
FFT
code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Reg register
#define ll long long
using namespace std;
const int maxn=4110000;
const double Pi=acos(-1);
int n,m,lim=1,L,R[maxn];
struct node{
double x,y;
node (double X=0,double Y=0){
x=X,y=Y;
}
node friend operator +(node A,node B){
node res;
res.x=A.x+B.x;
res.y=A.y+B.y;
return res;
}
node friend operator -(node A,node B){
node res;
res.x=A.x-B.x;
res.y=A.y-B.y;
return res;
}
node friend operator *(node A,node B){
node res;
res.x=(A.x*B.x-A.y*B.y);
res.y=(A.x*B.y+A.y*B.x);
return res;
}
}A[maxn],B[maxn];
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=(s<<1)+(s<<3)+(ch^48);
ch=getchar();
}
return s*w;
}
inline void FFT(node *a,double typ){
for(Reg int i=0;i<lim;++i){
if(i<R[i]) swap(a[i],a[R[i]]);
}
for(Reg int j=1;j<lim;j<<=1){
int p=(j<<1);
node t=node(cos(2.0*Pi/p),typ*sin(2.0*Pi/p));
for(Reg int k=0;k<lim;k+=p){
node T=node(1,0);
for(Reg int l=0;l<j;++l,T=T*t){
node Nx=a[k+l],Ny=T*a[k+j+l];
a[k+l]=Nx+Ny;a[k+j+l]=Nx-Ny;
}
}
}
}
int main(){
n=read(),m=read();
for(Reg int i=0;i<=n;++i) A[i].x=read();
for(Reg int i=0;i<=m;++i) B[i].x=read();
while(lim<=n+m) lim<<=1,++L;
//printf("%.8lf\n",Pi);
for(Reg int i=0;i<lim;++i) R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
// for(Reg int i=0;i<lim;++i){
// printf("%d ",R[i]);
// }
// printf("\n");
FFT(A,1),FFT(B,1);
for(Reg int i=0;i<lim;++i) A[i]=A[i]*B[i];
FFT(A,-1);
for(Reg int i=0;i<=n+m;++i) printf("%lld ",(ll)(A[i].x/lim+0.5));
printf("\n");
}
NTT
code
inline ll qpow(ll A,ll B){
ll Ans=1;
while(B){
if(B&1) Ans=Ans*A%mod;
A=A*A%mod;
B>>=1;
}
return Ans;
}
inline void NTT(ll *a,ll typ){
for(Reg int i=0;i<lim;++i){
if(i<R[i]) swap(a[i],a[R[i]]);
}
for(Reg int j=1;j<lim;j<<=1){
ll t=qpow(G,(typ*((mod-1)/(j<<1))+mod-1)%(mod-1));
for(Reg int k=0;k<lim;k+=(j<<1)){
ll T=1;
for(Reg int l=0;l<j;++l,T=T*t%mod){
ll Nx=a[k+l],Ny=a[k+l+j]*T%mod;
a[k+l]=(Nx+Ny)%mod;
a[k+l+j]=(Nx-Ny+mod)%mod;
}
}
}
if(typ==-1){
ll r=qpow(lim,mod-2);
for(Reg int i=0;i<lim;++i) a[i]=a[i]*r%mod;
}
}
分治FFT
code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Reg register
#define ll long long
using namespace std;
const int maxn=410000;
const ll mod=998244353,G=3;
int n,R[maxn];
ll g[maxn],A[maxn],B[maxn],f[maxn];
inline ll qpow(ll At,ll Bt){
ll Ans=1;
while(Bt){
if(Bt&1) Ans=Ans*At%mod;
At=At*At%mod;
Bt>>=1;
}
return Ans;
}
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=(s<<1)+(s<<3)+(ch^48);
ch=getchar();
}
return s*w;
}
inline void NTT(ll *a,int lim,ll typ){
for(Reg int i=0;i<lim;++i){
if(i<R[i]) swap(a[i],a[R[i]]);
}
for(Reg int j=1;j<lim;j<<=1){
ll t=qpow(G,(typ*((mod-1)/(j<<1))+mod-1)%(mod-1));
for(Reg int k=0;k<lim;k+=(j<<1)){
ll T=1;
for(Reg int l=0;l<j;++l,T=T*t%mod){
ll Nx=a[k+l],Ny=T*a[k+l+j]%mod;
a[k+l]=(Nx+Ny)%mod;
a[k+l+j]=(Nx-Ny+mod)%mod;
}
}
}
if(typ==-1){
ll r=qpow(lim,mod-2);
for(Reg int i=0;i<lim;++i) a[i]=a[i]*r%mod;
}
}
inline void Calc(ll *a,ll *b,int lim){
NTT(a,lim,1),NTT(b,lim,1);
for(Reg int i=0;i<lim;++i) a[i]=a[i]*b[i]%mod;
NTT(a,lim,-1);
}
inline void solve(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
solve(l,mid);
int up=r-l+1,lim=1,L=0;
while(lim<=up) lim<<=1,++L;
for(Reg int i=0;i<lim;++i) R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
memset(A,0,sizeof(ll)*(lim+10));
memset(B,0,sizeof(ll)*(lim+10));
for(Reg int i=0;i<mid-l+1;++i) A[i]=f[i+l];
for(Reg int i=1;i<=r-l;++i) B[i]=g[i];
Calc(A,B,lim);
for(Reg int i=mid-l+1;i<r-l+1;++i) f[i+l]=(f[i+l]+A[i])%mod;
solve(mid+1,r);
}
int main(){
n=read();
for(Reg int i=1;i<n;++i) f[i]=g[i]=read();
solve(1,n),f[0]=1;
for(Reg int i=0;i<n;++i) printf("%lld ",f[i]);
printf("\n");
return 0;
}
\(\mathbf{习题}\)
快速傅立叶之二
题意:
对 $\forall k \in [0,n) $,求 \(c_k=\sum_{k\le i <n}a_i\times b_{i-k}\)
\(n\le 10^5\)
思路:
令 \(m=n-1\) ,有:
设 \(a^{'}_i=a_{m-i}\),则有:
把数组 \(a\) 翻转过来卷积即可。
力
题意:
给出 \(n\) 个数 \(q_1,q_2, \dots q_n\),定义
对 \(1 \leq i \leq n\),求 \(E_i\) 的值。
\(n \le 10^5\),\(0 < q_i < 10^9\)。
思路:
设 \(f_i=q_i\) ,\(g_i=\frac{1}{i^2}\),则:
发现前一个式子已经是卷积的形式,现在只关注后面的式子:
设 \(f^{'}_i=f_{n-i}\) ,则有:
卷积即可。
Triple
题意:
现有 \(n\) 个不同价值的物品,可以分别选一个,两个,三个,分别求物品价值总和为 \(x\) 的方案数。 注意 \((a,b),(b,a)\) 视为一种方案,\((a,b,c),(b,c,a),(c,a,b),(c,b,a),(b,a,c),(a,c,b)\) 视为一种方案。
\(n\le 4\times 10^4\)
思路:
设一个物品选择一次的生成函数为 \(A(x)=x^{a_1} +x ^{a_2}+x ^{a_3}...+x^{a_n}\)
选择两次的生成函数为 \(B(x)=x^{2a_1}+x^{2a_2}+x^{2a_3}...+x^{2a_ n}\)
选择三次的生成函数为 \(C(x)=x^{3a_1}+x^{3a_2}+x^{3a_3}...+x^{3a_n}\)
可以通过容斥得出选择两个物品的函数和选择三个物品的函数。
选择两个物品的函数:
选择三个物品的函数:
因此目标函数则是:
一次 \(FFT\) 即可。
万径人踪灭
题意:
给定一个仅包含字符 \(a,b\) 的字符串,要求选取一个子序列,使得:
-
位置和字符都关于某条对称轴对称
-
不能是连续的一段
求合法的子序列个数,结果对 \(10^9+7\) 取模。
\(n\le 10^5\)
思路:
将答案转化为回文子序列个数减去回文子串个数。
首先可以用 \(\operatorname{manacher}\) 求回文子串个数。
设奇回文子序列的回文中心为 \(i\) ,关于该中心对称的字符对数为 \(x\),那么该回文中心形成的回文子序列数即为 \(2^{x+1}-1\) (每一对可选可不选,再减去全都不选的方案)。偶回文子序列则是 \(2^x-1\) (因为少了一个回文中心)。
将数组 \(A\) 中 \(a\) 字符出现的位置的系数设为 \(1\) ,数组 \(B\) 反之。这两个数组自己对自己卷积得到的 \(A_i\) 和 \(B_i\) 实际上是 \(a,b\) 以 \(\frac{i}{2}\) 为对称中心的字符对数(注意去重)。又注意到奇偶回文序列的差异,关于某一位置对称的字符对数实际上应为 \(A_i+B_i+(i\&1)\oplus 1\) 。
求出来的回文子序列个数减去回文子串个数即为答案。
序列统计
题意:
给定 \(n,m,x\) 和一个集合 \(S\),集合内的元素都是小于 \(m\) 的非负整数 。现在用 \(S\) 中的元素构造长度为 \(n\) 的序列,求数列所有数的乘积对 \(m\) 取模结果为 \(x\) 的序列数,答案对 \(1004535809\) 取模。
\(n\le 10^9 ,m\le 8\times 10^3,\ m\ 为质数,1\le x \le m-1\)
思路:
设序列长度为 \(x\) ,满足 \(\prod_{k=1}^{x} a_k \equiv i \pmod m\) 的序列数 \(f_{x,i}\) ,那么有:
发现 \(f\) 数组可以用类似快速幂的方式计算,复杂度 \(O(m^2 \log n)\), \(60pts\)。
观察到上式把乘法换成加法就变成了卷积形式。
考虑对数函数,用 $\log_a x $ 替换 \(x\) 。由于 \(\log _a x+ \log_a y= \log _a (x\times y)\) ,我们可以通过 \(x\to log_a x\) 的转化,卷积之后得到 \(log _a (x\times y)\) 的系数。
在模意义下,可以令 $ log_a (a^x \bmod m)=x$ 来建立映射关系。但要求 \(a^0,a^1...a^{m-2} \bmod m\) 不相同,不难想到 \(a\) 应该为 \(m\) 的原根。
此时注意当 \(x \in [0,m-2]\) 时,不存在 \(a^x \equiv 0 \pmod m\) 的情况。因此处理时直接把 \(0\) 省略即可(因为 \(0\) 不会被询问贡献)。
大致流程:
-
找出 \(m\) 的原根,建立 \(log_a x\) 与 \(x\) 的映射关系
-
快速幂 + NTT 计算出 \(f_n\)
-
输出 \(f_{n,log_a x}\)
求和
题意:
求:
其中 \(S(i,j)\) 指第二类斯特林数。
\(n\le 10^5\)
思路:
了解到第二类斯特林数的通项公式:
(容斥得到将 \(n\) 个不同物品放进 \(m\) 个不同盒子且盒子不空的方案数,再除以 \(m!\) 就是将 \(n\) 个不同物品放进 \(m\) 个相同盒子且盒子不空的方案数)
然后开始推式子。
首先改变枚举顺序:
设 \(F_i=\sum_{j=0}^{n} S(j,i)\),并将通项公式代入得:
设 \(f_i=\frac{(-1)^i}{i!},g_i=\frac{\sum_{j=0}^{n}i^j}{i!}\) ,因此就有:
特殊注意 \(g_0=1,g_1=n+1\) 。
染色
题意:
为了报答小 C 的苹果,小 G 打算送给热爱美术的小 C 一块画布,这块画布可以抽象为一个长度为 \(n\) 的序列, 每个位置都可以被染成 \(m\) 种颜色中的某一种。
然而小 C 只关心序列的 \(n\) 个位置中出现次数恰好为 \(S\) 的颜色种数, 如果恰好出现了 \(S\) 次的颜色有 \(k\) 种, 则小 C 会产生 \(w_k\) 的愉悦度。
小 C 希望知道对于所有可能的染色方案,他能获得的愉悦度的和对 \(1004535809\) 取模的结果是多少。
思路:
大力容斥。
暴力の容斥
设有 \(i\) 种颜色分别出现了 \(S\) 次,则选择 \(i\) 种颜色的方案数为 \(\binom{m}{i}\),选择 \(i\times S\) 位置的方案数为 $\binom{n}{i\times S} \frac{(i\times S)!}{(S!)^i} $。
显然剩下的位置只能填除这 \(i\) 种颜色以外的其他颜色,但其他颜色的出现次数不能恰好出现 \(S\) 次。通过容斥“至少有 \(j\) 种其他颜色出现了 \(S\) 次”得出关于其他颜色的合法方案数,即:
因此 \(i\) 种颜色分别出现 \(S\) 次的方案数为:
最终的答案是:
复杂度 \(O(n^2)\) 。
优雅の容斥
设 \(f_i\) 为至少有 \(i\) 种颜色出现了 \(S\) 次的方案数,有:
然后通过容斥得到恰好有 \(i\) 种颜色出现了 \(S\) 次的方案数:
然后就可以 NTT 了。
当然通过上面的式子(暴力の容斥)也能推出来一样的卷积形式...
城市规划
题意:
求 \(n\) 个点的有标号简单无向连通图数目(无重边无自环)。
\(n\le 1.2\times 10^5\)
思路:
考虑 \(\operatorname{dp}\) 。
设 \(f_i\) 为点数为 \(i\) 的无向连通图数目,计算时用总的无向图数目减去不连通的无向图数目。点数为 \(i\) 的简单无向图最多有 \(\frac{i\times (i-1) }{2}\) 条边,每条边可选可不选,因此总的无向图数目为 \(2^{\frac{i\times (i-1) }{2}}\) 。关于不连通的无向图,可以以 \(1\) 号节点为基准点,每次枚举 \(1\) 号节点的连通块大小,其他点单独构成一个无向图。因此有:
对后面的式子变换一下形式:
可以用分治 FFT 。
当然写多项式求逆也没问题。
图的价值
题意:
定义一个简单无向图(可以不连通)的价值为每个点的度数的 \(k\) 次方之和。求 \(n\) 个点的所有简单无向图的价值之和。
\(n\le 10^9,k\le 2\times 10^5\)
思路:
容易发现:
(只关注一个点,枚举度数 \(i\) ,考虑剩下 \(n-1\) 个点里选 \(i\) 个连边的方案,再让剩下 \(n-1\) 个点构成简单无向图,又因为这 \(n\) 个点是等价的,所以要乘上 \(n\))
但是 \(n\) 太大了, \(k\) 又较小,这提示我们思考复杂度关于 \(k\) 的算法。
发现有:
(把 \(k\) 个不同小球放到 \(i\) 个不同盒子里,等价于枚举非空的盒子 \(j\),将 \(k\) 个不同小球放进 \(j\) 个不同盒子里的方案数,即第二类斯特林数 \(S(k,j)\) 乘上 \(j!\) )
把式子带进去得到:
(考虑含义:从 \(n-1\) 个物品中选出 \(j\) 个再选出 \(i\) 个等价于从 \(n-1\) 个物品中直接选出 \(i\) 个剩下的可以选或不选)
因为当 \(i>k\) 时 \(S(k,i)=0\) ,所以最大枚举到 \(\min(k,n-1)\) 即可。
\(S(k,i)\) 可以通过第二类斯特林数的通项公式预处理出来( NTT )。