首页 > 其他分享 >【瞎口胡】多项式操作

【瞎口胡】多项式操作

时间:2022-09-01 17:22:50浏览次数:92  
标签:int 多项式 瞎口 poly len inline 操作 dea

前置

快速傅里叶变换 FFT

多项式的基石操作。

快速沃尔什变换 FWT

位运算卷积。鸽了。

快速数论变换 NTT

把 FFT 搬到了模意义下,终于可以做计数问题啦。

多项式牛顿迭代

简单粗暴的推导方式。

基本操作

封装

为了学习多项式的时候更加顺手,封装板子是很有必要的,而且也方便贺。试想 CF 最后十分钟的时候切了个多项式题,然后发现把求逆和 exp 的板子分别复制过来合不到一起,那感觉心态直接爆了。

这个封装的原版是 zhouyuyang 神仙的,可以去大佬的博客找到。

我们考虑把多项式封装到 namespace 里面,这样一定程度上避免了和程序变量重名,如果没有重名的风险也可以直接 using namespace。

namespace PolyNomial{
# define ull unsigned long long
# define ll long long
# define mo MOD
# define poly std::vector <int>
	const int FFTN=1<<18,MOD=998244353; // 做 NTT 的最大长度,可以调整成更大的或者更小的,要保证是 2 的幂次
	int w[FFTN+10],W[FFTN+10],rev[FFTN+10]; // 小 w 和大 W 存单位根,rev 存蝴蝶变换的位置
	inline void qadd(int &x,int v){ // 经典加法
		x+=v,((x>=mo)&&(x-=mo));
		return;
	}
	inline int qpow(int d,int p){ // 经典快速幂
		int ans=1;
		for(;p;p>>=1,d=1ll*d*d%mo) (p&1)&&(ans=1ll*ans*d%mo);
		return ans;
	}
    inline void initW(void){ // 大 W 存的是 FFTN 次单位根
		W[0]=1,W[1]=qpow(3,(mo-1)/FFTN);
		for(int i=2;i<=FFTN;++i) W[i]=1ll*W[i-1]*W[1]%mo;
		return;
	}
	inline int initRev(int len){ // 蝴蝶变换,len 是多项式**次数**(项数+1)
		int n=1;
		while(n<=len) n<<=1;
		for(int i=0;i<n;++i) rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0);
		return n;
	}

DFT(NTT) 和 IDTT(INTT)

然后就是涉及到多项式的部分。我们希望多项式操作后的结果是一个 vector,这样操作起来方便一点,不用一会 new 个数组出来一会又 delete 回去,看着太蠢了。

但是 NTT 的时候如果直接传 vector 进去会有长度不够之类的问题,于是我们用声明好的数组 A,B 来做 DFT 和 IDFT,这样就没啥越界的问题了。

接下来考虑优化 NTT。我们注意到,取模巨大多慢,于是我们把点值数组 va[] 变成 unsigned long long 来减少取模次数,每 \(16\) 层取一次模。考虑到内层乘法只有点值乘单位根,后者是不超过 \(998244352\) 的,并且每一层下来 va[] 最多加上 \(998244352\)。考虑 \(998244352\times 998244352 \times 16\),取 \(\log_2\) 约为 \(63.789635526\),\(64\) 位的 ull 刚好能存下。

	int A[FFTN+10],B[FFTN+10],C[FFTN+10]; // 最多的时候需要三个临时数组,一般只用 A,B
	ull va[FFTN+10]; // NTT 的临时数组
	
	inline void NTT(int *F,int len){
		for(int i=0;i<len;++i) va[rev[i]]=F[i];
		
		for(int l=1;l<len;l<<=1){ // merge 2 * l -> 1 * 2l
			int siz=FFTN/(l<<1);
			for(int i=0,j=0;i<l;++i,j+=siz) w[i]=W[j]; // omega_{n}^{i} = W_j
			for(int s=0;s<len;s+=(l<<1))
				for(int j=0;j<l;++j){
					int v=va[s+j+l]*w[j]%mo;
					va[s+j+l]=va[s+j]+mo-v,va[s+j]+=v;
				}
			if(l==(1<<15)) for(int i=0;i<len;++i) va[i]%=mo;
		}
		for(int i=0;i<len;++i) F[i]=va[i]%mo;
		return;
	}
	inline void INTT(int *F,int len){
		for(int i=0;i<len;++i) va[rev[i]]=F[i];
		
		for(int l=1;l<len;l<<=1){ // 2 * l -> 1 * 2l
			int siz=FFTN/(l<<1);
			for(int i=0,j=FFTN;i<l;++i,j-=siz) w[i]=W[j]; // 要反着取单位根了
			for(int s=0;s<len;s+=(l<<1))
				for(int j=0;j<l;++j){
					int v=va[s+j+l]*w[j]%mo;
					va[s+j+l]=va[s+j]+mo-v,va[s+j]+=v;
				}
			if(l==(1<<15)) for(int i=0;i<len;++i) va[i]%=mo;
		}
		int invn=qpow(len,MOD-2);
		for(int i=0;i<len;++i) F[i]=va[i]*invn%mo;
		return;
	}	

