试图讲解万能欧几里得算法

神秘算法,抽象至极。

学习自 ix35大犇的题解,但我死活没看懂,大概是我太菜了吧,写一篇题解理清思路以及加以补充。

记号有部分变动。

问题

万能欧几里得算法解决的是形如,给出直线 \(y=\frac{kx+b}{m}\),计算直线下方的整坐标点的贡献,贡献函数应于坐标有关,且满足结合律,即可合并。

比如模板题 【模板】类欧几里德算法 这样的:

给定 \(n,\,a,\,b,\,c\) ,分别求 \(\sum\limits_{i=0}^{n}\left\lfloor \frac{ai+b}{c} \right\rfloor\,,\ \sum\limits_{i=0}^{n}{\left\lfloor \frac{ai+b}{c} \right\rfloor}^2\,,\ \sum\limits_{i=0}^{n}i\left\lfloor \frac{ai+b}{c} \right\rfloor\) ,答案对 \(998244353\) 取模。多组数据。

万能欧几里得算法将计算复杂度降到了 \(\log\) 级别。

算法内容

从左往右扫这条直线,定义与平行于 \(x\) 轴且 \(y\) 坐标为整数的直线相交为 \(U\),与平行于 \(y\) 轴且 \(x\) 坐标为整数的直线相交为 \(R\),特别的,如果同时满足记为 \(UR\)。这里的记号表示对答案的贡献,分成两种是因为通常来说 \(U\) 和 \(R\)​​ 对答案的贡献往往不同。

定义函数 \(f(k,b,m,n,U,R)\) 为当前每一恒行的贡献为 \(U\),纵列贡献为 \(R\),直线 \(y=\frac{kx+b}{m}\) 在定义域 \(x\in (0,n]\) 产生的 \(UR\) 序列。

那么直线将得到一个确定的 \(UR\) 序列。不难发现直线跨过整坐标存在周期性规律。

若 \(k\geq m\)

\[y=\frac{kx+b}{m}=\Big\lfloor\frac{k}{m}\Big\rfloor+\frac{(k\bmod m)x+b}{m} \]

