题解 D. The Pool "蔚来杯"2022牛客暑期多校训练营7

传送门

出题人的题解实在是无法令人恭维,特此写一份自己的题解


【大意】

\(T\) 次询问,每次询问给定 \(n, m(1\leq n,m\leq 10^{18})\) ,问长宽分别为 \(n, m\) 的矩形顶点摆放在整点后;所有不同摆放方案中,每个方案完全包含的 \(1\times 1\) 格子数量的和是多少?

我们认为两个方案不同,当且仅当一个方案的矩形无法仅通过平移变换,得到另一个方案。


【分析】

我们将矩形 \(ABCD\) 的一个顶点放置在原点上,设 \(AB=n, AD=m\) ,则 \(B\) 点的点坐标需要满足 \(x_B^2+y_B^2=n^2\)

如果我们能找到所有满足条件的点坐标,那它们显然都是答案的候选(可能有的方案能通过别的方案平移得到、有的方案无法使得 \(D\) 在整点)

因此,当前的问题即是:给定 \(l\) ,如何求解所有满足 \(x_B^2+y_B^2=l\) 的点


转化一下问题,由于二维平面上的整点能唯一地对应复平面上,满足 \(\Re z, \Im z\in Z\) 的所有复数 \(z\) 。因此,我们可以直接以高斯整数 \(Z[i]\) 的方式描述待求问题:

求解有多少个高斯整数 \(z=x_B+i\cdot y_B\) ,使得 \(z\cdot \bar z=l\)

我们考虑将 \(l\) 进行质因数分解,显然可以将 \(l\) 分解为 \(\displaystyle l=2^{c_2}\cdot \prod_j p_j^{k_j} \cdot \prod_j q_j^{t_j}\) 的形式,其中 \(c_2\geq 0, k_j, t_j>0, q_j\)\(4n+3\) 型质数,\(p_j\)\(4n+1\) 型质数

根据高斯质数的规律,\(2=(1+i)(1-i), q_j\) 无法继续分解,而 \(p_j\) 能进一步分解为两个共轭的高斯质数 \((a+bi),(a-bi)\) 的乘积

为了对 \(p_j\) 进行高斯质数意义上的质因数分解,我们可以随机一个 \(t\neq 0\pmod {p_j}\) ,再令 \(\displaystyle k=t^{p_j-1\over 4}\bmod p_j\) 。根据二次剩余的理论,\(k^2\equiv \pm 1\pmod {p_j}\) ,取值为 \(-1\) 的概率恰为 \({1\over 2}\)

所以,我们期望两次随机,就能选出一个 \(k\) ,使得 \(k^2\equiv -1\pmod {p_j}\) ,即 \(p_j\mid (k^2+1)\) 。由于在 \(Z[i]\) 上,\(k^2+1=(k+i)(k-i)\) ,故有 \(p_j\mid (k+i)(k-i)\)

\(k, 1<p_j\) ,能推出不存在 \(z\in Z[i]\) 使得 \(p_j\cdot z=k+i\) ,从而 \(p_j\) 并不整除 \(k+i\) ,同理 \(p_j\) 并不整除 \(k-i\) ;所以 \((a+bi), (a-bi)\) 其中一者整除 \(k+i\) ;又因为两者均是高斯质数,不可再分解;故直接求解 \(\gcd(p_j, k+i)\) 即可得到两者之一,再用 \(p_j\) 除即可再得到另一个解

根据上述理论,我们可以将 \(l\) 进一步分解为 \(\displaystyle l=(1+i)^{c_2}\cdot (1-i)^{c_2}\cdot \prod_j (a+bi)^{k_j}(a-bi)^{k_j}\cdot \prod_j q_j^{t_j}\) ,我们需要求解有多少个高斯整数 \(z\) 使得 \(z\cdot \bar z=l\)


简单起见,我们不妨先考虑不含因子 \(2\) 的情况:

对于 \((a+bi)\)\((a-bi)\) 型的高斯质数,我们显然每次需要将其中一个分配给 \(z\) ,另一个分配给 \(\bar z\) ,才能使得两者保持共轭的性质;因此我们可以枚举 \((a+bi)\) 中,有 \(t(0\leq t\leq k_j)\) 个分配给了 \(z\) ,剩下的分配给了 \(\bar z\) ;因此 \(z\) 中的方案为 \((a+bi)^t\cdot (a-bi)^{k_j-t}\) ,方案数为 \(k_j+1\)

