【模板】多项式开根(加强版)

VI.【模板】多项式开根(加强版)

这题和上题唯一的区别就是a0的取值——本题a0不一定为1

咋办呢?

我们观察到里面有一句话:

保证a0mod 998244353下的二次剩余。

二次剩余?这是啥?能吃吗?

这时,你突然想起曾经看到过一道模板题:

【模板】二次剩余

于是你兴奋地点了进去,发现这正是我们需要的:求x2n(modp)的根。


于是我们先讲解一下二次剩余的Cipolla算法。该算法仅应用于p是奇质数的情形。

  1. 在上述方程中,x有多少解?

我们先从简单的情形想起——假设它有两解x1,x2,则一定有

x12x220(modp)

于是

(x1x2)(x1+x2)0(modp)

显然x1x20,不然就是重根了。

则必有x1+x20,即x1x2是在模p意义下的相反数

显然一个数只有唯一的相反数,故实际上二次剩余方程如果有解,则一定有两解。

(实际上n0是一个特例——它只有一根x=0,于是可以特判掉,因此我们接下来讨论的n都是非0


  1. 怎样判断对于一个n,其二次剩余方程是否有解?

如果一个n关于p的二次剩余方程有解,则我们np的二次剩余。同理,如果无解,我们称其为p非二次剩余

我们从费马小定理开始:

nφ(p)1(modp)

因为我们Cipolla算法应用的前提是p是奇质数,所以φ(p)=p1,所以

np11(modp)

显然p1是偶数,于是我们两边开根,得到

np12±1(modp)

我们考虑当n是二次剩余时,必有

np12(x2)p12xp11(modp)

因此np121(modp)n是二次剩余的必要条件。

我们考虑再证其充分性。设ngk(modp),其中gp的原根。则必有(gk)p121(modp)

而又有gφ(p)1(modp),故

(gk)p12gp1(modp)

所以

p1|k×p12

k必须是一个偶数来消掉分母上的2。于是我们只要令xgk2即是二次剩余方程的一个根,故n是二次剩余。

因此np121(modp)n是二次剩余的充分条件。

综上,np121(modp),等价于np的二次剩余。


  1. 如何求根(Cipolla算法)

我们考虑随机出来一个a使得a2n不是二次剩余。这期望需要两次随机,因为每组相反数都对应着一组二次剩余,一共p12组相反数,故一共有p12个二次剩余,则剩下的数都不是二次剩余,故期望两次即可随机得到一个非二次剩余。

我们现在设一个i2a2n(modp)

等等,a2n不是非二次剩余吗?那咋出来一个i2呢?

类比虚数概念,这个i实际上也是一个虚数,它没有实际对应的数。

我们要证明几条引理:

  • ipi(modp)

ipi×(i2)p12(a2n)p12(modp)

因为a2n是非二次剩余,所以(a2n)p121

所以

ipi(modp)

  • (A+B)pAp+Bp(modp)

我们二项式展开后,除了(p0)(pp)两项不存在p以外,其它项全都存在因子p,因此其0(modp),可以忽略,因此只保留Ap,Bp两项。

于是

(a+i)p+1(ap+ip)×(a+i)(ai)(a+i)a2i2n2(modp)

所以我们直接计算复数a+ip+1次幂即可得到一个解,取相反数即可得到令一个解。

注意这里i2的取值应是a2n,而不是正常复数中的1

时间复杂度期望为O(logp)

代码(【模板】二次剩余):

#include<bits/stdc++.h>
using namespace std;
int T,n,p,sqri;
struct cp{
	int x,y;
	cp(int a=0,int b=0){x=a,y=b;}
	friend cp operator *(const cp &a,const cp &b){
		return cp((1ll*a.x*b.x%p+1ll*a.y*b.y%p*sqri%p)%p,(1ll*a.y*b.x%p+1ll*a.x*b.y%p)%p);
	} 
};
int ksm(int x,int y){
	int z=1;
	for(;y;y>>=1,x=1ll*x*x%p)if(y&1)z=1ll*z*x%p;
	return z;
}
cp ksm(cp x,int y){
	cp z=cp(1,0);
	for(;y;y>>=1,x=x*x)if(y&1)z=z*x;
	return z;
}
bool che(int x){
	return ksm(x,p>>1)==1;
}
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d%d",&n,&p),n%=p;
		if(!n){puts("0");continue;}
		if(!che(n)){puts("Hola!");continue;}
		int a=rand()%p;
		while(true){
			sqri=(1ll*a*a%p-n+p)%p;
			if(!a||che(sqri))a=rand()%p;
			else break;
		}
		cp tmp=cp(a,1);
		tmp=ksm(tmp,(p+1)>>1);
		int u=tmp.x,v=(p-tmp.x)%p;
		if(u>v)swap(u,v);
		printf("%d %d\n",u,v);
	}
	return 0;
}

然后本题就只需要把迭代的初始值改成用Cipolla求出的二次剩余方程的根即可。

时间复杂度O(nlogn),代码就不贴了。

posted @   Troverld  阅读(74)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示