类欧几里得

例题

https://atcoder.jp/contests/practice2/tasks/practice2_c
O(log(n+m+k+b))的时间复杂度求:

i=0n1ki+bm

其中n,m,k,b都是整数。

类欧几里得

如果没有下取整函数,我们直接将里面的一次函数求前缀和(用等差数列转成一个二次函数)即可,这是简单的。

但由于有下取整函数,这个问题似乎难以解决。并且之前的套路也无法快速的处理下取整函数。

接下来要将一个神奇的做法——类欧几里得算法(这里只讲最简单的情况,其实这个算法还可以解决很多变式,打算以后开博客讲)

首先我们学过欧几里得算法:gcd(x,y)=gcd(y,x%y),但是其实,这个等式是建立在下面两个等式成立的基础上的:

gcd(x,y)=gcd(y,x)gcd(x,y)=gcd(x,y%x)

类欧几里得也是这个思路:

solve(k,b,m,n)=i=0n1ki+bm,尝试进行递归。

对于:i=0n1ki+bm ,如果k,b>m,我们可以直接拆开,故:

i=0n1ki+bm=i=0n1(k%m)i+(b%m)m+n(n1)2km+nbm

于是:solve(k,b,m,n)=solve(k%m,b%m,m,n)+n(n1)2km+nbm,也就像欧几里得算法中第二个式子一样。

之后我们要解决k,bm的情况,我们希望对(k,b,m,n)进行一个变换,再递归下去。

此时我们设smallest(j)表示让ki+bm=j的最小的一个i。并且设MAX=nk+bm,也就是在函数范围内能取到最大的数。特别的,我们设smallest(MAX+1)=n

于是:

i=0n1ki+bm=i=0MAXi(smallest(i+1)smallest(i))=MAXsmallest(MAX+1)i=1MAXsmallest(i)=nMAXi=0MAX1smallest(i+1)

其实smallest(i+1)smallest(i)就是一个前缀和,等价于等于iki+bm=j的数量。

并且此时,注意到i=0MAX1smallest(i+1)若能表示成一个下取整的形式,那么我们就可以递归了!

smallest(i)还是很好求的,它等于mibk。这个式子是上取整,这可不太妙,意味着我们不好转成一个递归的形式。但是大家都知道,上取整是可以转成下取整的,故:smallest(i)=mibk=mib+k1k

所以带入推出的式子中:

nMAXi=0MAX1smallest(i+1)=nMAXi=0MAX1m(i+1)b+k1k=nMAXi=0MAX1mi+m+kb1k

所以综上所述:

i=0n1ki+bm=nMAXi=0MAX1mi+m+kb1k

那么也就是solve(k,b,m,n)=nMAXsolve(m,m+kb1,k,MAX),也就是类似于欧几里得算法的第一个变换。

至此,我们在讨论边界情况n=0时,答案为0,就可以愉快的递归了!

时间复杂度显然和欧几里得算法一样,是log级别的。

代码还是很简单的:

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

using ll=long long;

ll solve(ll k,ll b,ll m,ll n) {
	if(n==0) return 0; 
	if(k>=m||b>=m) {
		return solve(k%m,b%m,m,n)+(n*(n-1)/2ll)*(k/m)+n*(b/m);
	} else {
		ll MAX=(k*(n-1)+b)/m;
		return n*MAX-solve(m,m+k-b-1,k,MAX);
	}
}

int main() {
	int k,b,m,n,T;
	scanf("%d",&T);
	while(T--) {
		std::cin>>n>>m>>k>>b;
		std::cout<<solve(k,b,m,n)<<'\n';
	}
	return 0;
}

常用套路

我们在求解一些整除问题时可以使用这个算法。我们可能会遇到统计两个函数内没有某个数D的倍数的问题,此时我们可以二分找到两个函数差的绝对值1的左右两端,此时内部最多只能出现一个D的倍数。只需用类欧几里得求区间和,再相减即可。

