关闭页面特效
Processing math: 100%

多项式总结

1|0基础算法


1|1多项式乘法


​ 多项式的基础就是多项式乘法,而且能够扩展出来的那些东西都是基于不损失精度的快速数论变换NTT,这是对于下面所有知识点的基础。

​ 令A(x)=n1i=0aixi,B(x)=n1i=0bixi,当我们计算A(x)B(x)的时候,最朴素的方法是O(n2)的,但实际上我们可以做到在O(n log n)的时间复杂度内解决这个问题,在这里就用到了快速数论变换NTT。

​ 这个东西讲解比较复杂这里就不讲了,挂一篇很久之前写的学习笔记,静下心把所有式子一步一步推出来应该不用多久就可以学会了。

1|2分治FFT


​ 问题是给定F(x)=xi=1G(i)F(xi),已知G(x),求F(x)

​ 首先这个东西我们直接做是O(n2),我们考虑计算每个F(x)对哪些状态有贡献,联想一下CDQ分治的过程,分治的时候每次计算左半部分区间对右半部分的影响,我们可以这样算:

fi+j=midi=lrlj=1figj[i+j[mid+1,r]]

​ 那么我们在分治的时候做一遍多项式乘法就可以把对右半部分所有的贡献都求出来了,复杂度O(n log2n)

1|3多项式求逆


​ 多项式有乘法,那有没有乘法逆元呢?答案是肯定的,我们可以通过多项式求逆来解决这样一个问题,对于已知多项式A(x)求解另一个多项式B(x)满足A(x)B(x)=1(mod xn),那么多项式B就称为多项式A的逆元,也记作A1(x)

​ 我们来推导一下多项式逆元的求解过程,首先有:

A(x)B(x)=1(mod xn)

​ 假设我们已经求出了A在模xn2的意义下的逆元B(x),那么有:

A(x)B(x)=1(mod xn2)

​ 两式相减后可得:

A(x)(B(x)B(x))=0(mod xn2)

​ 同时除去A(x)

B(x)B(x)=0(mod xn2)

​ 两边同时平方:

B2(x)+B(x)22B(x)B(x)=0(mod xn)

​ 同时乘上A(x)

B(x)+A(x)B(x)22B(x)=0(mod xn)

​ 那么最后可以得到递推式:

B(x)=2B(x)A(x)B(x)2(mod xn)

​ 推式子的时候可能会遇到一个问题,那就是往下递归的时候可能会让一些奇数项没有计算进去,那么我们刚开始的时候把多项式转换为模x2p(n2p<2n)意义下的就好了。

​ 那么我们可以用这个式子递归往下求解就好了,由主定理可知复杂度是O(n log n)的。

1|41.4 多项式除法


​ 这里的除法指的是带余除法,也就是让你求A(x)=B(x)×C(x)+D(x),给定A(x)(mod xn),B(x)(mod xm),求C(x)(mod xnm+1)D(x)(mod xm1)。我们令AR(x)为多项式A系数翻转后的结果,例如A(x)=1+2x+3x2,AR(x)=3+2x+x2,那么我们把原式的x换成1x,两边同时乘上xn

AR(1x)=BR(1x)CR(1x)+D(1x)×xnm+1

​ 我们知道多项数C的次数是小于nm+1的,而D(x)×xnm+1的最低次数是不会低于nm+1的,我们把这个式子放到模xnm+1意义下,那么AR(1x)=BR(1x)CR(1x),求出C后回代就可以算出D了,复杂度O(n log n)

1|5多项式求导与积分


​ 这个东西应该不用怎么讲,高中数学书已经讲的很清楚了,这里就简单列一下式子。

f(x)dx=ni=1ai1ixif(x)=n2i=0(i+1)ai+1xi

1|6多项式对数函数


​ 问题是给定多项式A(x),求解多项式B(x)满足B(x)=lnA(x)

​ 这个东西推导很简单,直接把两边求导:

B(x)=A(x)A(x)

​ 最后对B(x)进行积分就可以算出B(x)了,用上文提到的多项式求逆和多项式求导与积分可以实现。