调整多项式

多项式在操作之后次数可能会谜之减少,我们不希望多项式最高次项是 \(0\) 对吧?于是需要实现调整多项式的功能。

	inline poly Sweep(poly a){ // 返回多项式
		for(;a.size()>1&&!a.back();) a.pop_back();
		return a;
	}
	inline void Sweeps(poly &a){ // 直接在传进来的参数上调整
		for(;a.size()>1&&!a.back();) a.pop_back();
		return;
	}
	// ps: 我们不希望空多项式存在(两个相同的多项式相减按照定义也会剩个 0),于是零次项的 0 我们不管

多项式运算

多项式加减法

计算两个多项式相加和相减的结果。

	inline poly Plus(const poly &a,const poly &b){ // 常引用,省去了 copy 一份 a 和 b 的时间
        // 理论上不写 const 也行,感觉写了清楚一点,一眼就能看出“这只是个参数”
		int dea=a.size()-1,deb=b.size()-1;
		assert(dea>=0&&deb>=0); // 说不定从哪里传了个空多项式进来,要警惕 不过不写这个也没事
		poly ans(std::max(dea,deb)+1);
		for(int i=0;i<=dea;++i) ans[i]=a[i];
		for(int i=0;i<=deb;++i) qadd(ans[i],b[i]);
		Sweeps(ans);
		return ans;
	}
	inline poly Minus(const poly &a,const poly &b){
		int dea=a.size()-1,deb=b.size()-1;
		assert(dea>=0&&deb>=0);
		poly ans(std::max(dea,deb)+1);
		for(int i=0;i<=dea;++i) ans[i]=a[i];
		for(int i=0;i<=deb;++i) qadd(ans[i],mo-b[i]);
		Sweeps(ans);
		return ans;
	}

多项式乘法

计算两个多项式的积。

分别 NTT,然后乘起来 INTT 回去即可。

	inline poly Mul(const poly &a,int k){ // 数乘
		int dea=a.size();
		poly ans(dea+1);
		for(int i=0;i<=dea;++i) ans[i]=1ll*a[i]*k%mo;
		return ans;
	}
	inline poly Mul(const poly &a,const poly &b){
		int dea=a.size()-1,deb=b.size()-1;
		assert(dea>=0&&deb>=0);
		poly ans(dea+deb+1);
		int blen=initRev(dea+deb); // 确定 ntt 的长度
		for(int i=0;i<blen;++i) A[i]=((i<=dea)?a[i]:0),B[i]=((i<=deb)?b[i]:0); // 记得清零
		NTT(A,blen),NTT(B,blen);
		for(int i=0;i<blen;++i) A[i]=1ll*A[i]*B[i]%mo;
		INTT(A,blen);
		for(int i=0;i<=dea+deb;++i) ans[i]=A[i];
		return ans;
	}

多项式乘法逆

给定多项式 \(F(x)\),求 \(F(x)\) 在模 \(x^n\) 意义下的乘法逆 \(G(x)\),即:\(F(x)G(x) \equiv 1 \pmod{x^n}\)。

牛顿迭代得

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

