多项式入门
多项式入门
1 复数
-
\(i^2=-1\)
-
复数为 \(z=a+bi\)
-
\(a+bi,a-bi\) 互为共轭复数
-
一个复数对应复平面上的任意一个点,一个复数与复平面上 \((a,b)\) 一一对应。复数与向量之间的运算有相似的地方,假设复数到复平面原点的距离为 \(r\) ,那么复数也可以用 \((r\cos \alpha,r\sin \alpha)\) 来表示。
1.2 运算
-
加法为向量加法:\(z_1+z_2=(a+c)+(b+d)i\)
-
乘法:模长相乘幅角相加。\((ac-bd)+(ad+bc)i\)
也可以表示成 \((r_1r_2\cos(\theta_1+\theta_2),r_1r_2\sin(\theta_1+\theta_2))\)。实质是向量旋转。
1.3 单位根
\(n\) 次单位根为 \(x^n=1\) 的 \(n\) 个根。
\(\omega_n=\cos\frac{2\pi}{n}+\sin\frac{2\pi}{n}i\)
所谓的 \(n\) 个根为 \(\omega_n^1,\omega_n^2...\omega_n^n\)。
注意到:
推广后得到:
为什么单位根是对的?发现把 \(n\) 个单位根全部画到平面直角坐标系上时,这些单位根恰好在单位圆上,且对于一个单位根 \(\omega_n^k\) 来说,\(\omega_n^{k+1}\) 相当于把这个点沿着单位圆逆时针转了 \(\frac{2\pi}{n}\) 不难发现这些单位根的 \(n\) 次方都是 \((\omega_{n}^n)^k\) ,而 \(\omega_n^n=1\) ,所以不难知道这 \(n\) 个根就是 \(x^n=1\) 的 \(n\) 个根。
1.3.1 单位根的性质
-
\(\omega_n^k=\omega_{2n}^{2k}\)
-
证明:\(\omega_n^k=(\cos\frac{2k\pi}{n},\sin\frac{2k\pi}{n})=(\cos\frac{4k\pi}{2n},\sin\frac{4k\pi}{2n})=\omega_{2n}^{2k}\)
-
\(\omega_n^{k+\frac n2}=-\omega_{n}^k\)
-
证明:根据单位根的几何意义,\(n\) 次是转了一圈,\(\frac n2\) 是转了半圈,所以自然是对的。
我们也可以把坐标写出来用三角函数诱导公式来证明,也不难证明。
-
\(\omega_n^0=\omega_n^n=1\)
-
这个刚刚已经说过了。
2 多项式
2.1 点值表示法
-
原始表示法:\(f(x)=\sum\limits_{i=0}^na_ix^i\)
-
原始的多项式乘法:\(\sum\limits_{i=0}^{n_1}\sum\limits_{j=0}^{n_2}a_ib_jx^{i+j}\) 。不难发现这样是 \(n^2\) 的。
-
点值表示法:\(n+1\) 个点确定一个 \(n\) 次多项式,所以我们可以用 \((x_1,f(x_1)),(x_2,f(x_2)),...(x_{n+1},f(x_{n+1}))\) ,来表示一个 \(n\) 次多项式。
-
点值表示法多项式相乘:
不难发现,多项式相乘后的点值为 \((x_1,g(x_1)f(x_1)),(x_2,g(x_2)f(x_2)...x_{n+1}g(x_{n+1})f(x_{n+1}))\)
这样的话是 \(O(n)\) 的。
我们现在的核心位置是原始表示法和点值表示法的相互转化,我们称 \(DFT(f)\) 为原始表示法 \(f\) 化为点值表示法,\(IDFT(f)\) 表示点值表示法 \(f\) 化为原始表示法。 那么我们需要完成的步骤有三个:
- \(DFT(f),DFT(g)\)
- \(DFT(f\times g)=DFT(f)\times DFT(g)\)
- \(IDFT(DFT(f\times g))\)
其中,第二步可以 \(O(n)\)。现在只需要解决第一个步骤和第三个步骤。
2.2 FFT 快速傅里叶变换
2.2.1 DFT
以下我们假设多项式项数 \(n\) 为 \(2^k\) 我们可以强行补到 \(2^k\) 次方的形式。令多项式 \(A(x)=\sum\limits_{i=0}^{n-1}a_ix^i\)
而我们选择的 \(n\) 个点(此时多项式次数为 \(n-1\) )为 \(\omega_n^0,\omega_n^1...\omega_n^{n-1}\)。有什么好处?
我们令:
不难发现,这时有:
我们令 \(x=\omega_n^k\)
发现 \(A(\omega_{n}^k)=A_1(\omega_n^{2k})+\omega_{n}^kA_2(\omega_{n}^{2k})=A_1(\omega_{\frac{n}{2}}^k)+\omega_{n}^kA_2(\omega_{\frac{n}{2}}^k)\)
于是我们可以分治去做这个事情,每次递归一层,多项式的个数会加倍,所以复杂度为 \(n^2\)。
但是我们还有 \(A(\omega_n^{k+\frac n2})=A_1(\omega_{\frac n2}^k)-\omega_n^kA_2(\omega_{\frac n2}^k)\)
所以把下面的式子也应用上的话,每一层多项式的个数不会增加,这样一共递归 \(\log n\) 层,总复杂度为 \(n\log n\)。
2.2.2 代码实现(可能会假)
inline void fft(complex_ *z,int n,int inv){
if(n==1) return;
int h=n>>1;
static complex_ y[maxn];
for(int a=0;a<h;a++){
y[a]=z[a<<1];
y[a+h]=z[a<<1|1];
}
for(int a=0;a<n;a++) z[a]=y[a];
fft(z,h,inv);
fft(z+h,h,inv);
for(int k=0;k<h;k++){
complex_ x=complex_ (cos(2*k*pi/n),sin(2*k*pi/n));
y[k]=z[k]+z[k+h]*x;
y[k+h]=z[k]-z[k+h]*x;
}
for(int a=0;a<n;a++) z[a]=y[a];
}
2.2.3 代码解释
目前这段代码只需要完成 DFT
我们的目的是当 DFT 完成时。\(z\) 数组返回的值为点值表示法,显然当多项式项数为 \(1\) 时,此时他的系数就是它的点值表示法,所以我们什么也不用做。否则,我们首先把 \(z\) 数组的奇次项和偶次项分别处理,存回 \(z\) 数组,然后对每一个部分做 DFT 最后利用我们上面的两个式子,得到 \(z\) 的点值表示法。
2.2.4 IDFT
我们的目标是把点值表示法换回多项式表示法。
我们令 \(B=DFT(A)\) ,即我们让 \(A\) 的点值表示法变成一个多项式,即 \(b_i=A(\omega_n^i)\) ,我们设 \(C(x)=\sum\limits_{i=0}^{n-1} b_ix^i\),然后让 \(x=\omega_n^{-k}\)
我们就有了下面这个式子:
令 \(S(i)=\sum\limits_{i=0}^{n-1}(\omega_{n}^{j-k})^i\),然后我们讨论一下会发现:
- \(j=k\Rightarrow S(i)=n\)
- \(j\not =k\Rightarrow S(i)=0\)
所以有:
我们的目的是求 \(A\) 的原始表示法,所以只需要求 \(\frac{C(\omega_{n}^{-k})}{n}\) 就可以了。
注意到我们实际上干了一件什么事情,我们所做的实际上与 DFT 没有多少区别,我们只不过把 \(A\) 的点值表示法当做原始表示法又做了一遍 DFT 而已,只不过这里我们带入的是 \(\omega_{n}^{-k}\) 。不过把 \(k\) 换成 \(-k\) ,上面的性质依然成立,所以我们同样可以用 DFT 的方法做 IDFT。
通过考虑单位根几何意义这个 \(k\) 与 \(-k\) 的区别我们只用看
2.2.5 总代码(可能会假)
inline void FFT(complex_ *a,int n,int inv){
if(n==1) return;
static complex_ b[N];
int h=n>>1;
for(int i=0;i<h;i++){
b[i]=a[i<<1];
b[i+h]=a[i<<1|1];
}
for(int i=0;i<n;i++) a[i]=b[i];
FFT(a,h,inv);
FFT(a+h,h,inv);
for(int i=0;i<h;i++){
complex_ x=complex_ (cos(2*k*pi)/n,inv*sin(2*k*pi/n));
b[k]=a[k]+a[k+h]*x;
b[k+h]=a[k]-a[k+h]*x;
}
for(int i=0;i<n;i++) a[i]=b[i];
}
int main(){
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<n;i++) f[i]=f[i]/n;
}
2.2.6 迭代卡常与最后实现
可以发现这个代码是过不了模板的,会 T。
所以我们卡一卡常。
首先我们看是否能够避免数组拷贝操作:
首先是优化换位置的这个操作,我们能否一步到位?
注意到换完之后的位置实际上是原来位置的二进制翻转。而二进制翻转我们可以 \(O(n)\) 的去做这个东西。
注意到我们可以在转移上加以优化,这样就避免了所有数组的复制操作。
然后我们还可以迭代去做这个事情。
注意到我们令 \(P(x)=F(x)+G(x)i\),然后我们做 \(P(x)^2\) ,会发现:
所以我们直接把虚部除以 \(2\) 就可以。
代码:
inline void fft(cp *f,bool flag){
for(int i=0;i<n;i++) if(i<tr[i]) swap(f[i],f[tr[i]]);
for(int p=2;p<=n;p<<=1){
int len=p>>1;
cp tg(cos(2*pi/p),sin(2*pi/p));
if(!flag) tg.y*=-1;
for(int k=0;k<n;k+=p){
cp buf(1,0);
for(int l=k;l<k+len;l++){
cp tt=buf*f[len+l];
f[len+l]=f[l]-tt;
f[l]=f[l]+tt;
buf=buf*tg;
}
}
}
}
int main(){
n=read();m=read();
for(int i=0;i<=n;i++) scanf("%lf",&f[i].x);
for(int i=0;i<=m;i++) scanf("%lf",&f[i].y);
for(m+=n,n=1;n<=m;n<<=1);
for(int i=0;i<n;i++) tr[i]=(tr[i>>1]>>1)|((i&1)?n>>1:0);
fft(f,1);
for(int i=0;i<n;i++) f[i]=f[i]*f[i];
fft(f,0);
for(int i=0;i<=m;i++) printf("%d ",(int)(f[i].y/n/2+0.49));
return 0;
}
2.3 NTT 快速数论变换
FFT 一个最大的问题在于精度问题,因为参与运算的都是实数。
NTT 适用于在模意义下做多项式乘法。
在 FFT 中我们用到了的性质:
- \(\omega_n^n=1\)
- \(\omega_{n}^1,\omega_n^2...\omega_n^n\) 互不相等
- \(\omega_n^k=\omega_{2n}^{2k},\omega_{n}^{k+\frac{n}{2}}=-\omega_n^k\)
- \(\sum\limits_{i=0}^{n-1}(\omega_n^k)^i=n\times [k=0]\)
假设我们在 \(\bmod p\) 意义下,其中 \(p\) 为一个质数。设 \(g\) 为 \(p\) 的原根。
设 \(p-1=q\times 2^r=q\times n\) 我们令 \(n\) 为多项式项数。
设 \(\omega_n=g^q\),我们来证明这个东西仍然满足上面的性质。
-
\(\omega_{n}^n=g^{qn}=g^{p-1}\equiv 1(\bmod p)\)
-
假设有两个相等,则有 \(g^{qi}\equiv g^{qj}(\bmod p)\Rightarrow g^{qi-qj}\equiv 1(\bmod p)\),显然有 \(i-j\not = n\)。所以如果成立,与原根定义不符合,所以成立。
-
前半段:\(g^{qk}=g^{\frac{q}{2}\times 2k}\) 。所以前半段成立。
后半段:只需要证明:\(\omega_n^{\frac n2}=-1\) 。而 \(\omega_n^n=1,(\omega_n^{\frac{n}{2}})^2=1,\omega_n^{\frac{n}{2}}\not = 1\) 。所以成立。
-
这个性质依赖于前面的性质,显然成立。
所以我们找到了一个单位根的替代品。主要,在实践中,我们通常取模数为 \(998244353=2^{23}\times 7\times 17+1\)。这样,只要这个多项式长度不超过 \(2^{23}\) ,都可以用 NTT 而不是 FFT。
所以我们可以直接在 FFT 的代码上直接改。
同时,一样有卡常之后的非递归写法:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 1350000
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=998244353;
const int gg=3;
const int invg=332748118;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,m,tr[N<<1],f[N<<1],g[N<<1];
inline int ksm(int a,int b,int mod){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
inline void NTT(int *f,bool op){
for(int i=0;i<n;i++) if(i<tr[i]) swap(f[i],f[tr[i]]);
for(int p=2;p<=n;p<<=1){
int len=p>>1;
int tg=ksm((op==1?gg:invg),(mod-1)/p,mod);
for(int k=0;k<n;k+=p){
int buf=1;
for(int l=k;l<k+len;l++){
int now=buf*f[l+len]%mod;
f[l+len]=f[l]-now;
if(f[l+len]<0) f[l+len]+=mod;
f[l]=f[l]+now;
if(f[l]>mod) f[l]%=mod;
buf=buf*tg%mod;
}
}
}
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);n++;m++;
for(int i=0;i<n;i++) read(f[i]);
for(int i=0;i<m;i++) read(g[i]);
for(m+=n,n=1;n<m;n<<=1);
for(int i=0;i<n;i++) tr[i]=(tr[i>>1]>>1)|((i&1)?n>>1:0);
NTT(f,1);NTT(g,1);for(int i=0;i<n;i++) f[i]=1ll*f[i]*g[i]%mod;
NTT(f,0);int invn=ksm(n,mod-2,mod);
for(int i=0;i<=m-2;i++) printf("%lld ",1ll*f[i]*invn%mod);
return 0;
}
FFT NTT 小结
在做 DFT 的时候 我们位置 \(i\) 上的值是 \(F(g^{qi})\),是一个值,然后我们用这个值去做 IDFT 得到最终的多项式。其中 DFT 和 IDFT 我们可以分治来做。这就是多项式乘法优化的本质。
2.4 FWT 快速沃尔什变换
我们希望快速的解决下面这个问题:
同样,我们认为 \(n\) 是 \(2\) 的正整数次幂。
上面的这个式子我们下面统一写成 \(C=A\oplus B\)
和 FFT 以及 NTT 一样,我们希望能够找到一个 FWT ,使得 \(FWT(A\oplus B)=FWT(A)\times FWT(B)\) 其中右边的这个乘法就是各个位置相乘的形式。
我们下面给出或卷积和和卷积,并推导异或卷积,而实际上,我们可以自己定义位运算并推导它的卷积。
2.4.1 或卷积
2.4.1.1 或卷积 FWT
我们设 \(A_0\) 为 \(A\) 前半段,\(A_1\) 为 \(A\) 后半段。
定义 \(A=(Q,W)\) 为数组 \(A\) 是由 \(Q,W\) 组成的,其中前半段为 \(Q\) ,后半段为 \(W\)。
根据上面的那个卷积式,我们显然有 \(A|B=(A_0|B_0,A_0|B_1+A_1|B_0+A_1|B_1)\)
看不懂上面那个式子的读者可以这样想:因为 \(n\) 是一个 \(2\) 的整数次幂,所以能够为 \(A| B\) 前半段做贡献的只能是 \(A_0,B_0\) ,且 \(A_0| B_0\) 就是前半段,这是因为它们下标二进制的最高位为 \(0\) 。
或卷积的 FWT 是这样的:
显然,这样的 FWT 能够满足 \(FWT(A+B)=FWT(A)+FWT(B)\)
我们来证明这个式子满足 \(FWT(A)\times FWT(B)=FWT(A| B)\)
显然有:
有:
我们用数学归纳法,假设 \(FWT(A| B)=FWT(A)\times FWT(B)\) 在 \(\le \frac n2\) 时成立,其中,当 \(n=1\) 时,左式显然成立。
注意到:
所以这个 FWT 可行。
2.4.1.2 或卷积 IFWT
但是光有 FWT 不行。我们仍然需要 IFWT。考虑一下我们做 FWT 时,我们只需要把过程逆一下,就可以复原整个序列,由于递归,我们的复原时从底到上的,显然可以复原整个序列。具体来说是这样:
我们只需要让 \(IFWT(A)=(IFWT(A_0),IFWT(A_1)-IFWT(A_0))\)
显然,这样的 \(IFWT(A+B)=IFWT(A)+IFWT(B)\)
所以有:
于是归纳可以证明 IFWT 可行。
代码我们与和卷积一起给出。
2.4.2 和卷积
在和卷积中:
证明方法和或卷积类似,这里不再赘述。
2.4.3 和,或卷积代码
- 代码解释:
op=1
为或卷积,op=2
为和卷积。inv=0
为 FWT,inv=1
为IFWT
inline void fwt(int *z,int n,int op,int inv){
if(n==1) return;
int h=n>>1;
fwt(z,h,op,inv);fwt(z+h,h,op,inv);
static int y[N];
for(int i=0;i<h;i++){
if(op==1){
if(inv==0){
y[i]=z[i];
y[i+h]=z[i]+z[i+h];
}
else{
y[i]=z[i];
y[i+h]=z[i+h]-z[i];
}
}
else if(op==2){
if(inv==0){
y[i]=z[i]+z[i+h];
y[i+h]=z[i+h];
}
else{
y[i]=z[i]-z[i+h];
y[i+h]=z[i+h];
}
}
}
for(int i=0;i<n;i++) z[i]=y[i];
}
在主函数中我们可以参照 FFT 的写法。
2.4.4 异或卷积
在这里说明如何自己手退 FWT 和 IFWT。
-
下文中所涉及到的 FWT 和 IFWT 都是满足线性的,即:
\(FWT(A+B)=FWT(A)+FWT(B),IFWT(A+B)=IFWT(A)+IFWT(B)\)
首先根据异或的计算法则,我们有:
根据前面或卷积和和卷积的经验,我们设:
我们的目标是:
我们直接代入可以得到:
根据上面这些式子,我们对应项系数相等,可以得到一个方程:
很明显,这个方程有很多解,比如说 \(a=b=c=d=0\) ,又比如说 \(a=b=c=d=1\) ,但是,这些解都不合法,为什么?
其实,任何一个满足这个方程的解我们都可以拿来做 FWT ,但是并不是所有的解都能用来做 IFWT!
我们看一下做 IFWT 需要什么条件。注意到 IFWT 其实就是 FWT 的逆变换,所以我们只需要把 FWT 的计算反过来就可以了,FWT 是赋值,那么反过来就是解方程。我们需要知道给我们赋值的数是什么。这相当于我们要解这样的一个方程:
其中假设 \(a,b,c,d\) 我们是知道的。不理解这个方程的可以把或卷积那里的值代过来试一下。这种写法应该不是很合法,但可以这样想,其实每一次回溯,数组所有位置的值都被新覆盖掉了,我们 IFWT 做的就是找到新覆盖之前的那个值是什么。
我们可以解出来:
不难发现,要求有解必须满足 \(ad-bc\not =0\)
所以我们把上面这个方程完善一下:
在这样的条件下,有两组解:
这两组解都满足条件,我们就可以用来做 FWT。
至于 IFWT,我们已经知道如何复原上一次的数组了,也可以做,公式就是:
于是异或卷积也可以做了。
于是自定义的位运算也可以做了。
这里附上或并还有异或卷积的代码,用的是非递归实现,不得不说,这个要比 NTT 好写的多。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=998244353;
const int inv2=499122177;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline void FWT1(int *f,int n,int op){
for(int mid=1;mid<=(n>>1);mid<<=1)
for(int l=0;l<n;l+=(mid<<1))
for(int i=l;i<=l+mid-1;i++){
if(op==0){(f[i+mid]+=f[i])%=mod;}
else{(f[i+mid]-=f[i])%=mod;}
}
}
inline void FWT2(int *f,int n,int op){
for(int mid=1;mid<=(n>>1);mid<<=1)
for(int l=0;l<n;l+=(mid<<1))
for(int i=l;i<=l+mid-1;i++){
if(op==0){(f[i]+=f[i+mid])%=mod;}
else (f[i]-=f[i+mid])%=mod;
}
}
inline void FWT3(int *f,int n,int op){
for(int mid=1;mid<=(n>>1);mid<<=1)
for(int l=0;l<n;l+=(mid<<1))
for(int i=l;i<=l+mid-1;i++){
int a=f[i],b=f[i+mid];
if(op==0){f[i]=(a+b)%mod;f[i+mid]=(a-b)%mod;}
else{f[i]=1ll*((a+b)%mod)%mod*inv2%mod;f[i+mid]=1ll*((a-b)%mod)*inv2%mod;}
}
}
int a[N],n,b[N],c[N];
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);n=1<<n;
for(int i=0;i<n;i++) read(a[i]);
for(int i=0;i<n;i++) read(b[i]);
FWT1(a,n,0);
FWT1(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
FWT1(a,n,1);FWT1(b,n,1);FWT1(c,n,1);
for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
FWT2(a,n,0);FWT2(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
FWT2(a,n,1);FWT2(b,n,1);FWT2(c,n,1);
for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
FWT3(a,n,0);FWT3(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
FWT3(a,n,1);FWT3(b,n,1);FWT3(c,n,1);
for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
return 0;
}
关于异或卷积 IFWT,其实运算的逆就是IFWT,证明这个只需要想或卷积一样归纳就可以。
2.5 多项式算法变形
2.5.1 多项式求逆
我们考虑给定 \(A(x)\) 要求一个 \(B(x)\),使得 \(A(x)B(x)\equiv 1 (\bmod x^n)\)
如果 \(n=1\) 显然这个问题就是一个求逆元的事情。这启发我们尝试把答案逐渐缩小。
这里假设我们已经找到了一个 \(C(x)\) 使得 \(A(x)C(x)\equiv 1\bmod x^{\lceil\frac x2\rceil}\)
我们尝试用 \(A(x),C(x)\) 去表示 \(B(x)\) 。
不难发现,有:
所以我们就可以用多项式乘法来做了。
时间复杂度上限为 \(O(n\log^2 n)\)。
2.5.2 多项式开根
给定 \(A(x)\) 找到一个 \(B(x)\) 使得 \(B^2(x)\equiv A(x)\bmod x^n\)
我们同样考虑缩小这个问题,假设我们已经求到了一个 \(C(x),s.t.C^2(x)\equiv A(x)\bmod x^{\lceil\frac x2\rceil}\)
那么我们有:
运用多项式求逆就可以做了。
2.5.3 多项式除法
给定 \(A(x),B(x)\) 要求找到 \(C(x),D(x)\) 使得 \(A(x)=C(x)B(x)+D(x)\)。其中需要满足 \(D(x)\) 的次数小于 \(B(x)\) 的次数。
我们设 \(A\) 的次数为 \(n\) ,\(B\) 的次数为 \(m\) ,那么 \(C\) 的次数为 \(n-m\) ,可以认为 \(D\) 的次数为 \(m-1\)。
我们设 \(inv(A)=x^n\times A(\frac 1x)\) ,那么显然有 \(A=inv(inv(A))\) 。通俗来讲,\(inv\) 就是把系数倒着写一遍。
那么有:
我们可以利用多项式求逆来求解一个 \(C\) ,然后代入原式求 \(D\) 即可。
2.5.4 多项式求对数
给定 \(f(x)\) 要求找到 \(g(x),s.t.g(x)\equiv \ln f(x)\bmod x^n\)
我们首先关注这里的对数的定义是什么,我们对两边求导可以得到:
很明显等式右边是可以算的,那么我们算出后用不定积分算一下就可以得到 \(g\)。
一般来说,题目保证常数项等于 \(1\)。
2.5.5 多项式 exp
给定 \(f(x)\) 要求求解 \(g(x),s.t.g(x)\equiv e^{f(x)}\bmod x^n\)
首先引入 \(f(x)\) 的泰勒级数:
我们还是首先要考虑这里 exp 的定义是什么,我们对两边求对可以得到:
我们令 \(h(g(x))=\ln g(x)-f(x)\) ,注意这里函数 \(h\) 的自变量是多项式 \(g(x)\) 而不是 \(x\) ,也就是说,这并不是一个复合函数,这里的 \(f(x)\) 相当于一个常量。而我们期望得到一个 \(h(g(x))=0\) 的一个解。
我们对 \(h\) 多次求导可以得到:
我们还是和以前一样,设 \(\ln g_0(x)\equiv f(x)\bmod x^{\lceil\frac n2\rceil}\),假设这个 \(g_0(x)\) 是已知的。我们现在要求 \(g_1(x),s.t.\ln g_1(x)\equiv f(x)\bmod x^n\)。我们写出 \(h(g_1(x))\) 当 \(x_0=g_0(x)\) 的泰勒级数。有:
两边都对 \(x^n\) 取模可以得到:
注意到实际上有:
所以有:
而 \(h(g_1(x))\equiv 0\) :
所以 \(g_1(x)\) 就可以算了。
2.5.6 多项式快速幂
求 \(f^k(x)\bmod x^n\)。首先可以直接用快速幂。
还有一种方法:
所以可以用多项式取对数加多项式 exp 解决。
3 例题
3.1 例题 1
给定一个字符串 \(s\) 和一个字符串 \(t\) ,其中 \(t\) 中可能有通配符:也就是可以是任何字符的字符。
问 \(s\) 和 \(t\) 有多少个位置可以匹配。
我们先不考虑通配符,如何用多项式乘法来做这个事情。
一下我们省略所有的 \(x\)。
考虑到如果我们把 \(s\) 和 \(t\) 看做两个多项式的话,我们实际上是要算这个东西:
我们令 \(a_i=\sum\limits_{j=0}^m(s_{i+j}-t_j)^2\)
然后有:
不难发现 \(s_{i+j}^2,t_j^2\) 都可以预处理出来,所以我们不用考虑它们,我们只需要考虑 \(2s_{i+j}t_j\) 怎么做。
观察多项式乘法的形式,我们可以考虑让 \(t_j\) 翻转,即 \(t'_j=t_{m-j}\) 。
然后我们令:
其实这个是满足多项式乘法的形式的,我们来证明一下。
最后这个式子就是标准的多项式乘积的形式。
至于通配符,我们只需要让通配符的 \(t_i=0\) ,然后令:
就可以做了。
3.2 例题2
有 \(n\) 个线段 \(a_i\) 从中任意选出 \(3\) 条,问能够组成三角形的概率是多少。
\(n,a_i\le 10^5\)
这个问题本质上是一个计数问题。
我们首先令 \(c_i\) 表示长度为 \(i\) 的线段有多少条。
然后令 \(C(x)=\sum\limits_{i=0}^nc_ix^i\) 。我们令 \(D=C\times C\) 。那么 \(D_i\) 表示的就是能够选两条线段组成长度为 \(i\) 的方案数是多少。我们考虑用所有的方案减去不合法的方案,所有的方案就是 \(\dbinom{n}{3}\)。
而不合法的方案呢,我们考虑枚举最长的那一条边 \(l\) ,那么 \(\sum\limits_{i=1}^lD_i\) 就是不合法的方案。
这个题就做完了。