1|7多项式牛顿迭代


​ 问题是给定G(x),求解多项式F(x)使得G(F(x))=0(mod xn)

​ 首先先引入一个知识点,一个函数f(x)在某个点x0的泰勒展开有这样一个式子:

f(x)=i=0fi(x0)(xx0)ii!

​ 在这里f(x)n阶导数记作fn(x)

​ 我们类似多项式求逆考虑把这个问题划分成子问题递归求解,假设我们已经知道了G(F0(x))=0(mod xn2),我们在F0(x)处对这个函数进行泰勒展开:

G(F(x))=G(F0(x))+G1(F0(x))(F(x)F0(x))+G2(F0(x))(F(x)F0(x))2+

我们知道F(x)F0(x)的前n2项时相同的,那么F(x)F0(x)对于i<n2,都满足ai=0,那么对于展开后第三项以后的所有式子的次数最低项都不会小于xn,那么在模意义下它们的值都是0,那么实际对我们有意义的只有式子的前两项,即:

G(F(x))=G(F0(x))+G1(F0(x))(F(x)F0(x))(mod xn)

化一下式子,就得到了:

F(x)=F0(x)G(F0(x))G1(F0(x))(mod xn)

那么我们就可以用之前学到的多项式求逆递归下去求解这个式子了,复杂度还是O(n log n)

1|8多项式开方


​ 问题是给定F(x),求G(x)满足G(x)2=F(x)(mod xn)

​ 我们把之前的牛顿迭代的式子带进去就好了,设H(G(x))=G(x)2F(x),那么由牛顿迭代的式子有:

G(x)=G0(x)H(G0(x))H(G0(x))(mod xn)

​ 代入后可得:

G(x)=G0(x)G0(x)2F(x)2G0(x)(mod xn)G(x)=G0(x)2+F(x)2G0(x)(mod xn)

​ 如果常数项不是10的话还要计算二次剩余,就比较麻烦了,剩下的分治计算即可,复杂度O(n log n)

1|9多项式指数函数


​ 既然可以求对数,那么逆运算应该也是可以的吧,这个问题就是求给定G(x),求F(x)满足:

eG(x)=F(x)

​ 先同时对两边取自然对数,移项可得:

lnF(x)G(x)=0

​ 我们令H(F(x))=lnF(x)G(x),套用之前提到的牛顿迭代,可以得到:

F(x)=F0(x)H(F0(x))H(F0(x))

​ 对于H(F(x))=lnF(x)G(x),求导可得H(F(x))=1F(x),那么把这两个带进去:

F(x)=F0(x)lnF0(x)G(x)1F0(x)

​ 整理得:

F(x)=F0(x)(1lnF0(x)+G(x))

​ 我们利用之前的多项式对数函数即可,复杂度O(n log n),常数巨大。

2|0常见应用


2|1第一类斯特林数


​ 第一类斯特林数S(n,m)是表示把n个物品排成m个环的方案数,首先有递推式:

S(n,m)=S(n1,m)+S(n1,m1)×(n1)

​ 这个式子我们可以写出它的生成函数,也就是:

ni=0S(n,i)xi=n1i=0(x+i)

​ 理解起来应该很容易,乘的这个东西分别对应两种转移,那么这个式子我们就可以用分治+NTT做到O(n log2n) 的复杂度,

2|2第二类斯特林数


​ 第二类斯特林数S(n,m)是表示把n个物品分成m个集合的方案数,首先有递推式:

S(n,m)=S(n,m1)+S(n1,m)×m

​ 但是这个式子我们不方便用多项式的运算来优化,我们把这个式子写成容斥的形式:

S(n,m)=1m!mk=0(1)k×(mk)×(mk)n

​ 意义是我们每次枚举空集合的个数,计算至少有k个空集合的方案数,剩下的随便放,又因为集合是无序的,所以最后还要除去m!,我们再把组合数给展开:

S(n,m)=1m!mk=0(1)k×m!k!(mk)!×(mk)nS(n,m)=mk=0(1)kk!×(mk)n(mk)!

​ 用NTT优化多项式乘法解决这个问题就可以做到O(n log n)了。