递归,维护 \(G_0\) 和 \(F\) 做乘法即可。

	inline void Getinv(int *F,int *G,int len){ // given F, calc F^{-1} = G
	// G = inv(F) mod xn ; G_0 = inv(F) modxn/2 ; G = G_0(2-G_0*F)
		if(len==1){ // 边界,此时
			G[0]=qpow(F[0],MOD-2);
			return;
		}
		int mlen=(len+1)>>1;
		Getinv(F,G,mlen);
		int blen=initRev(2*len);
		for(int i=0;i<blen;++i) A[i]=((i<len)?F[i]:0),B[i]=((i<mlen)?G[i]:0);
		NTT(A,blen); // A 对应 F, B 对应 G0
		NTT(B,blen);

		for(int i=0;i<blen;++i) A[i]=1ll*B[i]*(2+mo-1ll*B[i]*A[i]%mo)%mo;
		INTT(A,blen);
		for(int i=0;i<len;++i) G[i]=A[i];
		return;
	}
	poly Getinv(const poly &a,int len){ // 外层调用只用这个函数
		int *f=new int[len],*g=new int[len]; // 需要 2*len 的额外空间
		for(int i=0;i<len;++i) f[i]=((i<(int)a.size())?a[i]:0),g[i]=0;
		Getinv(f,g,len);
		poly ans(len);
		for(int i=0;i<len;++i) ans[i]=g[i];
		delete[] f;
		delete[] g;
		return ans;
	}

多项式除法

给定一个 \(n\) 次多项式 \(F(x)\) 和一个 \(m\) 次多项式 \(G(x)\) ,请求出多项式 \(Q(x)\),\(R(x)\),满足以下条件:

  • \(Q(x)\) 次数为 \(n-m\),\(R(x)\) 次数小于 \(m\)
  • \(F(x) = Q(x)G(x) + R(x)\)

需要一点魔法。首先,\(n\) 次多项式 \(F(x)\) 把系数翻转形成的多项式是 \(x^nF(\dfrac{1}{x})\),记作 \(F'(x)\)。可以这样想,原来次数越高的项,要乘的 \(\dfrac{1}{x}\) 就越多,在结果中的次数就越低。

实际上次数不足 \(n\) 次的多项式可以在高次项补上 \(0\),然后这样翻转,效果也是一样的。

为了方便起见,我们下文中直接认为 \(R(x)\) 次数是 \(m-1\)。

运用魔法

\[\newcommand\df{\dfrac{1}{x}} F(x) = Q(x)G(x) + R(x) \\ F(\df) = Q(\df)G(\df) + R(\df) \\ x^nF(\df)=x^{n-m}Q(\df)x^{m}G(x)+x^{n-m+1}x^{m-1}R(\df) \\ F'(x) = Q'(x)G'(x) + x^{n-m+1}R'(x) \\ F'(x) \equiv Q'(x)G'(x) \pmod {x^{n-m+1}} \\ Q'(x) \equiv F'(x) \operatorname{inv}({G'}(x)) \pmod {x^{n-m+1}} \\ \]

于是先对 \(G'(x)\) 求逆,然后多项式乘法即可算出 \(Q'(x)\),从而算出 \(Q(x)\)。

考虑 \(R(x)\),它等于 \(F(x) - Q(x)R(x)\),算出 \(Q(x)\) 后容易求得。

	inline poly Rev(poly a){
		std::reverse(a.begin(),a.end());
		return a;
	}
	inline void Revs(poly &a){ // 注意这里只对多项式进行了翻转,没有去掉高位的 0,因为这个不影响操作
		std::reverse(a.begin(),a.end());
		return;
	}
	inline poly Divide(poly a,poly b){ // 计算 A / B
		int dea=a.size()-1,deb=b.size()-1;
		if(dea<deb) return poly(1); // 返回只有一个 0 的多项式
		Revs(a),Revs(b),a.resize(dea-deb+1);
		b=Mul(Getinv(b,dea-deb+1),a);
		b.resize(dea-deb+1),Revs(b); // 次数是 dea-deb,于是 vector 里有 dea-deb+1 项
		return b;
	}
	inline poly Modulo(poly a,poly b){ // 计算 A mod B
		poly res=Minus(a,Mul(Divide(a,b),b));
		res.resize(b.size()-1);
		return res;
	}

