首页 > 其他分享 >多项式求逆&多项式 ln 保姆级教程

多项式求逆&多项式 ln 保姆级教程

时间:2022-09-18 15:45:37浏览次数:106  
标签:求逆 ln 多项式 poly int pmod op MOD

话说原理八月初就会了,拖到现在才把代码写出来,是不是颓废之王?

前置知识:多项式乘法(FFT/NTT)(参考阅读:可能是废话最多的 FFT 教程

一步一步推式子

我们设 $F(x)G(x)\equiv1\pmod{x^n} $,我们设这里的 \(n\) 是 \(2\) 的整数次幂。

然后设 \(H(x)F(x)\equiv1\pmod{x^{\frac{n}{2}}}\),那么可以确定的是

\[G(x)F(x)\equiv1\pmod{x^{\frac{n}{2}}} \]

应该不难理解,因为在模 \(x^n\) 时就没有非常数项余项(\(1\))了,在模 \(x^{\frac{n}{2}}\) 时也不会有非常数项余项(\(1\))的

于是我们有:

\[H(x)F(x)-G(x)F(x)\equiv F(x)\left(H(x)-G(x)\right)\equiv0\pmod{x^{\frac{n}{2}}} \]

则:

\[H(x)-G(x)\equiv0\pmod{x^{\frac{n}{2}}} \]

平方一下(注意模数也要平方):

\[\left(H(x)-G(x)\right)^2\equiv H^2(x)-2H(x)G(x)+G^2(x)\equiv0\pmod{x^{\frac{n}{2}}} \]

那你一定会纳闷了,这一通操作猛如虎,到底是要得到啥?

问的好,我们先考虑一个问题,当 \(n=2^0=1\) 时,设:

\[F(x)=a_0+a_1x+......+a_nx^n \]

那么满足 \(F(x)G(x)\equiv1\pmod{x^n=x}\) 的 \(G(x)\) 只用是一个常值多项式即可,那么这个常值,其实就是 \(a_0\) 在模 \(998244353\) 意义下的逆元(因为一次项即以上在模 \(x^n=x\) 意义下不能造成实际贡献,只有常数项是会有贡献的)。

那么如果我们在 \(n=2^1=2\) 时,这个常值多项式就能作为 \(H(x)\) 出现,那么如果我们能得到从 \(H(x)\) 到 \(G(x)\) 的递推关系,就能得到 \(n=2\) 时的 \(G(x)\),以此类推,就能求出最后的 \(G(x)\)。

这样的目的,也是指示我们选择平方将模数翻倍的动机所在。

那我们争取从前面的式子中解出 \(G(x)\),但是存在一次方二次方,如果直接开根,那我们平方就没有意义了。

但别忘了在模 \(x^n\) 意义下还有一个关系:

\[G(x)F(x)\equiv1\pmod{x^n} \]

为了出现 \(F(x)\) 还有相乘的格式,我们不妨在式子两边同时乘上 \(F(x)\),即:

\[\begin{aligned}F(x)\left(H^2(x)-2H(x)G(x)+G^2(x)\right)&\equiv F(x)H^2(x)-2F(x)G(x)H(x)+F(x)G^2(x)\\&\equiv F(x)H^2(x)-2H(x)+G(x)\pmod{x^n}\end{aligned} \]

于是我们就能解出 \(G(x)\) 关于 \(F(x)\) 与 \(H(x)\) 的递推关系式:

\[G(x)\equiv 2H(x)-F(x)H^2(x)\pmod{x^n} \]

实现细节

由上面的结论我们可以得到模 \(x^{\frac{n}{2}}\) 意义下逆多项式到模 \(x^n\) 意义下逆多项式的转换,我们可以通过递推或递归方式实现。

而我是很不会递归的,所以就采用了递推的写法。

在实现时,因为数组不能当做函数的返回值,所以我们将多项式利用一个结构体 poly 存储,便于多项式运算的函数直接返回一个多项式。

另外二进制翻转数组 res[] 在多项式乘法结果最高次次数不同时也会变化,不能直接预处理一劳永逸,应该在每一次乘法之前更新一次。

利用结构体存储多项式易爆栈,可以在 [编译选项] 中设置连接器命令为“-Wl,--stack=128000000”以扩大栈空间,防止爆栈。

其他细节参见代码:

#include"iostream"
#include"cstring"
#include"cstdio"
#include"cmath"
using namespace std;

#define MAXN 100005
#define MOD 998244353
#define ll long long 

int N,M,n=1,l=0;
struct poly
{
	int a[MAXN*3];
	poly(){memset(a,0,sizeof(a));}
}f;
int res[MAXN*3],w[MAXN*3];

int mul(int a,int b){return (ll)a*b%MOD;}//大数相乘取模

int quickpow(int a,int b)//快速幂
{
	int ans=1,base=a;
	while(b)
	{
		if(b&1) ans=mul(ans,base);
		base=mul(base,base);
		b>>=1;
	}
	return ans;
}

int inv(int a){return quickpow(a,MOD-2);}//数的逆元

void NTT(int *a,int unit,int n,int l)//没什么特别的 NTT
{
    w[0]=1,w[1]=quickpow(unit,(MOD-1)/n);
    for(int i=2;i<n;i++) w[i]=w[i]=mul(w[1],w[i-1]);
    for(int i=0;i<n;i++) if(i>res[i]) swap(a[i],a[res[i]]);
	for(int i=2;i<=l+1;i++)
    {
        int t=n>>(i-1);
        for(int j=1;j<=t;j++) 
        {
            int s=n/t;
            for(int k=0;k<(s>>1);k++)
            {
                int op=s*(j-1)+k,G=mul(a[op+(s>>1)],w[k*t]);
                a[op+(s>>1)]=(a[op]-G+MOD)%MOD;
                a[op]=(a[op]+G)%MOD; 
            }   
        }
    } 
}

//多项式 F,G 的卷积,n 和 l 是长度和 n 对应的幂次
poly poly_mul(poly F,poly G,int n,int l)
{
	poly h;
	for(int i=0;i<n;i++) res[i]=(res[i>>1]>>1)|((i&1)<<(l-1));
	//每次都要重新算一次 res 数组 
	NTT(F.a,3,n,l),NTT(G.a,3,n,l);
	for(int i=0;i<n;i++) F.a[i]=mul(F.a[i],G.a[i]);
	NTT(F.a,332748118,n,l);
	int m=inv(n);
	for(int i=0;i<n;i++) h.a[i]=mul(F.a[i],m);
	return h;
}

poly poly_times(poly F,int k,int n)//数乘多项式
{
	poly h;
	for(int i=0;i<n;i++) h.a[i]=mul(F.a[i],k);
	return h;
}

poly poly_min(poly F,poly G,int n)//长度为 n 的多项式 F 和 G 的差多项式
{
	poly h;
	for(int i=0;i<n;i++) h.a[i]=(F.a[i]-G.a[i]+MOD)%MOD;
	return h;
}

poly poly_inv(poly F)//多项式求逆核心过程
{
	poly h,g;
	for(int i=0;i<n;i++) h.a[i]=0;
	h.a[0]=inv(F.a[0]);//初始的 h(x) 就是 a_0 的逆元
	for(int i=1;i<l;i++)
	{
		int s=1<<i;//求 F(x) 在模 x^s 次方意义下的逆元
		g=poly_mul(h,h,s,i);
		poly q;
		for(int j=0;j<s;j++) q.a[j]=F.a[j]; 
		//重新定义一个 q(x) 复制 F(x),因为在 NTT 中的交换数组操作会破坏 F(x) 
		g=poly_mul(g,q,s<<1,i+1);
		h=poly_min(poly_times(h,2,s>>1),g,s);
	}
	return h;
}

int main()
{
	scanf("%d",&N);
	while(n<=(N-1)*2) n<<=1,l++;//考虑一下为什么是这样的一个上界
	for(int i=0;i<N;i++) scanf("%d",&f.a[i]);
	poly H=poly_inv(f);
	for(int i=0;i<N;i++) printf("%d ",H.a[i]);
	return puts(""),0;
}

时间复杂度

多项式的时间复杂度主流算法即为主定理,然而我不会,又懒得学。

那我们就通过朴素的方式证明一下,因为多项式次数 \(n-1\) 和不超过 \(2(n-1)\) 的最小 \(2\) 的次幂同阶,于是设 \(n\) 就是一个 \(2\) 的幂次,且大于并最接近多项式次数的两倍,我们不妨讲问题简化为:

\[T(n)=\mathcal O(n\log n)\;\;\;\text{\{即为多项式乘法的复杂度\}} \]

\[T(2^m)+T(2^{m-1})+......+T(1)=? \]

其中 \(m=\log_2n\)。

先不计常数,即求:

\[M=2^m\log 2^m+2^{m-1}\log 2^{m-1}+......+2^1\log 2^1+2^0\log2^0 \]

而:

\[\begin{aligned}M&= \log2\left(m2^m+(m-1)2^{m-1}+......+1\times2^1\right)\\&=\log2\left((m-1)2^{m+1}+2\right)\\&=\log2\left((\log_2n-1)\times2n+2\right)\\&=\mathcal O(n\log n)\end{aligned} \]

这说明多项式求逆的时间复杂度为 \(\mathcal O(n\log n)\)。

多项式求导与积分

这一部分看起来最为吓人,但实现起来其实最为简单,我们设:

\[F(x)=a_0+a_1x+a_2x^2+......+a_nx^n \]

\[P(x)=F'(x)=b_0+b_1x+b_2x^2+......+b_{n-1}x^{n-1} \]

\[Q(x)=\int F(x)=c_0+c_1x+c_2x+......+c_{n+1}x^{n+1} \]

注意次数加一减一关系,以及我们一般默认 \(c_0=0\)。

那么由导数公式 \((x^n)'=nx^{n-1}\) 以及导数积分互为逆运算的关系,我们能得到关系:

\[b_i=(i+1)a_{i+1}\;\;\;c_i=\frac{a_{i-1}}{i} \]

容易知道,实现复杂度都是线性的。

多项式对数函数(多项式 ln)

借助导数与寄分(bushi)的工具,我们可以知道:

\[\ln F(x)\equiv \int \dfrac{F'(x)}{F(x)}\pmod {x^n} \]

借助刚刚学会的多项式求逆就可以轻松解题了,实现上主要还是注意多项式求逆的部分,其余的部分实现难度相对少很多了,实现复杂度容易知道为 \(\mathcal O(n\log n)\)。

code:

#include"iostream"
#include"cstring"
#include"cstdio"
#include"cmath"
using namespace std;

#define MAXN 100005
#define MOD 998244353
#define ll long long 

int N,M,n=1,l=0;
struct poly
{
	int a[MAXN*3];
	poly(){memset(a,0,sizeof(a));}
}f,g;
int res[MAXN*3],w[MAXN*3];

int mul(int a,int b){return (ll)a*b%MOD;}

int quickpow(int a,int b)
{
	int ans=1,base=a;
	while(b)
	{
		if(b&1) ans=mul(ans,base);
		base=mul(base,base);
		b>>=1;
	}
	return ans;
}

int inv(int a){return quickpow(a,MOD-2);}

void NTT(int *a,int unit,int n,int l)
{
    w[0]=1,w[1]=quickpow(unit,(MOD-1)/n);
    for(int i=2;i<n;i++) w[i]=w[i]=mul(w[1],w[i-1]);
    for(int i=0;i<n;i++) if(i>res[i]) swap(a[i],a[res[i]]);
	for(int i=2;i<=l+1;i++)
    {
        int t=n>>(i-1);
        for(int j=1;j<=t;j++) 
        {
            int s=n/t;
            for(int k=0;k<(s>>1);k++)
            {
                int op=s*(j-1)+k,G=mul(a[op+(s>>1)],w[k*t]);
                a[op+(s>>1)]=(a[op]-G+MOD)%MOD;
                a[op]=(a[op]+G)%MOD; 
            }   
        }
    } 
}

poly poly_mul(poly F,poly G,int n,int l)
{
	poly h;
	for(int i=0;i<n;i++) res[i]=(res[i>>1]>>1)|((i&1)<<(l-1));
	NTT(F.a,3,n,l),NTT(G.a,3,n,l);
	for(int i=0;i<n;i++) F.a[i]=mul(F.a[i],G.a[i]);
	NTT(F.a,332748118,n,l);
	int m=inv(n);
	for(int i=0;i<n;i++) h.a[i]=mul(F.a[i],m);
	return h;
}

poly poly_times(poly F,int k,int n)
{
	poly h;
	for(int i=0;i<n;i++) h.a[i]=mul(F.a[i],k);
	return h;
}

poly poly_min(poly F,poly G,int n)
{
	poly h;
	for(int i=0;i<n;i++) h.a[i]=(F.a[i]-G.a[i]+MOD)%MOD;
	return h;
}

poly poly_der(poly F,int n)//求导
{
	poly h;
	for(int i=0;i<n;i++) h.a[i]=mul(i+1,F.a[i+1]);
	return h; 
}

poly poly_cal(poly F,int n)//积分
{
	poly h;
	for(int i=1;i<=n+1;i++) h.a[i]=mul(inv(i),F.a[i-1]);
	return h;
}

poly poly_inv(poly F)
{
	poly h,g;
	for(int i=0;i<n;i++) h.a[i]=0;
	h.a[0]=inv(F.a[0]);
	for(int i=1;i<l;i++)
	{
		int s=1<<i;
		g=poly_mul(h,h,s,i);
		poly q;
		for(int j=0;j<s;j++) q.a[j]=F.a[j]; 
		g=poly_mul(g,q,s<<1,i+1);
		h=poly_min(poly_times(h,2,s>>1),g,s);
	}
	return h;
}

int main()
{
	scanf("%d",&N);
	while(n<=(N-1)*2) n<<=1,l++;
	for(int i=0;i<N;i++) scanf("%d",&f.a[i]);
	g=poly_der(f,N-1);
	poly H=poly_inv(f);
	g=poly_mul(H,g,n,l);
	for(int i=N+1;i<n;i++) g.a[i]=0;
	g=poly_cal(g,N),g.a[0]=0;
	for(int i=0;i<N;i++) printf("%d ",g.a[i]);
	return puts(""),0;
}

呐,就给菜鸡博主一个赞吧,奖励一下自己看完了这么多

标签:求逆,ln,多项式,poly,int,pmod,op,MOD
From: https://www.cnblogs.com/tlx-blog/p/16704881.html

相关文章

  • 多项式全(?)家桶
    贴个板子,以备复习点击查看代码#include<cstdio>#include<cstdlib>#include<algorithm>#include<unordered_map>#include<cmath>#definemod998244353#definemax......
  • go语言中Scan、Scanln、Scanf的区别
    1go语言中的输入操作在go语言中我们可以通过fmt包中的三种方法实现输入操作:fmt.Scan()fmt.Scanln()fmt.Scanf()2fmt.Scan()2.1简单使用Scan()可以输入一个值,也......
  • Linux 安装telnet
    一、安装telnet1、首先我们检测telnet-server的rpm包是否安装[root@localhost~]#rpm-qatelnet-server若无输入内容,则表示没有安装。linux的telnet-server.rpm默认......
  • 多项式全家桶
    前置芝士:FTT&NTT不低于高中的数学推导能力不低于高中的代数芝士高等数学初步复变函数初步(?)多项式乘法目标:给定两个多项式\(F,G\),求\(F\timesG\)。有\(F(x)......
  • centos下docker-compose搭建lnmp环境
     所有操作均在root权限下进行sudo-i 1、新建文件夹【/root/lnmp】和文件【/root/lnmp/docker-compose.yml】mkdir/root/lnmpvi/root/lnmp/docker-compose.y......
  • LNMP构建
    lnmp架构LNMP是指一组通常一起使用来运行动态网站或者服务器的自由软件名称首字母缩写。L指Linux,N指Nginx,M一般指MySQL,也可以指MariaDB,P一般指PHP,也可以指Perl或Python。......
  • 信息学一本通 1311:【例2.5】求逆序对
    时间限制:1000ms      内存限制:65536KB提交数:41023   通过数:9681【题目描述】给定一个序列a1,a2,…,ana1,a2,…,an,如果存在i<ji<j并且ai>ajai......
  • 6-2 多项式求值——15分
    本题要求实现一个函数,计算阶数为n,系数为a[0]…a[n]的多项式(上图)在x点的值。函数接口定义:doublef(intn,doublea[],doublex);其中n是多项式的阶数,a[]中存......
  • lnmp部署
    一,安装nginx1.安装nginx依赖库yuminstalllrzszwgetgitmakecmakegccgcc-c++pcrepcre-developensslopenssl-develncurses-devellibaiobisongitncurses......
  • Docker 映射端口telnet不通
    使用Docker启动端口无法telnethttps://blog.csdn.net/lyd135364/article/details/118369692#echo1>/proc/sys/net/ipv4/ip_forward#sysctl-p......