3|0例题讲解


3|1[BZOJ3456] 城市规划


​ 求n个点的带标号无向连通图个数,n105,mod = 998244353

​ 我们设f(i)i个点的带标号无向连通图个数,g(i)表示i个点的带标号图的个数,那么g(i)=2(2i),我们可以写出f的递推式:

f(i)=g(i)i1j=1(j1i1)f(j)×g(ij)

​ 把式子化开,我们可以得到:

f(i)=g(i)i1j=1(i1)!f(j)g(ij)(j1)!(ij)!

​ 把(i1)!除去:

f(i)(i1)!=g(i)(i1)!i1j=1f(j)g(ij)(j1)!(ij)!

​ 我们发现把左边那个式子刚好是右边那个求和的第i项,合并这两个式子:

ij=1f(j)(j1)!g(ij)(ij)!=g(i)(i1)!

​ 设:

F(x)=nj=1f(j)(j1)!xj,G(x)=n1j=0g(j)j!xj,H(x)=nj=1g(j)(j1)!xj

​ 那么H(x)=G(x)F(x),所以F(x)=H(x)G1(x),多项式求逆即可。

3|2[HDU5322] Hope


​ 对于1到n这n个数的任何一个排列A可以这样计算其价值:对所有下标i找到最小的j满足j>iA[j]>A[i],然后ij之间连边,最后所有连通块个数之积的平方就是该排列的价值,问所有排列的价值和是多少,n105,mod = 998244353

​ 首先发现一个小性质,对于数列中最大的数一定和它前面的所有数在同一个集合,那么我们设dpii个数组成的排列的价值,每次枚举最大的数放在第几位:

dpi=ij=1dpij×Aj1i1×j2

​ 把这个式子拆一下:

dpi=(i1)!ij=1dpijj2(ij)!

​ 就可以用分治FFT求解了,每次只要把前面的dp值除一个阶乘就好了。

3|3[LOJ2541] 「PKUWC2018」猎人杀


​ 有n个人,每个人有一个权值wi,每次随机杀一个人,杀第i个人的概率是wij[j is alive],求第一个人最后一个死的概率,对998244353取模。

​ 第一个人最后一个死就代表恰好有0个人在第一个人之后死,算这个是个很套路的东西用容斥转化为至少,那么设f(S)为至少有S集合的人在第一个人之后死的概率,那么ans=(1)|S|f(S),现在我们转化成了求f(S),实际上这个东西我们可以写出一个式子:

f(S)=w1w1+w[wS]

​ 这个东西理解起来也不是很难,每次只有选择S集合内的人击杀或者1击杀的时候才会影响他们的相对死亡顺序,而选中这里面的人击杀时必须要击杀第一个人才满足S集合都在1之后死,那么我们发现f(S)只与w[wS]有关,又因为题目中的条件有w105,我们可以考虑计算出每个值的容斥系数,可以直接设f[i][j]表示考虑前i个人,权值和为j时的容斥系数,用背包的方法转移就是f[i][j]=f[i1][j]f[i1][jwi],但这样子显然是无法通过的,我们需要优化这个方法。

​ 我们设出这个东西的生成函数,第iaixi代表当前dpi=ai,那么每次转移就是乘上(1xwi),分别代表是否把当前这个人加入集合的决策,那么这个生成函数就是:

ni=2(1xwi)

​ 这个东西直接分治,合并两个区间的信息的时候用NTT计算就好了,这个东西开数组有点麻烦,那么我们可以类似于线段树动态开点回收空间的方法一样,用完以后利用以前的空间,分治出最深的一条链深度应该是logn的,那么我们开logn个数组就好了,复杂度O(nlog2n)

3|4[AGC005F] Many Easy Problems


​ 给你一棵n个点的树和一个整数k,设S是树上某些点的集合,f(S)是最小的包含S的联通子图的大小,n个点选k个点一共有(kn)种方案,你需要对于k[1,n]求出所有k个点的集合的答案,对924844033取模。