多项式开根

给定 \(F(x)\),求 \(G(x)\) 使得 \(G^2(x) \equiv F(x) \pmod{x^n}\)。

牛顿迭代得

\[G(x) \equiv \dfrac 12\left(\dfrac{F(x)}{G_0(x)}+G_0(x) \right)\pmod{x^n} \]

每层求逆即可。如果想常数小一点可以同时维护 \(G_0\) 和 \(G_0\) 的逆。另外,如果 \([x^0]F(x) \neq 1\),要加上求二次剩余的板子。

	inline void Getsqrt(int *F,int *G,int len){
		if(len==1) return G[0]=1,void(); // 如果 F[0]!=1, 要用上二次剩余
		int mlen=(len+1)>>1;
		Getsqrt(F,G,mlen);
		int *invc=new int[len];
		for(int i=0;i<len;++i) invc[i]=0;
		Getinv(G,invc,len); // 存 G0 的逆
		int blen=initRev(2*len); // 要在模 x^{2n} 意义下做,因为 F(x) 和 1/G0(x) 都是 n-1 次多项式,相乘是 2n-2 次
		for(int i=0;i<blen;++i) A[i]=((i<len)?F[i]:0),B[i]=((i<len)?invc[i]:0);
		NTT(A,blen),NTT(B,blen);
		for(int i=0;i<blen;++i) A[i]=1ll*A[i]*B[i]%mo;
		INTT(A,blen); // 计算 F(x)/G0(x)
		for(int i=0;i<len;++i) G[i]=1ll*(G[i]+A[i])*(mo+1)/2ll%mo;
		delete[] invc;
		return;
	}
	
	inline poly Getsqrt(const poly &a,int len){
		int *f=new int[len],*g=new int[len];
		for(int i=0;i<len;++i) f[i]=((i<(int)a.size())?a[i]:0),g[i]=0;
		Getsqrt(f,g,len);
		poly ans(len);
		for(int i=0;i<len;++i) ans[i]=g[i];
		delete[] f;
		delete[] g;
		return ans;
	}

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

给定 \(F(x)\),求 \(G(x)\) 使得 \(G(x) \equiv \ln F(x) \pmod{x^n}\)。

我们现在还不知道 \(\ln F(x)\) 到底是什么。所幸我们知道 \((\ln x)'=\dfrac{1}{x}\),根据复合函数的求导法则,\((\ln F(x))' = \dfrac{1}{F(x)}\times F'(x) = \dfrac{F'(x)}{F(x)}\)。然后对它积分就可以算出 \(\ln F(x)+c\) 了,其中 \(c\) 是一个常数,因为求导再积分会丢失常数信息。

那 \(G(x)\) 的常数项是多少呢?我们知道 \(G(x)\) 是个多项式,带入 \(x=0\),得 \(G(0)=\ln F(0) = \ln ([x^0]F(x))\)。

参考证明,容易发现 \(F(x)\) 常数项不为 \(1\) 时 \(\ln F(0)\) 是超越数,这也很符合直觉。模意义下超越数没有意义,于是我们有以下结论:当且仅当 \(F(x)\) 常数项等于 \(1\),\(F\) 在模 \(x^n\) 意义下有对数多项式

