CDQ分治
Part1:前置知识
归并排序,逆序对,二维偏序,树状数组
Part 2:CDQ分治
【模板题】三维偏序
题目大意
有
对于
解题思路
-
同“二维偏序”,先按
数组从小到大排序 -
现在考虑当
时,首先将数组一分为二,递归左边 ,递归右边 , 再计算左边对右边的影响(即左边是否有元素能被右边的元素统计进它的 ) -
我们假设左边和右边内部的答案都已经计算得出,那么再来考虑左边对右边的贡献(影响)。此时问题就又变成了一个二维偏序,我们可以在左右两个区间内部按
的大小排序(因为内部答案已算出,内部排序不影响最终结果),再利用树状数组求出左边对右边的贡献. -
那么我们不断地对一个区间一分为二地递归,再计算左边对右边的影响,就可以计算出答案
注意事项
-
因为题目的
的判断条件可以取等号,所以我们排序完后需要将数组去重 -
每层递归后,我们需要将树状数组清空。但用
可能会超时,所以需要一个数组来记录修改的元素
代码
#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=200010;
struct node
{
int a,b,c,cnt,num; //cnt表示f(i)的大小(不取等号),num表示该元素的个数
}t[N],f[N];
int n,k,tot,c[M],ans[N];
bool cmp1(node x,node y)
{
return x.a<y.a || (x.a==y.a && x.b<y.b) || (x.a==y.a && x.b==y.b && x.c<y.c);
}
bool cmp2(node x,node y)
{
return x.b<y.b || (x.b==y.b && x.c<y.c);
}
void add(int x,int y)
{
for(x; x<=k; x+=(x&-x))
c[x]+=y;
}
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s+=c[x];
return s;
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid); //一分为二地递归
solve(mid+1,r);
int len1=mid-l+1,len2=r-mid;
sort(f+l,f+l+len1,cmp2); //按b的大小排序
sort(f+mid+1,f+mid+1+len2,cmp2);
vector <int> rec; //统计树状数组修改了哪个元素
for(int i=l,j=mid+1; j<=r; j++)
{
while(i<=mid && f[i].b<=f[j].b)
{
add(f[i].c,f[i].num);
rec.push_back(i);
i++;
}
f[j].cnt+=ask(f[j].c); //更新答案
}
for(int i=0; i<rec.size(); i++) //清空树状数组
add(f[rec[i]].c,-f[rec[i]].num);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1; i<=n; i++)
scanf("%d%d%d",&t[i].a,&t[i].b,&t[i].c);
sort(t+1,t+1+n,cmp1); //按照a的大小排序
int tt=0;
for(int i=1; i<=n; i++) //去重
{
tt++;
if(t[i].a!=t[i+1].a || t[i].b!=t[i+1].b || t[i].c!=t[i+1].c)
{
f[++tot].a=t[i].a;
f[tot].b=t[i].b;
f[tot].c=t[i].c;
f[tot].num=tt;
tt=0;
}
}
solve(1,tot); //CDQ分治
for(int i=1; i<=tot; i++) //统计答案
ans[f[i].cnt+f[i].num-1]+=f[i].num;
for(int i=0; i<n; i++)
printf("%d\n",ans[i]);
return 0;
}
*总结:CDQ分治的模型
对于区间
1.设
2.递归计算
3.计算第
时间复杂度:
关于上述模型的正确性,大家可自行证明
【练习题】Mokia
(题目传送门)
题目大意
维护一个
每次操作可以增加某格子的权值,或询问某子矩阵的总权值
。
解题思路
-
首先,如二维前缀和一般,对于左下角为
,右上角为 的询问,我们可以把它转化为四个询问: , , , -
之后,我们发现,对于第
项查询,必定会受到前面修改操作的影响,因此,我们可以考虑CDQ分治。类似三维偏序,此问题的查询也有三维:时间 ,行 ,列 。所以,我们只需要寻找并计算 且 且 的第 项修改对第 项查询的影响 -
我们先对整个区间一分为二,对于两个独立的区间在里面进行CDQ分治,虽然右边的修改不会对左边的查询产生影响,但左边的修改会对右边的查询产生影响,所以我们还需计算左对右的影响
-
计算左对右的影响时,因为左边的
始终小于右边的 ,所以问题就变成了一个二维偏序:对 且 的第 项修改进行计算
代码
#include<bits/stdc++.h>
using namespace std;
const int N=2000000,M=200010;
struct node
{
int op,x,y; //op表示操作类型,x,y表示坐标
int val,id; //val对于操作1来说就是增加量,对于操作2来说就是前缀和运算时是加还是减
//id是对于操作2来说的,表示是第几个查询操作
}a[M];
int s,w,n,cnt,ans[M]; //n表示操作序列长度,cnt表示查询操作个数
int c[N]; //树状数组
bool cmp(node a,node b)
{
return a.x<b.x || (a.x==b.x && a.y<b.y);
}
void add(int x,int y)
{
for(x; x<=w; x+=(x&-x))
c[x]+=y;
}
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s+=c[x];
return s;
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid);
solve(mid+1,r);
int len1=mid-l+1,len2=r-mid;
sort(a+l,a+l+len1,cmp); //对左半边和右半边排序
sort(a+mid+1,a+mid+1+len2,cmp);
vector <int> rec;
for(int i=l,j=mid+1; j<=r; j++)
{
while(i<=mid && a[i].x<=a[j].x) //寻找xi<=xj
{
if(a[i].op==1) //如果是操作1
{
add(a[i].y,a[i].val);
rec.push_back(i);
}
i++;
}
ans[a[j].id]+=a[j].val*ask(a[j].y); //更新答案
}
for(int i=0; i<rec.size(); i++) //恢复树状数组
add(a[rec[i]].y,-a[rec[i]].val);
}
int main()
{
cin>>s>>w; //s无用
int temp;
while(cin>>temp && temp!=3)
{
if(temp==1)
{
int x,y,v;
cin>>x>>y>>v;
a[++n]=(node){1,x,y,v,0};
}
else
{
int x,y,xx,yy;
cin>>x>>y>>xx>>yy;
cnt++; //记录查询操作个数
a[++n]=(node){2,x-1,y-1,1,cnt}; //拆分成4次查询操作
a[++n]=(node){2,xx,y-1,-1,cnt};
a[++n]=(node){2,x-1,yy,-1,cnt};
a[++n]=(node){2,xx,yy,1,cnt};
}
}
solve(1,n);
for(int i=1; i<=cnt; i++)
cout<<ans[i]<<endl;
return 0;
}
【练习题】天使玩偶
(题目传送门)
题目大意
- 定义两个点之间的距离为
- 在刚开始时,Ayu 已经知道有
个点可能埋着天使玩偶 - 再接下来
行,每行三个非负整数- 如果
,则表示 Ayu 又回忆起了一个可能埋着玩偶的点 - 如果
,则表示 Ayu 询问如果她在点 那么在已经回忆出来的点里,离她近的那个点有多远
- 如果
解题思路
-
首先来看问题的简化版——假设没有
的操作,这时问题的答案很明显为 -
为了去掉绝对值符号,我们不妨把原来的询问分为
个,分别询问在 的左下、左上、右上、右下方向上距离最近的点有多远, 个结果取最小值即为答案。以左下方向为例,此时要求的式子变为:
进一步化简为:
-
所以,对于左下方向的点,我们可以先将所有点按横坐标从小到大排序,再利用树状数组去求出
,那么就完成了对左下方向的求解 -
对于其它三个方向,我们可以通过坐标的变换,把它们均转化为左下方向
-
那么现在,我们就要来考虑带有
的操作,我们可以把输入变成一个长度为 的序列,进行 次CDQ分治,即可求出答案
注意事项
-
如果在
次CDQ里的每一层都进行sort排序,那么复杂度会变得非常大,所以我们在操作的过程中顺便进行归并排序,这样便大大节省了时间 -
由于此题是一个平面直角坐标系,坐标可能会取到0,但树状数组在进行
运算时会出错,所以要将输入的所有坐标+1 -
再进行坐标变换时,坐标会变成负数,此时需要给坐标加上一个偏移量,这个偏移量 =
,注意要+1,否则最大的那个坐标变换后会变为0 -
有一种特殊情况:某一点非常靠近边界,导致某次变换时,没有点在它的左下。这样查询时默认返回了0,最终的距离就成了这个点到原点的距离,但原点是不存在的(经过刚刚的更改,已经没有横坐标或纵坐标为0的点)。为避免这种情况,在树状数组查询时需要特判,若为0则返回
。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000010,M=1000010,INF=1e9;
struct node
{
int op,x,y;
int ans,id;
}a[N],b[N],s[N]; //b用于CDQ里的操作,s用于归并排序
int n,m,mx,c[M];
bool cmp(node a,node b)
{
return a.x<b.x;
}
void add(int x,int y)
{
for(; x<=mx; x+=(x&-x))
c[x]=max(c[x],y);
} //树状数组查询最大值
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s=max(c[x],s);
return s==0? -INF:s; //特殊情况
}
void clear_(int x)
{
for(; x<=mx; x+=(x&-x))
c[x]=0; //清空树状数组
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid);
solve(mid+1,r);
int i=l,k=l;
for(int j=mid+1; j<=r; j++)
{
while(i<=mid && b[i].x<=b[j].x)
{
if(b[i].op==1)
add(b[i].y,b[i].x+b[i].y);
s[k++]=b[i++]; //顺便进行归并排序
}
if(b[j].op==2)
a[b[j].id].ans=min(a[b[j].id].ans,b[j].x+b[j].y-ask(b[j].y)); //更新答案
s[k++]=b[j];
}
for(int j=l; j<i; j++) //清空树状数组
if(b[j].op==1)
clear_(b[j].y);
while(i<=mid) //归并排序的善后工作
s[k++]=b[i++];
for(int i=l; i<=r; i++) //将排好序的s数组赋值给b数组
b[i]=s[i];
}
void cdq(int xx,int yy) //xx和yy控制x和y坐标是否变换
{
for(int i=1; i<=n+m; i++)
{
b[i]=a[i];
if(!xx)
b[i].x=-b[i].x+mx;
if(!yy)
b[i].y=-b[i].y+mx;
}
solve(1,n+m);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
int xx,yy;
scanf("%d%d",&xx,&yy);
a[i].x=++xx; a[i].y=++yy;
a[i].op=1; a[i].id=i;
mx=max(mx,max(xx,yy));
}
for(int i=n+1; i<=n+m; i++)
{
int t,xx,yy;
scanf("%d%d%d",&t,&xx,&yy);
a[i].x=++xx; a[i].y=++yy;
a[i].op=t; a[i].id=i;
a[i].ans=INF;
mx=max(mx,max(xx,yy));
}
mx++;
cdq(1,1); cdq(1,0); cdq(0,1); cdq(0,0); //4次cdq
for(int i=n+1; i<=n+m; i++)
if(a[i].op==2)
printf("%d\n",a[i].ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探