涉及了拉插,FFT,NTT,生成函数入门等多项式儿童套餐。
〇. 基本概念:
-
多项式: 形如 的 n 次函数。
-
度(Degree):对于一个多项式 ,称其最高次项的次数为该多项式的度,记作 .
多项式四则运算: 加减乘很好记,对于除法运算我们同样引入两个概念:
-
多项式的逆元(Inverse Element): 对于多项式 ,若存在 满足:,
则称 为 在模 意义下的逆元 ,记作 。 -
余数和商:对于 存在 使得:
亦可表示为:
- 幂级数:指在级数的每一项均为与级数项序号 n 相对应的以常数倍的(x-a)的n次方。
说人话就是有无穷项的多项式。而我们主要探讨的是 形式多项式,即关注其 系数序列,主元 x 只是走个形式。
- 生成函数:可简单理解为 的幂级数
其中 时被称作 普通生成函数 , 被称作指数生成函数,具体知识会在第四章介绍。
- x 的 n 次下降幂:
壹. 拉格朗日插值:
1. 插值:
给定一系列点(共 n+1 个),确定一个 n 次多项式使得这些点在该函数图像上。
In other words, 就是将 点值表示 转换为 系数表示 的过程。
2. 拉插:
实质是对于每个点 都凑一个式子,在联立求出所有的系数。
那怎么凑这个式子呢?由多项式 余数和商 的定义,我们不难给出一个引理:
上式可以作差证明。对每个点都列出该式,再套上 中国剩余定理 的公式,我们便可以得到拉插的一般形式:
其中 为 ,注意到其中的变量可能有负数,取模注意一下。时间复杂度 .
3. 拉插特例:
当 连续时,我们可以通过预处理阶乘,将时间复杂度降到 ,尤其是可自选点值的题,往往通过选择 来快速求解。
如例题: 求 ,已知 .
经推导可知 是一个 k+1 次多项式,我们取 作为点值带入,则原式变形为:
对于其中各项:
-
可以边求值边计算得出;
-
后面一坨的分子可以预处理 前缀和 与 后缀和 ,把 那一点挖掉求得;
-
而对于分母,我们可以注意到它对于任意一个 i 都是连续的:从负值 k+2-i 开始,到正值 i-1 结束,我们可以通过预处理阶乘将其拆解为: ,注意一下 k+2-i 若为奇数,就要多一个负号。
写的时候好像爆了 ll,套了一个龟速乘。
貳.快速傅里叶变换:
1. 概念:
给定两个不高于 n 次的多项式 ,可以在 内求出 各个系数的算法。
其实质是利用 复数单位根 的种种性质,将两函数的系数表示转化为点值表示,在通过 逆变换 求出新函数的点值表示.
2. DFT 的实现:
对于两函数的系数表示,利用复数单位根性质将 带入,进行奇偶项分治,并最终求出整个函数对应点值的过程。
设 为当前分治总函数的点值序列, 为奇数项分治后的序列, 为偶数项分治后的序列,根据性质:
不断向上合并答案,最终即可得到 的点值序列,对应相乘即可。
3. IDFT 的实现:
即 DFT 的逆运算,将求出的 点值表示 转换为 系数表示。
可以通过 单位根求逆 或者 构造法 证明:把 DFT 中的 换成 ,做完之后除以 n 即可实现 IDFT。
#include <bits/stdc++.h>
#define FO(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout)
#define cle(x) memset(x,0,sizeof(x))
#define inf 2147483647
#define ll long long
using namespace std;
const int maxn=2100000;
const double pi=acos(-1);
int read()
{ int x=0; char ch=getchar(); bool f=0;
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<3)+(x<<1)+(ch^48);
return f?-x:x;
}
struct cp{
cp (double X=0,double Y=0) {x=X; y=Y;}
double x,y;
cp operator + (cp const &b) const
{return cp(x+b.x,y+b.y);}
cp operator - (cp const &b) const
{return cp(x-b.x,y-b.y);}
cp operator * (cp const &b) const
{return cp(x*b.x-y*b.y,y*b.x+x*b.y);}
}f[maxn],g[maxn],y[maxn];
int n,m;
void fft(cp *f,int l,int c)
{
if (l==1) return;
cp *L=f,*R=f+l/2;
//指针可直接 引用 原数组 ,从而快速对原函数进行奇偶分项
for (int i=0;i<l;i++) y[i]=f[i];
for (int i=0;i<l/2;i++) L[i]=y[i<<1],R[i]=y[i<<1|1];
fft(L,l/2,c); fft(R,l/2,c);
cp w0( cos(2*pi/l),c*sin(2*pi/l) ), b(1,0);
for (int k=0;k<l/2;k++) {
cp t=b*R[k]; b=b*w0;
y[k]=L[k]+t; y[k+l/2]=L[k]-t;
//求出对应的点值表示
//用 Y,不能用 F 存!否则会覆盖L,R数组导致 F 的改变!
}
for (int i=0;i<l;i++) f[i]=y[i];
//重新覆盖,原函数转化为点值,且不影响后续操作
}
int main()
{
n=read(),m=read();
for (int i=0;i<=n;i++) f[i].x=read();
for (int i=0;i<=m;i++) g[i].x=read();
for (m+=n,n=1;n<=m;n<<=1);
fft(f,n,1);
fft(g,n,1); // 操作对象,长度,操作参数
for (int i=0;i<=n;i++) f[i]=f[i]*g[i];
fft(f,n,-1);
for (int i=0;i<=m;i++) printf("%d ",(int)(f[i].x/n+0.49));
}
4. 位逆序置换优化:
我们注意到原版 FFT 进行了大量数组复制操作,加上是递归版本自带的小常数,很容易被卡。
通过分析递归底层的 数组,我们惊讶地发现每个位置编号对应的正是原位置编号的 二进制翻转。
利用数位 DP 的思想,我们设 为 二进制数 x 进行二进制翻转后的大小,且长度是固定的 (就像我们预先设计的那样)。那么得到如下转移方案:
-
由于从小到大求解,我们已经得知了 的大小,x>>1 此时代表着 二进制 x 除去个位后的值,而移位的同时导致最高位多了一个前导 0 。为了消除该影响,我们只需要将其再次移位即可。
-
对于个位,若其为 1 我们要将其翻转至最高位,直接或上 即可。
综上,转移方程为:
同时我们也得到了非迭代版的FFT:
#include <cstring>
#include <cmath>
#include <cstdio>
#include <algorithm>
#define inf 2147483647
#define maxn 2100000
using namespace std;
const double pi=acos(-1);
struct cp{
cp (double xx=0,double yy=0){x=xx,y=yy;}
double x,y;
cp operator + (cp const &B) const
{return cp(x+B.x,y+B.y);}
cp operator - (cp const &B) const
{return cp(x-B.x,y-B.y);}
cp operator * (cp const &B) const
{return cp(x*B.x-y*B.y,x*B.y+y*B.x);}
}f[maxn],g[maxn],y[maxn];
int n,m,ro[maxn];
void fft(cp *f,int c)
{
for (int i=0;i<n;i++) if (i<ro[i]) swap(f[i],f[ro[i]]);
//直接得到递归至 1 的数组序列,非迭代向上转移
for (int h=2;h<=n;h<<=1)
{
cp w0( cos(2*pi/h), c*sin(2*pi/h) );
for (int j=0;j<n;j+=h){
cp buf(1,0);
for (int i=j;i<(j+(h>>1));i++) {
cp u=f[i]; cp t=buf*f[i+(h>>1)];
f[i]=u+t; f[i+(h>>1)]=u-t;
buf=buf*w0;
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=0;i<=n;i++) scanf("%lf",&f[i].x);
for (int i=0;i<=m;i++) scanf("%lf",&g[i].x);
for (m+=n,n=1;n<=m;n<<=1);
for(int i=0;i<n;i++) ro[i]=(ro[i>>1]>>1)|((i&1)?n>>1:0);
//这里一定要带括号!! 优先级问题已经挂了亿次了!
fft(f,1);
for (int i=0;i<=m;i++) printf("%d ",(int)(f[i].x/n+0.49)); fft(g,1);
for (int i=0;i<=n;i++) f[i]=f[i]*g[i];
fft(f,-1);
for (int i=0;i<=m;i++) printf("%d ",(int)(f[i].x/n+0.49));
}
叁. 快速数论变换:
一. 原根:
- 阶: 设 ,且 ,使 成立的 最小 正整数 ,称为 模 的阶,记为 。
这篇博客给出了许多关于阶的重要性质。
- 原根的定义:设 ,且 ,若 ,则称 是模 的原根。
不加证明地给出:膜 m 存在原根 的充要条件是.
很显然的是,如果 m 存在一个原根 g,那么对于 的任意质因数 都有 .
- 假设我们能快速地(?) 求出膜 m 下最小的原根 g,尝试着以此求出所有膜 m 的原根:
已知: .
那么对于 ,我们都可以来讨论 是原根的可能性:
-
若 ,由欧拉定理我们可将其转换为 ,也就轮不到 s 了。
-
否则设 为 的质因子,若 ,则,与二式矛盾。
综上,当且仅当 时, 是模 的原根。根据定义,模 的原根有 个。
二. NTT 与 FFT 的转换
我们之所以选择 NTT 而非 FFT ,是因为后者由于使用单位根的关系,精度多多少少受到影响,加上部分数论题目要求取模的限制,找一个膜意义下的 “单位根”替身 未尝不可。
我们盯上了 原根,由于其阶的周期性与单位根的周期性匹配的非常好,所以对于一个质数 ,我们可以保证 其膜 p 意义下的原根 与 是等价的。
由于通常我们取 ,所以若一个质数减一后含有很多 2 的幂,那他就适合做 NTT 的模数 p。 ( 比如说 )
给出递归版的 NTT(因为非递归满地都是):
#include <bits/stdc++.h>
#define FO(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout)
#define cle(x) memset(x,0,sizeof(x))
#define inf 2147483647
#define ll long long
using namespace std;
const ll maxn=2100000,mod=998244353;
const double pi=acos(-1);
ll read()
{ ll x=0; char ch=getchar(); bool f=0;
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<3)+(x<<1)+(ch^48);
return f?-x:x;
}
ll n,m,f[maxn],g[maxn],y[maxn],inv3;
ll qpow(ll x,ll k)
{
ll X=1; for (;k;k>>=1,x=(x*x)%mod) if (k&1) X=(X*x)%mod; return X;
}
void ntt(ll *f,ll l,bool c)
{
if (l==1) return;
ll *L=f,*R=f+l/2;
for (ll i=0;i<l;i++) y[i]=f[i];
for (ll i=0;i<(l>>1);i++) L[i]=y[i<<1],R[i]=y[i<<1|1];
ntt(L,l/2,c); ntt(R,l/2,c);
ll w0=qpow(c?3:inv3 , (mod-1)/l), w=1;
for (ll i=0;i<(l>>1);i++) {
ll t=w*R[i]%mod; w=(w*w0)%mod;
y[i]=(L[i]+t)%mod, y[i+(l>>1)]=(L[i]-t+mod)%mod;
//用 Y,不能用 F 存!会覆盖L,R数组!
}
for (ll i=0;i<l;i++) f[i]=y[i];
}
int main()
{
n=read(), m=read(); inv3=qpow(3,mod-2);
for (ll i=0;i<=n;i++) f[i]=read();
for (ll i=0;i<=m;i++) g[i]=read();
for (m+=n,n=1;n<=m;n<<=1);
// for (ll i=0;i<n;i++) r[i]=(r[i>>1]>>1) | ((i&1)?n>>1:0); //括号括起来!
ntt(f,n,1); ntt(g,n,1);
for (ll i=0;i<=n;i++) f[i]=(f[i]*g[i])%mod;
ntt(f,n,0); ll invn=qpow(n,mod-2);
for (ll i=0;i<=m;i++) printf("%lld ",(f[i]*invn)%mod);
}
这玩意比 FTT 还慢,仅供娱乐,请勿参考。
肆. 多项式儿童套餐
仅仅知道如何求出两个多项式的卷积没啥用,对于大多题目只有知道如何转化才能利用所学的知识解题。
但学的内容又比较肤浅,既没有 多项式计数 也没有 多项式指数对数计算,多项式快速幂 这些高阶算法。算不上全家桶,就当是儿童套餐吧。
一. 生成函数入门
对于一个序列 ,它的生成函数记做,注意到 可以有限项,也可以无限项。
但不论是哪一种情况,如果我们能搞出生成函数的系数序列,我们也就推出了 的通项,这是确定的。
1. 生成函数的四则运算
已知序列 a,b 的生成函数为,则对于序列 a+b 的生成函数自然是 ,减法同理。
对于乘法,我们可以表示为 ,注意到 的下标和始终为 n,这对应了多项式里的卷积形式。同时我们也称序列 的生成函数为 .
除法暂且不谈。
2. 生成函数的封闭形式
-
简单的例子(1): ,系数列为 。可构造出等式,解得.
-
简单的例子(2): ,由泰勒展开可知
-
简单的例子(3): ,系数列为,可构造 ,解得.
-
复杂的例子: ,系数列为,由卷积性质可知: ,即.
-
经典的例子(1):斐波拉契数列的生成函数
已知 ,生成函数为 ,凑项可得:
解得:
利用配项法,,底下的 a,b用韦达,上面通分联立求解,可知:
我们还可以注意到,提了一个 后里面满足简单例子(2)的格式,说明其展开式是两个公比为 等比数列,分别代入整理可解得最终结果为:
但实际并没有什么卵用。。。应该吧。
指数型母函数:
对于一个多重集,其中 a1 重复 n1 次,a2 重复 n2 次,…若从中取出 r 个 排列不同的方案 所对应的母函数即为:
每一个括号内都称作该项的 指数型母函数,最终对应的答案为 。
二.多项式求逆
咕咕咕。
三. 例题:K 阶差分前缀和
题目大意: 给定一序列 与 ,求其 K 阶差分 or 前缀和序列。
解析:
我们对于 给出他的生成函数 ,并尝试构造一个函数 ,使二者的卷积就是我们答案。
我们容易想到的是,对于 的前缀和,我们可以构造 ,由卷积定义我们可以得到 ,即其第 i 项对应了 的一阶前缀和。
同理,一阶差分要求前后两项相减,不难得到 便可满足条件。
那么我们应该如何把它拓展到 k 阶呢?由于卷积的结合律,若要做 k 次前缀和,就相当于 ,也就是 。对于 阶差分,也同样可以得到。
而两者的处理方式却又微妙的差别:
-
对于差分的情况 运用二项式定理,其系数表示为 ,根据 n 的范围完全可以线性递推。
-
对于前缀和的情况,运用广义二项式定理,其系数表示为
把 放进去,得到 ,发现也可以线性递推。
然后把 和 一卷就可以了。
#include <bits/stdc++.h>
#define FO(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout)
#define cle(x) memset(x,0,sizeof(x))
#define inf 2147483647
#define ll long long
using namespace std;
const ll maxn=2000100,mod=1004535809;
ll read(ll p)
{ ll x=0; char ch=getchar(); bool f=0;
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(( (x<<3)+(x<<1) )%p+(ch^48))%p;
return f?-x:x;
}
ll L,n,k,t,a[maxn],inv3;
ll r[maxn],f[maxn],inv[maxn];
ll qpow(ll x,ll k){ ll X=1ll; for (;k;k/=2,x=(x*x)%mod) if (k&1) X=(X*x)%mod; return X%mod; }
void ntt(ll *f,bool c)
{
for (int i=0;i<n;i++) if (i<r[i]) swap(f[i],f[ r[i] ]);
for (int l=2;l<=n;l<<=1) { //等于号不能丢,不然不能合并最后一步
ll w0=qpow( c?3:inv3 , (mod-1)/l );
for (int i=0;i<n;i+=l) {
ll w=1;
for (int k=i;k<(i+l/2);k++,w=(w*w0)%mod) {
ll t = (w*f[k+l/2])%mod , u = f[k] ;
f[k] = (u+t)%mod; f[k+(l>>1)] = (u-t+mod)%mod;
}
}
}
if (!c) { ll invn= qpow(n,mod-2); for (int i=0;i<n;i++) f[i]=(f[i]*invn)%mod; }
}
void cal()
{
L=2*n; for (n=1;n<L;n<<=1);
for (int i=1;i<n;i++) r[i]= (r[i>>1]>>1) | ((i&1)?(n>>1):0);
ntt(a,1); ntt(f,1);
for (int i=0;i<n;i++) a[i] = (a[i]*f[i])%mod; ntt(a,0);
}
int main()
{
n = read(mod); k = read(mod); t = read(mod); inv3 = qpow(3,mod-2);
for (ll i=0;i<n;i++) a[i] = read(mod); inv[1]=1ll;
for (ll i=2;i<n;i++) inv[i]= (ll)(mod-mod/i)*inv[mod%i]%mod;
if (t == 1) { f[0]=1; for (ll i=1;i<n;i++) f[i] = ( ((mod-f[i-1])*inv[i])%mod * (k-i+1+mod)%mod )%mod; }
else { f[0]=1; for (ll i=1;i<n;i++) f[i] = ( (f[i-1]*inv[i])%mod * (k+i-1+mod)%mod )%mod; }
cal(); for (int i=0;i<L/2;i++) printf("%lld ",a[i]);
}