比如下面的两道题:ARC111E和ARC123E。

ARC111E

我们设:
f(x)=bx+a,g(x)=cx+a,由于题中说了b<c,故f(x)g(x)

g(x)f(x)d,那么显然区间[f(x),g(x)]内部至少有一个数是d的倍数。

我们二分出最大的点n,满足g(x)f(x)<d。(其实也可以O(1)算,不过有精度误差问题,并且最后类欧几里得时你还是要O(logn)的计算)

那么对于i=0,1,...,n,区间[f(x),g(x)]内只能有一个d的倍数,要么没有。

我们直接求出cnt=i=0n1g(x)di=0n1f(x)1d,这样我们就算出区间内有多少个d的倍数了,然而每个点最多只能有一个d的倍数,所以cnt实际上就是多少个i使得区间内有d的倍数。我们直接将(n+1)减去cnt即可算出区间内没有d的倍数的i的个数。

也就是answer=n+1(i=0n1g(x)di=0n1f(x)1d),显然这个求和式是类欧几里得板子。

注意题中是不算0的。如果amodd不为0时,我们会将0也统计进去,此时我们要将答案减一。

时间复杂度为log(a+b+c)

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

using ll=long long;

ll lgcd(ll k,ll b,ll m,ll n) {
	if(n==0) return 0ll;
	if(k>=m||b>=m) {
		return lgcd(k%m,b%m,m,n)+(k/m)*(n*(n-1)/2ll)+(b/m)*n;
	} else {
		ll MAX=((n-1)*k+b)/m;
		return n*MAX-lgcd(m,m+k-b-1,k,MAX);
	}
}

void solve() {
	ll a,b,c,d;
	scanf("%lld%lld%lld%lld",&a,&b,&c,&d);
	ll lef=0,rig=1e10,p=-1;
	while(lef<=rig) {
		ll mid=lef+rig>>1ll;
		if((a+c*mid)-(a+b*mid)<d) {
			p=mid;
			lef=mid+1;
		} else {
			rig=mid-1;
		}
	}
	ll ans=p+1-(lgcd(c,a,d,p+1)-lgcd(b,a-1,d,p+1));
	if(a%d!=0) ans--;
	printf("%lld\n",ans);
	return;
}

int main() {
	int T;
	scanf("%d",&T);
	while(T--) solve();
	return 0;
}

ARC123E

其实这题和ARC111E是类似的,不过添加了另一侧的情况。

如果Bx<By,我们交换Ax,Ay,Bx,By。否则我们设:

f(x)=x+AxBxBxg(x)=x+AyByByF(x)=f(x)G(x)=g(x)

这题就是想让我们求F(x)=G(x)1xn的个数。

那么还是同上题的套路,我们求出左端点l为满足f(x)g(x)1的最小的点,右端点r为满足g(x)f(x)1最大的点。并且1l,rn

如果l,r不存在,说明没有这样的点,直接输出0

为了以防万一,我还特判了l>r,输出0;以及当Bx=By时,若Ax=Ay,输出n,否则输出0

不同于上一题,这题有左右两个部分,我们要找到f(x)g(x)的交点p,并分几种情况讨论:

第一种是lpr,也就是下图:

同ARC111E,我们求出(pl+1)i=lp(F(i)G(i))+(rp)i=p+1r(G(i)F(i)),就是答案。

还有一种是l,r都在p左侧,答案就是(rl+1)i=lr(F(i)G(i))

最后一种是l,r都在p右侧,答案就是(rl+1)i=lr(G(i)F(i))