对于 \(q_j^{t_j}\) 型高斯质数,当且仅当 \(t_j\) 为偶数时可以平分给 \(z\)\(\bar z\) ,产生唯一的方案;否则分配是不平均的,方案数为 \(0\)

由于式子 \(z\cdot \bar z=m=n^2\) ,故方案数必然不为 \(0\) ;然而对于每个求解出的合法解 \(z\) ,我们乘上单位数 \(1, i, -1, -i\) ,并向 \(\bar z\) 乘上相应单位数的共轭,都能得到一组互不相同的解(相伴解);因此我们即可得出所有的合法方案,方案数为 \(\displaystyle 4\prod_j (k_j+1)\)

对于含 \(2\) 因子时,同样我们可以考虑分配多少个 \((1+i)\)\(z\) ,剩下的给 \(\bar z\) ;然而两者交换一个 \((1+i),(1-i)\) 后,我们发现答案实际上只相差一个 \(i\) ,这个的贡献其实在后续乘单位数中已经进行了消除;为此,我们不妨将所有的 \((1+i)\) 分配给 \(z\) ,因为其不影响方案数

根据题解,方案数为 \(\displaystyle 4\prod_j(k_j+1)\) ,最大值为 \(4\times 295245\) ,在 \(n=495229111954868525\) 时取得


现在,我们已经求解所有满足 \(x_B^2+y_B^2=l\) 的点,需要在这些候选点中,选择无法通过别的方案平移得到、\(D\) 在整点的方案

对于 \(n=m\) 的情况,显然,角 \(\angle BAx\in [0, {\pi\over 2})\) 都对应互不相同的矩形。我们从候选点中,选择那些 \(\Im z\geq 0\wedge \Re z>0\) 的解即可。

而对于 \(n\neq m\) 的情况,角 \(\angle BAx\in[0, \pi)\) 都对应互不相同的矩形;同理可以从候选点中,选择 \(\Im z>0\vee (\Im z=0\wedge \Re z>0)\) 的解。

那如何确认候选点 \((a+bi)\) 是否满足 \(D\) 也位于整点呢?

根据几何关系,我们直接将候选点乘上单位数 \(i\) 即可旋转 \({pi\over 2}\) ,再乘上 \({m\over n}\) 即可放缩至 \(D\) 点;因此只需要验证 \((a+bi)\cdot i\cdot {m\over n}\) 是否为整点即可


终于来到了最终答案求解的部分,已知 \(B\) 位于 \((a'+b'i)\)\(D\) 位于 \((c'+d'i)\) ,如何求解答案包含的 \(1\times 1\) 格子数量的和?

为了方便,我们不妨假设 \(a=|a'|, b=|b'|, c=|c'|, d=|d'|\) ,它们有几何关系如下:

我们按下述方法分别统计每个三角形对答案的贡献

红色三角形的贡献是 \(y={b\over a}x,x\in[0, a]\) 内的格子数,蓝色的是 \(y={d\over c}x,x\in[0, c]\) 内的格子数,很显然,有一部分会被重复计算或遗漏,需要再扣除一个 \((c-a)(d-b)\)

而关于三角形内部的贡献,我们考虑直线的斜率是不少于 \(0\) 的,因此只要格子的左上端点位于直线 \(y={b\over a}x\) 下方(或线上)则能被统计到

故答案变为:除最下行、最右列外,直线下方的整点数

显然是可以由类欧或万欧解决,这里介绍一下 pick 定理的解法:

对于这类所有顶点都在整点上的多边形,称为格点多边形,其面积满足公式:\(S={1\over 2}B+I-1\) ,其中 \(B\) 为边缘点数,\(I\) 为多边形内部点数

该直线面积显然为 \({ab\over 2}\) ,边缘点数可以发现是 \((a+1)+(b+1)+(\gcd(a,b)+1)-3=a+b+\gcd(a,b)\) ,故内部点数为 \(I={ab-a-b-\gcd(a,b)\over 2}+1\)

而可以发现,格子的左上端点仅由内部点、斜线上的非端点构成;因此格子数为 \(I+\gcd(a,b)-1={ab-a-b+\gcd(a,b)\over 2}\)


