类欧几里得
例题
https://atcoder.jp/contests/practice2/tasks/practice2_c
在的时间复杂度求:
其中都是整数。
类欧几里得
如果没有下取整函数,我们直接将里面的一次函数求前缀和(用等差数列转成一个二次函数)即可,这是简单的。
但由于有下取整函数,这个问题似乎难以解决。并且之前的套路也无法快速的处理下取整函数。
接下来要将一个神奇的做法——类欧几里得算法(这里只讲最简单的情况,其实这个算法还可以解决很多变式,打算以后开博客讲)
首先我们学过欧几里得算法:,但是其实,这个等式是建立在下面两个等式成立的基础上的:
类欧几里得也是这个思路:
设,尝试进行递归。
对于: ,如果,我们可以直接拆开,故:
于是:,也就像欧几里得算法中第二个式子一样。
之后我们要解决的情况,我们希望对进行一个变换,再递归下去。
此时我们设表示让的最小的一个。并且设,也就是在函数范围内能取到最大的数。特别的,我们设。
于是:
其实就是一个前缀和,等价于等于的的数量。
并且此时,注意到若能表示成一个下取整的形式,那么我们就可以递归了!
还是很好求的,它等于。这个式子是上取整,这可不太妙,意味着我们不好转成一个递归的形式。但是大家都知道,上取整是可以转成下取整的,故:。
所以带入推出的式子中:
所以综上所述:
那么也就是,也就是类似于欧几里得算法的第一个变换。
至此,我们在讨论边界情况时,答案为,就可以愉快的递归了!
时间复杂度显然和欧几里得算法一样,是级别的。
代码还是很简单的:
#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;
}
常用套路
我们在求解一些整除问题时可以使用这个算法。我们可能会遇到统计两个函数内没有某个数的倍数的问题,此时我们可以二分找到两个函数差的绝对值的左右两端,此时内部最多只能出现一个的倍数。只需用类欧几里得求区间和,再相减即可。
比如下面的两道题:ARC111E和ARC123E。
ARC111E
我们设:
,由于题中说了,故。
若,那么显然区间内部至少有一个数是的倍数。
我们二分出最大的点,满足。(其实也可以算,不过有精度误差问题,并且最后类欧几里得时你还是要的计算)
那么对于,区间内只能有一个的倍数,要么没有。
我们直接求出,这样我们就算出区间内有多少个的倍数了,然而每个点最多只能有一个的倍数,所以实际上就是多少个使得区间内有的倍数。我们直接将减去即可算出区间内没有的倍数的的个数。
也就是,显然这个求和式是类欧几里得板子。
注意题中是不算的。如果不为时,我们会将也统计进去,此时我们要将答案减一。
时间复杂度为。
#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是类似的,不过添加了另一侧的情况。
如果,我们交换。否则我们设:
这题就是想让我们求的的个数。
那么还是同上题的套路,我们求出左端点为满足的最小的点,右端点为满足最大的点。并且。
如果不存在,说明没有这样的点,直接输出。
为了以防万一,我还特判了,输出;以及当时,若,输出,否则输出。
不同于上一题,这题有左右两个部分,我们要找到和的交点,并分几种情况讨论:
第一种是,也就是下图:
同ARC111E,我们求出,就是答案。
还有一种是都在左侧,答案就是。
最后一种是都在右侧,答案就是。
实现时可以写一个函数,解决左侧和右侧的不同统计方式。(其实就是一侧,另一侧是)
#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
首先我们观察到:
左半部分就是一个等差数列,求法是简单的,右边部分则是我们刚才提到的类欧几里得板子。
于是我们可以轻松计算一段区间修改了。考虑这题是多次修改区间,使用离散化+线段树就可以做到查询,加上类欧几里德就是两个,是可以通过的。
注意线段树维护的是区间,并且要将区间拆成几个部分。比如我们将所有区间左右节点排序后,有:
这几个点,那么我们建立个区间,分别是:
并用线段树维护这个区间即可。
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!