​ 我们直接算k的所有答案是不好算的,这个时候考虑算每个点对于每个k的贡献,我们发现一个点产生贡献当且仅当选中的点不全在以它相邻的点为根的子树内,所以一个点u对一个k的贡献为:

(kn)edge(u,v)(ksizev)

​ 观察这个式子发现,一个点的子树大小会在它的父亲统计它时记为sizeu统计一遍,在它统计它的父亲时作为nsizeu统计一遍,那么我们设cntx为子树大小为x被统计到答案内的次数,那么:

ansk=n(kn)ni=1cnti(ki)

​ 前一部分很好算,后一部分把组合数拆开,就是:

ni=1i!×cnti×(ik)!k!

​ 这个东西就可以做卷积了,顺带提一句924844033=221×32×72+1,并且它的原根是5而不是3

3|5[LOJ2058] 「TJOI / HEOI2016」求和


​ 设S(i,j)表示第二类斯特林数,求f(n)=ni=0ij=0S(i,j)×2j×j!

​ 我们首先发现j>i时,S(i,j)=0,那么我们可以把j的枚举范围扩展到n,那么就变成了求:

ni=0nj=0S(i,j)×2j×j!

​ 用第二类斯特林数的容斥公式:

S(n,m)=1m!mk=0(1)k×m!k!(mk)!×(mk)n

​ 那么要求的变成了:

ni=0nj=02jjk=0(1)kk!×(jk)i(jk)!

nj=02jjk=0(1)kk!×ni=0(jk)i(jk)!

ni=0(jk)i用等比数列求和公式可以可以变成1(jk)n+11(jk),那么我们可以设:

f(x)=(1)kk!,g(x)=1xn+1x!(1x)

​ 那么:

ans=nj=02jjk=0f(k)×g(jk)

​ 把fg卷起来之后再求和就可以了。

3|6[CF438E] The Child and Binary Tree


​ 求有多少种不同的二叉树,满足二叉树每个点的权值C,求出所有权值和[1,m]的二叉树种类数,对998244353取模。

​ 设fi表示权值为i的二叉树种类数,那么有如下递推式:

fi=nj=1icjk=0f(k)f(icjk)

​ 我们设出f答案与存在的物品的权值c的生成函数,那么:

f=cff+1

​ 最后那个加1是因为f0=1,但是在答案的卷积中不会计算f0

​ 我们利用一元二次方程求根公式可得:

f=1±14c2c

​ 我们发现取正号的时候这个东西不收敛,所以舍去。

​ 那么f=114c2c,多项式开方+多项式求逆即可。

3|7[LUOGU4389] 付公主的背包


​ 你有n个物品,每个物品有无穷多个,求选择一些物品恰好体积为v的方案数,求出v[1,m]的所有答案,n,m105,mod=998244353

​ 我们还是按生成函数的套路,设aixi这一项的系数表示体积为i的方案数,那么每加入一件物品就相当于给这个生成函数乘上了一个(1+xvi+x2vi+...),也就是11xvi,那么我们最后要求的就是:

ni=11(1xvi)

​ 我们发现直接算不好算,先对整个函数取ln,那么我们来考虑快速计算每一项的ln,也就是如何快速计算:

f(x)=11xv,g(f(x))=ln(f(x))

​ 我们先对这个东西求导,也就是:

g(x)=f(x)×g(f(x))=vxv1(1xv)2×111xv=vxv11xv

​ 把11xv写成无穷级数求和的形式,那么:

g(x)=i=0vxv(i+1)1=i=1vxvi1

​ 把这个积分回去,有:

g(x)=g(x)dx=i=1vvi1+1xv=i=11ixv

​ 我们就可以用O(n ln n),枚举倍数的方法计算出ln(ni=111xvi)的每一项系数了,我们最后再用多项式exp把这个转化回来就可以了,复杂度是O(n log n)

3|8[LUOGU4705] 玩游戏


对于k[1,t],求ni=1mj=1(ai+bj)knm

​ 先不管分母,把分子用二项式定理展开:

ansk=kr=0ni=1mj=1(rk)aribkrj

ansk=k!×kr=0(ni=1arir!)(mjbkrj(kr)!)