如果 \([x^0]F(x)=1\),我们发现积分常数等于 \(0\),这和求导再积分回去的常数项相同,于是我们不用刻意计算积分常数。

	inline poly Int(const poly &a){ // 积分:integral(x^n) = 1/(n+1) x^{n+1}
		int dea=a.size()-1;
		poly ans(dea+2);
		ans[0]=0;
		for(int i=0;i<=dea;++i) ans[i+1]=1ll*qpow(i+1,mo-2)*a[i]%mo;
		return ans;
	}
	inline poly Der(const poly &a){ // 求导: (x^n)' = n * x^{n-1}
		int dea=a.size()-1;
		poly ans(dea);
		if(!dea) ans.push_back(0);
		for(int i=1;i<=dea;++i) ans[i-1]=1ll*i*a[i]%mo;
		return ans;
	}
	inline poly Getln(poly a,int len){ // 求 ln
		assert(a[0]==1);
		if((int)a.size()>len) a.resize(len);
		poly res=Int(Mul(Der(a),Getinv(a,len)));
		res.resize(len);
		return res;
	}
	inline void Getln(int *F,int *G,int len){ // 求 F 的 ln, 存到 G 中
        // 待会算 exp 的时候有用
		poly a(len);
		for(int i=0;i<len;++i) a[i]=F[i];
		a=Getln(a,len);
		for(int i=0;i<len;++i) G[i]=a[i];
		return;
	}

多项式指数函数(多项式 exp)

给定 \(F(x)\),求 \(G(x)\) 使得 \(G(x) \equiv \exp F(x) \pmod{x^n}\)。

牛顿迭代得

\[G(x) \equiv G_0(x)(1-\ln G_0(x) + F(x))\pmod {x^n} \]

和上面相似,求 exp 的时候要保证 \([x^0]F(x) = 0\)。

	inline void Getexp(int *F,int *G,int len){
		if(len==1) return G[0]=1,void();
		int mlen=(len+1)>>1;
		Getexp(F,G,mlen);
		int *lnc=new int[len];
		for(int i=0;i<len;++i) lnc[i]=0;
		Getln(G,lnc,len);
		int blen=initRev(2*len); // 同理,两个模 x^n 意义下的多项式相乘可能不止 n 次
		for(int i=0;i<blen;++i) A[i]=((i<len)?F[i]:0),B[i]=((i<mlen)?G[i]:0),C[i]=((i<len)?lnc[i]:0); // A: F(x) B: G0(x) C: ln(G0(x))
		NTT(A,blen),NTT(B,blen),NTT(C,blen);
		for(int i=0;i<blen;++i) A[i]=1ll*B[i]*(1ll+mo-C[i]+A[i])%mo;
		INTT(A,blen);
		for(int i=0;i<len;++i) G[i]=A[i];
		delete[] lnc;
		return;
	}
	inline poly Getexp(const poly &a,int len){
		int *f=new int[len],*g=new int[len];
		for(int i=0;i<len;++i) f[i]=((i<(int)a.size())?a[i]:0),g[i]=0;
		Getexp(f,g,len);
		poly ans(len);
		for(int i=0;i<len;++i) ans[i]=g[i];
		delete[] f;
		delete[] g;
		return ans;
	}

多项式快速幂

给定多项式 \(F(x)\) 和 \(k\),求 \(G(x) \equiv F^k(x) \pmod{x^n}\)。保证 \([x^0]F(x) = 1\)。

可以类比矩阵快速幂的做法,但复杂度是 \(O(n \log^2 n)\) 的。

但这里保证 \([x^0]F(x)=1\),而且是模 \(x^n\) 意义下,于是可以对 \(F\) 直接求 \(\ln\),然后多项式乘上 \(k\),再 \(\exp\) 回去。

	inline poly __Getpower(poly a,int len,int k){
		if((int)a.size()>len) a.resize(len);
		a=Getln(a,len),a=Mul(a,k),a=Getexp(a,len);
		return a;
	}

设 \(F(x) = \sum \limits_{i=0}^{n-1} a_ix^i\)。对于 \(a_0\) 不为 \(1\) 的情况怎么办?如果 \(a_0\) 不是 \(0\),那直接给每项除个 \(a_0\),求完快速幂再给每项乘上 \(a_0^k\)。

如果 \(a_0\) 是 \(0\) 那咋办?我们可以提公因式,例如,\(F(x)=2x^3+6x^4+4x^5\),那么就可以变成 \(F(x)=2x^3(1+3x+2x^2)\)。对后面括号里的部分快速幂,完了再乘上 \((2x^3)^k\)。当然不可能傻乎乎地去把结果和 \(2^kx^{3k}\) 做多项式乘法,直接把结果多项式往右移 \(3k\) 位,再给每个系数乘上 \(2^k\) 即可。

