1074多项式展开 题解
author : uniecho1
思路
其实一上来我是心存幻想,希望能不用多项式乘法切掉的…
然后简单算了一下,如果用线性求逆元预处理组合数,上二项式展开,再怎么样都是O(n2)O(n^2)O(n2)级别…
所以肯定是要推出卷积形式上多项式乘法了
(顺便一提,这个题涉及到对998244353998244353998244353取模了,必然不会是快速傅里叶变换,而是快速数论变换)
如果A=0A=0A=0的话就直接输出就行了,我们只考虑A!=0A!=0A!=0的情况
f(x)=Σi=0nai(x+A)i=Σi=0naiΣj=0iCijxjAi−j=Σi=0nxiΣj=inajCjiAj−i=Σi=0nxiΣj=inajj!i!(j−i)!Aj−i
f(x)=\Sigma_{i=0}^{n}a_i(x+A)^i=\Sigma_{i=0}^{n}a_i\Sigma_{j=0}^iC_{i}^{j}x^jA^{i-j}=\Sigma_{i=0}^nx^i\Sigma_{j=i}^na_jC_{j}^iA^{j-i}=\Sigma_{i=0}^nx^i\Sigma_{j=i}^na_j\frac{j!}{i!(j-i)!}A^{j-i}
f(x)=Σi=0nai(x+A)i=Σi=0naiΣj=0iCijxjAi−j=Σi=0nxiΣj=inajCjiAj−i=Σi=0nxiΣj=inaji!(j−i)!j!Aj−i
又
f(x)=Σi=0nbixi
f(x)=\Sigma_{i=0}^nb_ix^i
f(x)=Σi=0nbixi
对比一下系数,得到
bi=Σj=inj!i!(j−i)!Aj−iaj
b_i=\Sigma_{j=i}^n\frac{j!}{i!(j-i)!}A^{j-i}a_j
bi=Σj=ini!(j−i)!j!Aj−iaj
移项,得到
i!∗bi∗Ai=Σj=inj!(j−i)!Aj=Σj=0n−i(n−j)!(n−j−i)!Ajaj
i!*b_i*A^i=\Sigma_{j=i}^n\frac{j!}{(j-i)!}A^j=\Sigma_{j=0}^{n-i}\frac{(n-j)!}{(n-j-i)!}A^ja_j
i!∗bi∗Ai=Σj=in(j−i)!j!Aj=Σj=0n−i(n−j−i)!(n−j)!Ajaj
注意到(n−j−i)!(n-j-i)!(n−j−i)!的首项会随着iii的变化而不断变化,不能很好的处理,尝试用n−in-in−i来替换iii,得到
(n−i)!∗bn−i∗An−i=Σj=0i(n−j)!An−j(i−j)!an−j
(n-i)!*b_{n-i}*A^{n-i}=\Sigma_{j=0}^i\frac{(n-j)!A^{n-j}}{(i-j)!}a_{n-j}
(n−i)!∗bn−i∗An−i=Σj=0i(i−j)!(n−j)!An−jan−j
我们依次令
pi=(n−i)!bn−iAn−i
p_i=(n-i)!b_{n-i}A^{n-i}
pi=(n−i)!bn−iAn−i
qj=(n−j)!An−jan−j
q_{j}=(n-j)!A^{n-j}a_{n-j}
qj=(n−j)!An−jan−j
rj=1j!
r_j=\frac{1}{j!}
rj=j!1
那么可以得到卷积形式
pi=Σj=0iqjri−j
p_i=\Sigma_{j=0}^{i}q_{j}r_{i-j}
pi=Σj=0iqjri−j
先把这玩意儿放在一边,我们来看看多项式乘法:
给定nnn次多项式B(x)=bnxn+bn−1xn−1+...+b1x+b0B(x)=b_nx^n+b_{n-1}x^{n-1}+...+b_1x+b_0B(x)=bnxn+bn−1xn−1+...+b1x+b0和mmm次多项式C(x)=cnxn+cn−1xn−1+...+c1x+c0C(x)=c_nx^n+c_{n-1}x^{n-1}+...+c_1x+c0C(x)=cnxn+cn−1xn−1+...+c1x+c0,求它们的乘积n+mn+mn+m次多项式A(x)=B(x)∗C(x)=an+mxn+m+an+m−1xn+m−1+...+a1x+a0A(x)=B(x)*C(x)=a_{n+m}x^{n+m}+a_{n+m-1}x^{n+m-1}+...+a_1x+a_0A(x)=B(x)∗C(x)=an+mxn+m+an+m−1xn+m−1+...+a1x+a0
简单思考一下,发现有
ai=Σj=0ibjci−j
a_i=\Sigma_{j=0}^{i}b_jc_{i-j}
ai=Σj=0ibjci−j
和上面的卷积形式一模一样,所以实际上我们现在要做的就是求一个多项式乘法
考虑一下做法:如果直接展开,显然还是O(n2)O(n^2)O(n2),鉴定为纯纯的超时
这个时候就要引入新的概念:多项式的点值表达形式。具体来说,对于一个nnn次多项式B(x)B(x)B(x),如果给定了其在nnn个不同xxx处的值,那么我们可以唯一确定B(x)B(x)B(x).回想一下线性代数的知识,这个结论还是比较容易得到的
那么用点值表达形式的多项式怎么求多项式乘法A(x)=B(x)∗C(x)A(x)=B(x)*C(x)A(x)=B(x)∗C(x)呢?如果两个多项式的取值的xxx集合相同,那么我们只需要将对应于同一xxx的B(x)与C(x)B(x)与C(x)B(x)与C(x)相乘,然后就能得到A(x)A(x)A(x)在xxx处的值,进一步得到A(x)A(x)A(x)的点值表达式
现在我们可以O(n)O(n)O(n)进行点值表达式的求积了,然而我们还需要考虑如何将B(x)B(x)B(x)与C(x)C(x)C(x)从一般表达转换为点值表达,以及如何把A(x)A(x)A(x)从点值表达转换为多项式表达
如果随便取nnn个不同的xxx进行多项式求值(注意这里其实已经n2n^2n2复杂度了),然后再拉格朗日插值反解系数,时间复杂度依旧是O(n2)O(n^2)O(n2),鉴定为寄
所以我们要通过精心选取xxx来进行求值,来减少多项式求值与插值的复杂度
所谓精心选取,放在FFTFFTFFT里,是选取复数单位根;放在NTTNTTNTT里,是选取原根。这俩都有一些比较神奇的性质
限于篇幅,就题论题,只说NTTNTTNTT
-
原根的定义:设mmm为正整数,aaa是整数,若amod ma \mod mamodm的阶等于ϕ(m)\phi(m)ϕ(m)(欧拉函数),那么称aaa为模mmm的一个原根
-
阶的定义:若对于存在一个最小正整数ddd使得admod m=1a^d \mod m=1admodm=1,那么称ddd是amod ma \mod mamodm的阶
998244353998244353998244353是一个素数(可以记一下,这个数经常出现),它的一个原根为333
基于上述定义,以及现在模数是一个素数的情况(注意这一点很重要,任意模数NTTNTTNTT会非常棘手),原根会有以下有趣的性质:
- 对于任意0<i<p0 < i < p0<i<p ,gimod pg^i\mod pgimodp互不相同(一句话证明:如果中间出现相同的了,说明会有更小的ddd使得admod m=1a^d \mod m=1admodm=1,与原根定义矛盾)
有什么用呢?假如我们现在要求一个n=2ln=2^ln=2l次多项式A(x)A(x)A(x)在nnn个不同点的值:
- 令gn=gp−1ng_n=g^{\frac{p-1}{n}}gn=gnp−1(ps.998244353=223∗119+1998244353=2^{23}*119+1998244353=223∗119+1,只要nnn没有大于2232^{23}223就不会有问题,对于这个题而言,显然够了)
- 将A(x)A(x)A(x)按项数的奇偶性拆分成两部分A(x)=Σi=02laixi=Σi=02l−1a2ix2i+xΣi=02l−1a2i+1x2iA(x)=\Sigma_{i=0}^{2^l}a_ix^i=\Sigma_{i=0}^{2^{l-1}}a_{2i}x^{2i}+x\Sigma_{i=0}^{2^{l-1}}a_{2i+1}x^{2i}A(x)=Σi=02laixi=Σi=02l−1a2ix2i+xΣi=02l−1a2i+1x2i
- 我们将gnkg_n^{k}gnk与gnk+n2g_n^{k+\frac{n}{2}}gnk+2n分别带入A(x)A(x)A(x)求值(其中0≤k<n/20\le k<n/20≤k<n/2),我们会得到
formula1:A(gnk)=Σi=02l−1a2ign2ki+gnkΣi=02l−1a2i+1gn2ki formula1: A(g_n^k)=\Sigma_{i=0}^{2^{l-1}}a_{2i}g_n^{2ki}+g_n^k\Sigma_{i=0}^{2^{l-1}}a_{2i+1}g_n^{2ki} formula1:A(gnk)=Σi=02l−1a2ign2ki+gnkΣi=02l−1a2i+1gn2ki
formula2:A(gnk+n2)=Σi=02l−1a2ign2ki+ni+gnk+n2Σi=02l−1a2i+1gn2ki+ni formula2:A(g_n^{k+\frac{n}{2}})=\Sigma_{i=0}^{2^{l-1}}a_{2i}g_n^{2ki+ni}+g_n^{k+\frac{n}{2}}\Sigma_{i=0}^{2^{l-1}}a_{2i+1}g_n^{2ki+ni} formula2:A(gnk+2n)=Σi=02l−1a2ign2ki+ni+gnk+2nΣi=02l−1a2i+1gn2ki+ni
引理:
- gnn=1g_n^{n}=1gnn=1(一句话证明:gnn=gp−1=gϕ(p)=1g_n^n=g^{p-1}=g^{\phi(p)}=1gnn=gp−1=gϕ(p)=1)(欧拉定理)
- gnk+n=gnkg_n^{k+n}=g_n^{k}gnk+n=gnk(一句话证明:gnk+n=gnk∗gnn=gnkg_n^{k+n}=g_n^k*g_n^n=g_n^kgnk+n=gnk∗gnn=gnk)
- gnn2=−1g_n^{\frac{n}{2}}=-1gn2n=−1(一句话证明:gnn=(gnn2)2g_n^n=(g_n^{\frac{n}{2}})^2gnn=(gn2n)2,所以gnn2=±1g_n^{\frac{n}{2}}=\pm1gn2n=±1,根据之前互不相同的结论gnn2=−1g_n^{\frac{n}{2}}=-1gn2n=−1)
- gnk+n2=−gnkg_n^{k+\frac{n}{2}}=-g_n^{k}gnk+2n=−gnk(一句话证明:gnk+n2=gnk∗gnn2=−gnkg_n^{k+\frac{n}{2}}=g_n^k*g_n^\frac{n}{2}=-g_n^kgnk+2n=gnk∗gn2n=−gnk)
所以我们可以把第二个式子换成
formula3:A(gnk+n2)=Σi=02l−1a2ign2ki−gnkΣi=02l−1a2i+1gn2ki
formula3:A(g_n^{k+\frac{n}{2}})=\Sigma_{i=0}^{2^{l-1}}a_{2i}g_n^{2ki}-g_n^{k}\Sigma_{i=0}^{2^{l-1}}a_{2i+1}g_n^{2ki}
formula3:A(gnk+2n)=Σi=02l−1a2ign2ki−gnkΣi=02l−1a2i+1gn2ki
把formula1formula1formula1和formula3formula3formula3对比一下,我们会发现,唯一的区别,就是中间那个正负号
这说明,我们只需要计算一半的值,剩下的一半可以直接改正负号得到
再来观察一下formula3formula3formula3:
因为gn2ki=gp−1n∗2ki=gp−12l−1∗kig_n^{2ki}=g^{\frac{p-1}{n}*2ki}=g^{\frac{p-1}{2^{l-1}}*ki}gn2ki=gnp−1∗2ki=g2l−1p−1∗ki
我们令n′=2l−1n'=2^{l-1}n′=2l−1,那么我们要计算的前一个部分会变成
Σi=02l−1a2ign2ki=Σi=0n′a2ign′ki=Aeven(gn′ki)
\Sigma_{i=0}^{2^{l-1}}a_{2i}g_n^{2ki}=\Sigma_{i=0}^{n'}a_{2i}g_{n'}^{ki}=A^{even}(g_{n'}^{ki})
Σi=02l−1a2ign2ki=Σi=0n′a2ign′ki=Aeven(gn′ki)
本质是对AAA中偶数项进行gn′kig_{n'}^{ki}gn′ki(其中0≤k<n′0 \le k<n'0≤k<n′)处的表达式求值,我们依旧可以使用gn′kig_{n'}^{ki}gn′ki与gn′ki+n′2g_{n'}^{ki+\frac{n'}{2}}gn′ki+2n′来成对计算,从而降低复杂度;后一个部分同理
也就是说,这个计算量减半的过程是可递归的
简单看一下复杂度:f(n)=2∗f(n2)+O(n)f(n)=2*f(\frac{n}{2})+O(n)f(n)=2∗f(2n)+O(n)根据主定理复杂度为O(nlogn)O(nlogn)O(nlogn)
所以,现在我们做到了以O(nlogn)O(nlogn)O(nlogn)的时间代价从多项式的一般表达式得到点值表达式
再来看一下怎么从点值表达式求得一般表达式
假如我们现在有A(x)A(x)A(x)在n{n}n个gnkig_{n}^{ki}gnki出的值
formula1+formula32\frac{formula1+formula3}{2}2formula1+formula3可以直接得到Aeven(x)A^{even}(x)Aeven(x)在n′n'n′个gn′kig_{n'}^{ki}gn′ki处的值
formula1−formula32∗(gnki)−1\frac{formula1-formula3}{2}*(g_n^{ki})^{-1}2formula1−formula3∗(gnki)−1可以直接得到Aodd(x)A^{odd}(x)Aodd(x)在n′n'n′个gn′kig_{n'}^{ki}gn′ki处的值
依旧是一个递归问题,eveneveneven和oddoddodd不断划分最后就得到某个系数的值了
显然这个过程的时间复杂度也是O(nlogn)O(nlog n)O(nlogn)的
综上所述,我们的流程变成了
B和C求点值表达O(nlogn)−>B和C在每一个点求积得到A的点值表达式O(n)−>把A转换为一般表达式O(nlogn)
B和C求点值表达O(nlogn)->B和C在每一个点求积得到A的点值表达式O(n)->把A转换为一般表达式O(nlog n)
B和C求点值表达O(nlogn)−>B和C在每一个点求积得到A的点值表达式O(n)−>把A转换为一般表达式O(nlogn)
时间复杂度成功降下来了
不过现在是递归进行快速数论变换,常数巨大,有可能卡不过去,所以我们还需要把它改成迭代版NTTNT TNTT
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PYAkOxDz-1653984449189)(C:\Users\uniecho1\AppData\Roaming\Typora\typora-user-images\image-20220529104427727.png)]
观察一下每次按奇偶性分类,最后的结果就是二进制表达翻转排序(其实考虑每一次都是按最后一位是0/10/10/1分类的话挺好证明的,我就不证了)
所以我们可以直接先预处理排序结果,然后自底向上迭代
好了假如现在我们通过NTTNTTNTT把i!biAii!b_iA^ii!biAi求出来了,那么接下来就只需要对i!i!i!和AiA^iAi求逆元再乘回去就行了
AiA^iAi很简单,i!i!i!也就是个线性求逆元的事儿,i−1=−⌊(P/i)⌋(Pmod i)−1i^{-1}=-\lfloor (P/i)\rfloor(P \mod i)^{-1}i−1=−⌊(P/i)⌋(Pmodi)−1,然后累乘就行了
完结撒花,附上完整代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;
const int mod=998244353;
const int G=3;
const int invG=332748118;
int quickPow(int x,int k) { //经典快速幂
if(!k)
return 1;
else if(k&1)
return quickPow(x*x%mod,k>>1)*x%mod;
else
return quickPow(x*x%mod,k>>1);
}
struct convolve {
int lim,len;
int R[MAXN];
void NTT(int* A,int flag) {
//A:实现NTT变换的数组指针
//flag=1:一般表达式->点值表达式
//flag=-1:点值表达式->一般表达式
for(int i=0; i<lim; i++)
if(i<R[i])
swap(A[i],A[R[i]]);//显然R[R[i]]=i,所以交换一下就完事儿了
for(int i=1; i<lim; i<<=1) {
int n=i<<1;//一次性处理的区间的长度
int gn=quickPow(flag>0?G:invG,(mod-1)/n);
//一般表达式转点值表达式的时候获得n次原根
//点值表达式转一般表达式的获得其逆元
for(int j=0; j<lim; j+=n) { //枚举处理哪个区间
int g=1;
for(int k=0; k<i; k++) {
//对于上一层的区间而言,A[j+k]和A[j+i+k]使用的都是g_{n/2}^{k}
//所以放在这里直接取用就行
int x=A[j+k],y=g*A[j+i+k]%mod;
A[j+k]=(x+y)%mod;
A[j+i+k]=(x-y+mod)%mod;
//注意,对于点值表达式转一般表达式的情况,我们没有进行除2的操作,这个会在最后补上
g=g*gn%mod;//获得下一个g
}
}
}
}
void conv(int N,int* A,int* B,int* C) {
lim=1;
while(lim<=2*N)//添加0项使得多项式项数为2^lim,方便后面均分操作
lim<<=1,++len;
for(int i=0; i<lim; i++)
R[i]=(R[i>>1]>>1)|((i&1)<<(len-1));//计算i的二进制翻转得到的值,本质上是一个动态规划
NTT(B,1);
NTT(C,1);
for(int i=0; i<lim; i++)
A[i]=B[i]*C[i]%mod;//直接求积
NTT(A,-1);
int invlim=quickPow(lim,mod-2);
for(int i=0; i<=N; i++)
A[i]=A[i]*invlim%mod;//把NTT里面没有除去的2给除了
}
} solution;
int n,A;
int a[MAXN],p[MAXN],q[MAXN],r[MAXN],fac[MAXN],inv[MAXN],invfac[MAXN];
signed main() {
//freopen("in.txt","r",stdin);
ios::sync_with_stdio(false);
cin>>n;
for(int i=0; i<=n; i++)
cin>>a[i];
cin>>A;
if(!A)
for(int i=0; i<=n; i++)//特判一下A=0的情况,直接输出就行
cout<<a[i]<<" ";
else {
fac[0]=fac[1]=1;
for(int i=2; i<=n; i++)
fac[i]=fac[i-1]*i%mod;//阶乘
inv[0]=inv[1]=1;
for(int i=2; i<=n; i++)
inv[i]=(mod-mod/i)*inv[mod%i]%mod;//线性求逆元
invfac[0]=invfac[1]=1;
for(int i=2; i<=n; i++)
invfac[i]=invfac[i-1]*inv[i]%mod;//阶乘逆元
for(int i=0; i<=n; i++)
q[i]=fac[i]*quickPow(A,i)%mod*a[i]%mod;
reverse(q,q+n+1);//获得q
for(int i=0; i<=n; i++)
r[i]=invfac[i];//获得r
solution.conv(n,p,q,r);
reverse(p,p+n+1);//转成原来的顺序
int invA=quickPow(A,mod-2);
for(int i=0; i<=n; i++)
p[i]=p[i]*invfac[i]%mod*quickPow(invA,i)%mod;//把多余的项给抵消掉
for(int i=0; i<=n; i++)
cout<<p[i]<<" ";//输出答案
return 0;
}
return 0;
}
本文详细探讨了利用快速数论变换(NTT)技巧对多项式展开中的998244353取模问题进行优化,通过巧妙地运用原根和点值表达式,将多项式乘法的时间复杂度从O(n^2)降低到O(n log n),并介绍了关键步骤和代码实现。
1709

被折叠的 条评论
为什么被折叠?