​ 这样子就写成了卷积的形式,那么我们只要能快速求出nk=1ak,就可以用NTT计算卷积了。

​ 我们写出这个东西的生成函数:

1+a1x+a2x2+...+ax

xk的系数表示k次幂和,那么我们用等比数列求和公式解出来可以得到:

11ax

​ 令f(x)为这些生成函数的和,那么:

f(x)=ni=111aix

​ 这个东西不好算,我们发现:

ln(1aix)=11aix

​ 我们从对数函数角度考虑,又可以发现:

(ln(1aix))=ai1aix

​ 设g(x)=ni=1ai1aix,那么f(x)=x×g(x)+n

g(x)也很好算,化一下式子就变成了:

g(x)=ni=1(ln(1aix))=(ln(ni=1(1aix)))

​ 这样子g就可以用分治+NTT算,算出g后再推出ffxi的系数就是nj=1aij,那么我们代回原式再做一遍卷积就好了,总的复杂度为O(nlog2n)

4|0总结


​ 多项式的题目其实有很多套路,而且多项式难的地方应该在推式子而不是模板上面,毕竟多项式的模板并没有什么可以改的地方,在OI中一般多项式不会直接拿来出题,可能有对O(n2)DP的优化,也有那种直接计算生成函数系数的,多项式可能比较烦的是打模板,因为这种东西根本不能调的,在后面我会附上包含我这上面所有操作的模板的,然后为了准备这篇学习笔记也花了蛮多时间整理博客,希望看完这篇学习笔记后你能对多项式相关内容有更深的理解:)。

5|0多项式模板