实现时可以写一个函数,解决左侧和右侧的不同统计方式。(其实就是一侧F(i)>G(i),另一侧是F(i)<G(i)

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

using ll=long long;

ll n,k1,k2,b1,b2,m1,m2;

double f(ll x) {
	return double(k1*x+b1)/(double)m1;
}

double g(ll x) {
	return double(k2*x+b2)/(double)m2;
}

ll solve(ll k,ll b,ll m,ll n) {
	if(n==0) return 0ll;
	if(k>=m||b>=m) {
		return solve(k%m,b%m,m,n)+(k/m)*(n*(n-1)/2ll)+(b/m)*n;
	} else {
		ll MAX=((n-1)*k+b)/m;
		return n*MAX-solve(m,m+k-b-1,k,MAX);
	}
}

ll get(ll l,ll r,ll type) {
	if(type==1) {
		return (r-l+1)-(solve(k1,b1,m1,r+1)-solve(k1,b1,m1,l))+(solve(k2,b2,m2,r+1)-solve(k2,b2,m2,l));
	} else {
		return (r-l+1)-(solve(k2,b2,m2,r+1)-solve(k2,b2,m2,l))+(solve(k1,b1,m1,r+1)-solve(k1,b1,m1,l));
	}
}

void solve() {
	ll a,b,c,d;
	scanf("%lld%lld%lld%lld%lld",&n,&a,&b,&c,&d);
	m1=b,k1=1,b1=a*b;
	m2=d,k2=1,b2=c*d;
	//f(x)=(k1*x+b1)/m1; (k1=k2=1)
	//g(x)=(k2*x+b2)/m2; 
	if(m1<m2) {
		std::swap(m1,m2);
		std::swap(k1,k2);
		std::swap(b1,b2);
	} else if(m1==m2) {
		if(b1==b2) printf("%lld\n",n);
		else printf("0\n");
		return;
	}
	ll l=-1,r=-1,lef,rig;
	lef=1,rig=n;
	while(lef<=rig) {
		ll mid=lef+rig>>1;
		if(g(mid)-f(mid)<=1) {
			r=mid; lef=mid+1;
		} else {
			rig=mid-1;
		}
	}
	lef=1,rig=n;
	while(lef<=rig) {
		ll mid=lef+rig>>1;
		if(f(mid)-g(mid)<=1) {
			l=mid; rig=mid-1;
		} else {
			lef=mid+1;
		}
	}
	if(l==-1||r==-1||l>r) {
		printf("0\n");
		return;
	}
	
	ll p=(b2*m1-b1*m2)/(k1*m2-k2*m1),ans=0;	
	
	if(p<l) {
		ans=get(l,r,2);
	} else if(r<=p) {
		ans=get(l,r,1);
	} else {
		ans=get(l,p,1)+get(p+1,r,2);
	}
	
	printf("%lld\n",ans);
	return;
}

int main() {
	int T;
	scanf("%d",&T);
	while(T--) solve();
	return 0;
}

Luogu P433 ALADIN

首先我们观察到:

(iL+1)×AmodB=(iL+1)×AA×iA×L+AB×B

左半部分就是一个等差数列,求法是简单的,右边部分则是我们刚才提到的类欧几里得板子。

于是我们可以轻松计算一段区间修改了。考虑这题是多次修改区间,使用离散化+线段树就可以做到O(logn)查询,加上类欧几里德就是两个log,是可以通过的。

注意线段树维护的是区间,并且要将区间拆成几个部分。比如我们将所有区间左右节点排序后,有:

1,4,6,10,11

这几个点,那么我们建立8个区间,分别是:

[1,1],[2,3],[4,4],[5,5],[6,6],[7,9],[10,10],[11,11]

并用线段树维护这8个区间即可。

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

using ll=long long;


ll solve(ll k,ll b,ll m,ll n) {
	//类欧几里德 
	if(n==0) return 0ll;
	if(k>=m||b>=m) {
		return solve(k%m,b%m,m,n)+(k/m)*(n*(n-1)/2ll)+(b/m)*n;
	} else {
		ll MAX=((n-1)*k+b)/m;
		return n*MAX-solve(m,m+k-b-1,k,MAX);
	}
}

const int maxn=300005;

int n,q;
std::vector<int> vec;
std::vector<std::pair<int,int>> seg;

struct node {
	ll sum;
	int real_left,real_right;
	int lzy,lzy_A,lzy_B,lzy_begin;
}tree[maxn<<2];

void build(int pos,int lef,int rig) {
	tree[pos].real_left=seg[lef-1].first,tree[pos].real_right=seg[rig-1].second;
	tree[pos].sum=0; tree[pos].lzy=0;
	if(lef!=rig) {
		int mid=lef+rig>>1;
		build(pos<<1,lef,mid);
		build(pos<<1|1,mid+1,rig);
	}
}

ll upd(int pos,int begin,int A,int B) {
	ll first=tree[pos].real_left-begin+1,last=tree[pos].real_right-begin+1;
	tree[pos].sum=(first+last)*(tree[pos].real_right-tree[pos].real_left+1)/2ll*(ll)A;	
	ll b_=(ll)(-begin)*A+A,k_=A,m_=B;
	tree[pos].sum-=(solve(k_,b_,m_,tree[pos].real_right+1)-solve(k_,b_,m_,tree[pos].real_left))*(ll)B;	
	/////tagged//////
	tree[pos].lzy=1,tree[pos].lzy_begin=begin,tree[pos].lzy_A=A,tree[pos].lzy_B=B;
}

void pushdown(int pos) {
	if(tree[pos].lzy==0) return;
	upd(pos<<1,tree[pos].lzy_begin,tree[pos].lzy_A,tree[pos].lzy_B);
	upd(pos<<1|1,tree[pos].lzy_begin,tree[pos].lzy_A,tree[pos].lzy_B);
	tree[pos].lzy=0;
}

void update(int l,int r,int A,int B,int pos,int lef,int rig) {
	if(l<=lef&&rig<=r) {
		upd(pos,seg[l-1].first,A,B);//注意这里是seg[l-1].first而不是vec[l-1]!!! 
	} else if(l<=rig&&r>=lef) {
		pushdown(pos);
		int mid=lef+rig>>1;
		update(l,r,A,B,pos<<1,lef,mid);
		update(l,r,A,B,pos<<1|1,mid+1,rig);
		tree[pos].sum=tree[pos<<1].sum+tree[pos<<1|1].sum;
	}
}

ll query(int l,int r,int pos,int lef,int rig) {
	if(l<=lef&&rig<=r) {
		return tree[pos].sum;
	} else if(l<=rig&&r>=lef) {
		pushdown(pos);
		int mid=lef+rig>>1;
		return query(l,r,pos<<1,lef,mid)+query(l,r,pos<<1|1,mid+1,rig);
	}
	return 0ll;
}

int opt[maxn],l[maxn],r[maxn],a[maxn],b[maxn];

int main() {
	scanf("%d%d",&n,&q);
	for(int i=1;i<=q;i++) {
		scanf("%d%d%d",&opt[i],&l[i],&r[i]);
		if(opt[i]==1) scanf("%d%d",&a[i],&b[i]);
		vec.push_back(l[i]);
		vec.push_back(r[i]);
	}
	std::sort(vec.begin(),vec.end());
	vec.erase(std::unique(vec.begin(),vec.end()),vec.end());
	for(int i=0;i<(int)vec.size();i++) {
		seg.push_back({vec[i],vec[i]});
		if(i&&vec[i]-vec[i-1]>=2) seg.push_back({vec[i-1]+1,vec[i]-1});
	}
	std::sort(seg.begin(),seg.end());
	build(1,1,(int)seg.size());
	for(int i=1;i<=q;i++) {
		int from=std::lower_bound(seg.begin(),seg.end(),std::make_pair(l[i],l[i]))-seg.begin()+1;
		int to=std::lower_bound(seg.begin(),seg.end(),std::make_pair(r[i],r[i]))-seg.begin()+1;
		if(opt[i]==1) {
			update(from,to,a[i],b[i],1,1,(int)seg.size());	
		} else {
			printf("%lld\n",query(from,to,1,1,(int)seg.size()));
		}
	}
	return 0;
} 
posted @   Nastia  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示