对于一般的情况类比即可。

	inline poly Getpower(poly a,int len,int k,int pk){ // pk: k 对 phi(p) 取模的结果,方便算 a_0^k ps: 模板题的 k 大得离谱
		if((int)a.size()>len) a.resize(len);
		int t=0,b,dea=a.size()-1;
		while(!a[t]&&t<=dea) ++t;
		assert(t<=dea),b=a[t]; // 如果多项式全 0 那混进来了怪东西
		poly res(len);
		if(1ll*t*k>len) return res; // t*k >= len 那模 x^len 就只剩下 0 了
		for(int i=0;i<=dea;++i) a[i]=((i+t<=dea)?(1ll*a[i+t]*qpow(b,mo-2)%MOD):0); // 提公因式
		a=__Getpower(a,len,k); // 快速幂
		int dk=qpow(b,pk),rlen=t*k; // dk: 提出来的系数
		for(int i=0;i<=dea;++i) a[i]=1ll*a[i]*dk%mo,((i+rlen<=dea)&&(res[i+rlen]=a[i])); // 往右移多项式并乘上系数
		return res;
	}

另外,做模板题的时候有一个小细节:如果 \(k\) 非常大并且能提出来至少一个 \(x\),那这个快速幂的结果一定是全 \(0\) 了。但 \(k\) 取完模可能变得很小,于是在读入的时候就要判断,如果在任意时刻 \(k \geq 10^7\) 且 \(a_0>0\),那么答案就一定是全 \(0\)。

标签:int,多项式,瞎口,poly,len,inline,操作,dea
From: https://www.cnblogs.com/liuzongxin/p/16647219.html

相关文章

  • 助教工作总结(计算机操作系统)
    一、助教工作的具体职责和任务批改同学们每一次的课堂作业并且登记成绩,及时对同学们作业中出现的主要问题进行总结并反馈给任课老师,协助老师更好地推进课程进度,帮助同学们......
  • C++ delete进行了什么操作
    #include<iostream>classA{public:voidt(){std::cout<<"helloworld!"<<std::endl;}~A(){std::co......
  • Mysql基本操作
    mysql数据库管理软件底层还是文件操作不用IO流使用sql语言数据库database表table列column数据datacmd控制台里操作-uroot-pshowdatabases;展示所有数据库;created......
  • 【瞎口胡】单位根反演
    单位根反演是用单位根来表示整除关系的东西。定义式\[\left[k\midn\right]=\dfrac{1}{k}\sum\limits_{i=0}^{k-1}\omega_k^{in}\]如果\(k\midn\),那么\(\omeg......
  • Appium - 模拟手机滑动操控的操作
    在模拟“滑动操控”的时候,使用的方法就是swipe(),该方法的参数说明如下:start_x:起始横坐标start_y:起始纵坐标end_x:结束时横坐标end_y:结束时纵坐标duration:滑动持续......
  • 日常开发记录-elementUI 文件上传假删除,防止删除文件后后悔的操作,无需调用后端删除文
    此篇博客关键是记录这种假删除的思想,后端给的删除接口也不一定非要用。。。上传文件假删除:<template><div><el-uploadclass="upload-demo"ac......
  • 每个程序员都需要知道的操作系统基础知识
    每个程序员都需要知道的操作系统基础知识实际上什么是操作系统?操作系统或操作系统可以理解为您可以在设备上获得的最低控制层。操作系统管理您计算机的内存、进程、软件......
  • js操作技巧
    //返回多个数据可以用数据的形式functiondivision(dividend,divisor){varquotient=dividend/divisor;vararr=[dividend,divisor,quotient]ret......
  • 操作系统
    1.CPU缓存CPU缓存分为3级结构:寄存器->L1缓存(数据缓存+指令缓存)->L2缓存->L3共享缓存缓存的最小单位:缓存行(64kb),这意味着对于内存连续的数据结构,一......
  • 今日内容 Django连接MySQL操作及ORM基本操作
    静态文件及相关配置1.先编写一个登录功能(1)创建django项目并创建一个app(2)在url.py添加一组对应关系(3)在app的views.py中编写登录核心逻辑......