对应的几何意义是:不难发现每个 \(R\) 之前必然会有 \(\Big\lfloor\frac{k}{m}\Big\rfloor\) 个 \(U\),我们不妨把这些整行打包起来放到 \(R\) 里,创建一个新的 \(R'\)。

\[f(k,b,m,n,U,R)=f(k\bmod m,b,m,n,U,R’) \]

其中 \(R'=U^{\Big\lfloor\frac{k}{m}\Big\rfloor}R\)。

若 \(k<m\)

转一下坐标系,翻转 \(x\)、\(y\) 的地位,反解得到 \(x=\frac{my-b}{k}\)。

但这样有一个问题,我们一开始定义的,如果同时与横线与竖线相交记为 \(UR\),在这里会刚好反过来。所以我们向下偏移一个很小的量,完美避开这个问题。

\[x=\frac{my-b-1}{k} \]

仍然有很多细节问题。

定义的时候,我们的定义域是 \((0,n]\),而由于转之前的 \(y\) 轴截距未必是整数,转过来之后没法完美贴合到 \(x\) 轴上,所以我们要把这一段裁掉,也就是转之前第一个 \(U\) 及其以下的部分。

旋转坐标系后平移直线,带入 \(y=1\) 得到有 \(\lfloor\frac{m-b-1}{k}\rfloor\) 个旋转前的 \(R\),再拼上一个 \(U\)​(因为左开右闭)。

新的 \(n'=\lfloor\frac{kn+b}{m}\rfloor\) 应为旋转前 \(U\) 的个数。

但这样又让坐标偏移了一位,提出来这一位偏移加入常数项:

\[x=\frac{m(y+1)-b-1}{k}=\frac{my+m-b-1}{k} \]

根据定义应该以 \(R\) 结尾。但转过去之后 \(R\) 变成 \(U\) 就不符合定义了,所以末尾这一部分也要截掉,这一部分有 \(n-\lfloor\frac{mn'-b-1}{k}\rfloor\) 个。

\[f(k,b,m,n,U,R)=R^{\lfloor\frac{m-b-1}{k}\rfloor}U+f(m,(m-b-1)\bmod k,k,n'-1,R,U)+R^{n-\lfloor\frac{mn'-b-1}{k}\rfloor} \]

\(n'\) 减一是刚才截掉的。

若我们发现 \(n'=0\),那说明当前全是 \(R\),直接返回 \(R^{n}\)​ 即可。

还有问题:初始直线是有截距的,算法处理的必须过原点,所以我们需要先把截距的 \(\lfloor\frac{b}{m}\rfloor\) 个 \(U\) 合并在前面。

至此我们完成了贡献的合并。

模板题

原题解讲的很清楚了,感觉只要理解上面的东西这些都不是啥问题。

#include <bits/stdc++.h>
#define int int64_t
//#define int __int128
//#define MOD (1000000007)
//#define eps (1e-6)
#define endl '\n'
#define debug_endl cout<<endl;
#define debug cout<<"debug"<<endl;
using namespace std;
template<typename T_,T_ mod>
struct ModT_ {
	const static T_ MD = mod;
	T_ x;
	inline ModT_(T_ x = 0): x(x % MD) {}
	T_ get() { return x; }
	inline ModT_ operator + (const ModT_ &that) const { T_ x0 = x + that.x; return ModT_(x0 < MD? x0 : x0 - MD); }
	inline ModT_ operator - (const ModT_ &that) const { T_ x0 = x - that.x; return ModT_(x0 < MD? x0 + MD : x0); }
	inline ModT_ operator * (const ModT_ &that) const { return ModT_((long long)x * that.x % MD); }
	inline ModT_ operator / (const ModT_ &that) const { return *this * that.inverse(); }
	inline ModT_ operator += (const ModT_ &that) { x += that.x; if (x >= MD) x -= MD; return *this;}
	inline ModT_ operator -= (const ModT_ &that) { x -= that.x; if (x < 0) x += MD; return *this;}
	inline ModT_ operator *= (const ModT_ &that) { x = (long long)x * that.x % MD; return *this;}
	inline ModT_ operator /= (const ModT_ &that) { *this = *this / that; return *this;}
	inline bool operator == (const ModT_ &that) { return (x==that.x); }
	inline bool operator != (const ModT_ &that) { return (x!=that.x); }
	inline bool operator > (const ModT_ &that) {return x > that.x; }
	inline bool operator < (const ModT_ &that) {return x < that.x; }
	inline bool operator >= (const ModT_ &that) {return x >= that.x; }
	inline bool operator <= (const ModT_ &that) {return x <= that.x; }
	inline bool operator == (const int &that) { return (x==that); }
	inline bool operator != (const int &that) { return (x!=that); }
	inline bool operator > (const int &that) {return x > that; }
	inline bool operator < (const int &that) {return x < that; }
	inline bool operator >= (const int &that) {return x >= that; }
	inline bool operator <= (const int &that) {return x <= that; }
	inline ModT_ operator = (const int &that) {x=that;return *this;}
	inline ModT_ inverse() const {
		T_ a = x, b = MD, u = 1, v = 0;
		while(b) {
			T_ t = a / b;
			a -= t * b; std::swap(a, b);
			u -= t * v; std::swap(u, v);
		}
		if(u < 0) u += MD;
		return u;
	}
};
typedef ModT_<int,998244353> modint;
struct Contrib{
	modint cntr,cntu,sums,sumi,sumss,sumis;
	Contrib(){cntr=cntu=sums=sumi=sumss=sumis=0;}
	Contrib(int _cntr=0,int _cntu=0,int _sums=0,int _sumi=0,int _sumss=0,int _sumis=0){
		cntr=_cntr,cntu=_cntu,sums=_sums,sumi=_sumi,sumss=_sumss,sumis=_sumis;
	}
	void operator+=(const Contrib& other){
		sumss+=other.sumss+other.cntr*cntu*cntu+other.sums*cntu*2;//后面的s整体抬升cntu,展开∑(cntu+s)^2
		sumis+=other.sumis+other.cntr*cntu*cntr+cntr*other.sums+other.sumi*cntu;//展开∑(cntr+i)(cntu+s)
		sums+=other.sums+other.cntr*cntu;
		sumi+=other.sumi+other.cntr*cntr;
		cntr+=other.cntr;
		cntu+=other.cntu;
	}
	Contrib operator+(const Contrib& other){
		Contrib res=*this;
		res+=other;
		return res;
	}
};
Contrib U(0,1,0,0,0,0),R(1,0,0,1,0,0);
Contrib qpow(Contrib a,int b){
	Contrib res(0,0,0,0,0,0);
	while(b){
		if(b&1) res+=a;
		a+=a;
		b>>=1;
	}
	return res;
}
Contrib solve(int k,int b,int m,int n,Contrib U,Contrib R){
	if(!n) return Contrib(0,0,0,0,0,0);
	if(k>=m) return solve(k%m,b,m,n,U,qpow(U,k/m)+R);
	int nn=(k*n+b)/m;
	if(!nn) return qpow(R,n);
	return qpow(R,(m-b-1)/k)+U+solve(m,(m-b-1)%k,k,nn-1,R,U)+qpow(R,n-((m*nn-b-1)/k));
}
signed main(){
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int T;
	cin>>T;
	while(T--){
		int n,a,b,c;
		cin>>n>>a>>b>>c;
		Contrib res=qpow(U,b/c)+solve(a,b%c,c,n,U,R);
		cout<<(res.sums+(b/c)).x<<' ';
		cout<<(res.sumss+((b/c)*(b/c))).x<<' ';
		cout<<res.sumis.x<<endl;
	}
	return 0;
}
posted @ 2026-09-24 21:37  司马只因锥  阅读(9)  评论(0)    收藏  举报