#include <bits/stdc++.h> using namespace std; const int N = 1 << 18 | 1; const int mod = 998244353; inline int mul(int x, int y) { return 1ll * x * y % mod; } inline int add(int x, int y) { return (x += y) < mod ? x : x - mod; } inline int qpow(int _, int __) { int ___ = 1; for (; __; _ = 1ll * _ * _ % mod, __ >>= 1) if (__ & 1) ___ = 1ll * ___ * _ % mod; return ___; } namespace Poly { static int rev[N]; int Get_Rev(int x) { int limit = 1, k = 0; while (limit < x) limit <<= 1, ++ k; for (int i = 0; i < limit; ++ i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (k - 1)); return limit; } void NTT(int *a, int n, int fh) { for (int i = 0; i < n; ++ i) if (i < rev[i]) swap(a[i], a[rev[i]]); for (int Wn, limit = 2; limit <= n; limit <<= 1) { Wn = qpow(fh ^ 1 ? qpow(3, mod - 2) : 3, (mod - 1) / limit); for (int W = 1, j = 0; j < n; j += limit, W = 1) for (int i = j; i < j + (limit >> 1); ++ i, W = mul(W, Wn)) { int a1 = a[i], a2 = mul(W, a[i + (limit >> 1)]); a[i] = add(a1, a2), a[i + (limit >> 1)] = add(a1, mod - a2); } } if (fh ^ 1) for (int inv = qpow(n, mod - 2), i = 0; i < n; ++ i) a[i] = mul(a[i], inv); } void Add(int *a, int *b, int *c, int len, int typ) { for (int i = 0; i < len; ++ i) c[i] = add(a[i], typ ? b[i] : mod - b[i]); } void Mul(int *a, int n, int *b, int m, int *c) { int limit = Get_Rev(n + m), a_[N], b_[N]; for (int i = 0; i < limit; ++ i) { a_[i] = i < n ? a[i] : 0; b_[i] = i < m ? b[i] : 0; } NTT(a_, limit, 1), NTT(b_, limit, 1); for (int i = 0; i < limit; ++ i) c[i] = mul(a_[i], b_[i]); NTT(c, limit, -1); } void Get_Inv(int *a, int *b, int len) { if (len & 1) return (void)(b[0] = qpow(a[0], mod - 2)); Get_Inv(a, b, len >> 1); static int A[N], B[N]; Mul(a, len, b, len, A); Mul(A, len, b, len, B); for (int i = 0; i < len; ++ i) b[i] = add(b[i], add(b[i], mod - B[i])); } void Inv(int *a, int *b, int len) { Get_Inv(a, b, Get_Rev(len)); } void Der(int *a, int *b, int len) { for (int i = 0; i < len - 1; ++ i) b[i] = mul(i + 1, a[i + 1]); b[len - 1] = 0; } void Int(int *a, int *b, int len) { for (int i = len - 1; i; -- i) b[i] = mul(a[i - 1], qpow(i, mod - 2)); b[0] = 0; } void Ln(int *a, int *b, int len) { static int A[N], B[N]; Inv(a, A, len), Der(a, B, len); Mul(A, len, B, len, b), Int(b, b, len); } void Get_Exp(int *a, int *b, int len) { if (len & 1) return (void)(b[0] = 1); Get_Exp(a, b, len >> 1); static int A[N], B[N]; Ln(b, A, len), Add(a, A, B, len, 0); ++ B[0], Mul(b, len, B, len, b); } void Exp(int *a, int *b, int len) { Get_Exp(a, b, Get_Rev(len)); } void Get_Sqrt(int *a, int *b, int len) { if (len & 1) return (void)(b[0] = calcsqrt(a[0])); Get_Sqrt(a, b, len >> 1); static int A[N], B[N]; Add(b, b, A, len, 1), Inv(A, B, len), Mul(b, len, b, len, A); Add(A, a, A, len, 1), Mul(A, len, B, len, b); } void Sqrt(int *a, int *b, int len) { Get_Sqrt(a, b, Get_Rev(len)); } void Div(int *a, int n, int *b, int m, int *c, int *d) { static int A[N], B[N], C[N]; for (int i = 0; i < m; ++ i) A[m - i - 1] = b[i]; Inv(A, B, n - m + 1); for (int i = 0; i < n; ++ i) A[n - i - 1] = a[i]; Mul(A, n, B, n - m + 1, C); for (int i = 0; i < n - m + 1; ++ i) c[i] = C[n - m - i]; Mul(b, m, c, n - m + 1, A); for (int i = 0; i < m; ++ i) d[i] = add(a[i], mod - A[i]); } } int main() { // 首先定义n - 1次多项式是长度为n的多项式。 // Mul(a, n, b, m, c) 表示把长度分别为n, m的两个多项式a, b乘起来的结果,答案存在c数组中。 // Inv(a, b, len) 表示把长度为len的多项式a在模x^len下的结果计算在c数组中。 // Int(a, b, len) 表示把长度为len的多项式a积分后的结果计算在数组b中。 // Der(a, b, len) 表示把长度为len的多项式a求导后的结果计算在数组b中。 // Ln(a, b, len) 表示把长度为len的多项式a取ln后的结果计算在数组b中。 // Sqrt(a, b, len) 表示把长度为len的多项式a开方后的结果计算在数组b中,但是在此处我没有写计算二次剩余的函数,需要自己实现。 // Exp(a, b, len) 表示把长度为len的多项式a求exp后的结果计算在数组b中。 // Div(a, n, b, m, c, d) 表示把长度为n,m的两个多项式a,b做带余除法的结果,商存在c数组,余数存在d数组中。 return 0; }

__EOF__

作  者lunch
出  处https://www.cnblogs.com/brunch/p/10144695.html
关于博主:苟在强校快要退役的高二蒟蒻OIer,From Moon High School。
版权声明:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!

posted @   lùnch  阅读(411)  评论(1编辑  收藏  举报
编辑推荐:
· .NET Core 对象分配(Alloc)底层原理浅谈
· 聊一聊 C#异步 任务延续的三种底层玩法
· 敏捷开发:如何高效开每日站会
· 为什么 .NET8线程池 容易引发线程饥饿
· golang自带的死锁检测并非银弹
阅读排行:
· 一个适用于 .NET 的开源整洁架构项目模板
· AI Editor 真的被惊到了
· API 风格选对了,文档写好了,项目就成功了一半!
· 【开源】C#上位机必备高效数据转换助手
· 关于linux网桥(Linux Bridge)的一些个人记录
1
0
关注
跳至底部
点击右上角即可分享
微信分享提示