综上,我们可以先对 \(n\) 进行 Pollard_Rho 算法进行质因数分解;暴力求解所有高斯整数后,选出合法点,并利用上述公式求解答案。


【代码】

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mp make_pair
#define de(x) cout << #x <<" = "<<x<<endl
#define dd(x) cout << #x <<" = "<<x<<" "
mt19937 rnd(time(0));
const int P=998244353;
inline __int128 absl(__int128 x) { return x<0?-x:x; }
inline ll kpow(ll a, ll x, ll p) { ll ans=1; for(;x;x>>=1, a=(__int128)a*a%p) if(x&1) ans=(__int128)ans*a%p; return ans; }
struct Zi {
	__int128 re, im;
	inline Zi (ll re_=0, ll im_=0):re(re_), im(im_) {}
	inline Zi& operator += (const Zi &rhs) { re+=rhs.re; im+=rhs.im; return *this; }
	inline Zi& operator -= (const Zi &rhs) { re-=rhs.re; im-=rhs.im; return *this; }
	inline Zi& operator *= (const Zi &rhs) { tie(re, im)=mp(re*rhs.re-im*rhs.im, re*rhs.im+im*rhs.re); return *this; }
	inline Zi& operator /= (const Zi &rhs) {
		__int128 r=rhs.re, i=rhs.im, l=r*r+i*i;
		tie(r, i)=mp(2*(re*r+im*i)+l, 2*(im*r-re*i)+l); l*=2;
		if(r<0) re=-((-r+l-1)/l);
		else re=r/l;
		if(i<0) im=-((-i+l-1)/l);
		else im=i/l;
		return *this;
	}
	inline Zi& operator %= (const Zi &rhs) { Zi tmp=*this; tmp/=rhs; tmp*=rhs; return *this-=tmp; }
	
	inline Zi operator + (const Zi &rhs) const { Zi lhs=*this; return lhs+=rhs; }
	inline Zi operator - (const Zi &rhs) const { Zi lhs=*this; return lhs-=rhs; }
	inline Zi operator * (const Zi &rhs) const { Zi lhs=*this; return lhs*=rhs; }
	inline Zi operator / (const Zi &rhs) const { Zi lhs=*this; return lhs/=rhs; }
	inline Zi operator % (const Zi &rhs) const { Zi lhs=*this; return lhs%=rhs; } 
};
inline Zi kpow(Zi a, ll x) { Zi ans=1; for(;x;x>>=1, a=a*a) if(x&1) ans=ans*a; return ans; }
inline Zi norm(Zi x) {
	__int128 r=x.re, i=x.im;
	if(r==0) return Zi(absl(i), 0);
	switch((r<0)<<1|(i<0)) {
		case 0: return Zi(r, i); break;
		case 1: return Zi(-i, r); break;
		case 2: return Zi(i, -r); break;
		case 3: return Zi(-r, -i); break;
	}
	return Zi();
}
inline Zi gcd(Zi a, Zi b) {
	while(b.re||b.im)
		swap(a=norm(a%b), b);
	return a;
}
inline Zi GaussPrime(ll p) {
	if(p==2)
		return Zi(1, 1);
	if(p%4!=1)
		return Zi(p, 0);
	ll k=0;
	while(((__int128)k*k+1)%p!=0) k=kpow(rnd(), p>>2, p);
	return gcd(Zi(p, 0), Zi(k, 1));
}

inline bool Fermat(ll n,int p){
	if( kpow(p,n-1,n)!=1 ) return 0;
	ll k=n-1;
	k>>=__builtin_ctzll(k);
	ll t=kpow(p, k, n);
	if(t==1) return 1;
	for(;k<n-1;k<<=1, t=(__int128)t*t%n)
		if(t==n-1) return 1;
	return 0;
}
inline bool Miller_Rabin(ll n){
	static int p[]={2, 3, 5, 7, 11, 13, 17, 19, 23, 61, 233}, siz=sizeof(p)/sizeof(p[0]);
	if(n==1) return 0;
	for(int i=0;i<siz;++i)
		if(n%p[i]==0) return n==p[i];
	else if( !Fermat(n,p[i]) ) return 0;
	return 1;
}
inline ll Rho(ll n,ll c){
	static ll lim=128, tmp[128];
	ll buf=1, tot=0;
	for(ll f1=(c==n-1?0:n+1), f2=((__int128)f1*f1+c)%n; f1!=f2;
		f1=((__int128)f1*f1+c)%n, f2=((__int128)f2*f2+c)%n, f2=((__int128)f2*f2+c)%n){
		tmp[tot++]=f1-f2;
		if(tmp[tot-1]<0) tmp[tot-1]+=n;
		buf=(__int128)buf*tmp[tot-1]%n;
		if(tot==lim){
			if( __gcd(buf,n)>1 ) break;
			tot=0;
		}
	}
	for(int i=0;i<tot;++i){
		buf=__gcd(tmp[i], n);
		if(buf>1) return buf;
	}
	return n;
}
void Pollard_Rho(ll n, vector<pair<ll, int> > &pf, ll cnt){
	static int p[]={2, 3, 5, 7, 11, 13, 17, 19, 23, 61, 233}, siz=sizeof(p)/sizeof(p[0]);
	if(n==1) 
		return ;
	if( Miller_Rabin(n) ) {
		pf.emplace_back(n, cnt);
		return ;
	}
	ll d=n;
	int cntd=0;
	for(int i=0;i<siz;++i) if(n%p[i]==0){
		d=p[i];
		cntd=0;
		while(n%d==0) n/=d, ++cntd;
		pf.emplace_back(d, cnt*cntd);
	}
	while(d==n) d=Rho(n, rand()%(n-1)+1);
	cntd=0;
	while(n%d==0) n/=d, ++cntd;
	Pollard_Rho(d, pf, cnt*cntd);
	Pollard_Rho(n, pf, cnt);
}
inline ll tri2(ll a, ll b) {
	if(a<=1||b==0) return 0;
	return ((a%P)*(b%P)-a-b+__gcd(a, b))%P;
}

vector<pair<ll, int> > pf;
vector<Zi> tmp;
inline void solvePrime(ll n, vector<Zi> &v) {
	pf.clear();
	Pollard_Rho(n, pf, 1);
	
	v.clear();
	v.push_back(Zi(1, 0));
	Zi q, qk, r;
	for(auto &[p, k] : pf) {
		k*=2;
		if(p==2) {
			q=kpow(Zi(1, 1), k);
			for(auto &e : v)
				e*=q;
			continue;
		}
		if(p%4!=1) {
			q=kpow(Zi(p, 0), k/2);
			for(auto &e : v)
				e*=q;
			continue;
		}
		
		qk=kpow(q=GaussPrime(p), k);
		r=Zi(p, 0)/q;
		tmp=v;
		v.clear();
		v.reserve((k+1)*tmp.size());
		for(int i=0; i<=k; ++i) {
			for(const auto &e : tmp)
				v.push_back(e*qk);
			qk=qk/q*r;
		}
	}
	for(auto &e : v)
		e=norm(e);
	sort(v.begin(), v.end(), [](const Zi &a, const Zi &b) {
		if(a.re!=b.re) return a.re<b.re;
		else return a.im<b.im;
		});
	int cnt=1;
	for(int i=1, I=v.size(); i<I; ++i)
		if(v[i].re!=v[i-1].re||v[i].im!=v[i-1].im)
			v[cnt++]=v[i];
	v.erase(v.begin()+cnt, v.end());
}
vector<Zi> a;
inline ll calc(ll a, ll b, ll c, ll d) {
	return (tri2(a, b)+tri2(c, d)-((a-c)%P)*((b-d)%P))%P;
}
inline ll ans() {
	ll n, m;
	cin>>n>>m;
	solvePrime(n, a);
	if(n!=m) {
		int siz=a.size();
		a.resize(siz*2);
		for(int i=0, I=siz; i<I; ++i)
			a[i+siz]=a[i]*Zi(0, 1);
	}
	
	ll res=0;
	Zi c;
	for(auto z : a) {
		c=z*Zi(0, 1);
		if((__int128)c.re*m%n!=0||(__int128)c.im*m%n!=0)
			continue;
		c=Zi((__int128)c.re*m/n, (__int128)c.im*m/n);
		res=(res+calc(absl(z.re), absl(z.im), absl(c.re), absl(c.im)))%P;
	}
	if(res<0) res+=P;
	return res;
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int T; cin>>T;
	while(T--)
		cout<<ans()<<"\n";
	cout.flush();
	return 0;
}
posted @ 2022-08-10 18:23  JustinRochester  阅读(51)  评论(0编辑  收藏  举报