cdq分治 基础篇
简介
前置芝士:归并排序。
陌上花开
维护三维偏序有个口诀:一维排序,二维归并,三维数据结构。
考虑第一维直接排序解决掉,然后还剩两维。
我们考虑第二维用归并排序解决掉。然后假设当前区间
我们在递归时已经按照第二维排好序了。但是有个问题,第一维的限制呢?这样不是把第一维的限制忽略掉了吗?
再想一想我们求的东西,两个端点分别在两个区间内,而这时两个区间内的第一维的相对大小仍然是满足要求的,因为这时他们还没有完成第二维的排序。
然后如果我们发现这时第二维满足了要求,我们就把第三维扔进树状数组或者权值线段树内(推荐树状数组)。否则(重点),此时的
然后就是,我们把没跑完的部分跑完,但是这里的循环顺序非常重要,不能更改,具体原因写在代码注释里了。
最后记得堆原数组去个重,不然会出问题。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,k,c[N],siz[N],f[N],res[N];
struct node{
int x,y,z,id;
bool operator<(const node &t)const{
if(x!=t.x)return x<t.x;
if(y!=t.y)return y<t.y;
return z<t.z;
}
bool operator!=(const node &t)const{
return x!=t.x||y!=t.y||z!=t.z;
}
}a[N],b[N];
int lowbit(int x){//树状数组三件套1
return x&-x;
}
void add(int x,int v){//树状数组三件套2
while(x<=k){
c[x]+=v;
x+=lowbit(x);
}
}
int qry(int x){//树状数组三件套3
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;//求有多少数比x小
}
void cdq(int l,int r){
if(l>=r)return;
int mid=l+r>>1;
cdq(l,mid);cdq(mid+1,r);//归并排序
int i=l,j=mid+1,now=0;
while(i<=mid&&j<=r){
if(a[i].y<=a[j].y){//如果满足,证明还不是极大区间
add(a[i].z,siz[a[i].id]);//那么就把这个数存进去,注意有多个一起存
b[now++]=a[i++];//归并排序
}
else{
res[a[j].id]+=qry(a[j].z);//这时区间达到极大,可以统计
b[now++]=a[j++];
}
}
while(j<=r){//1,如果放在2后面会导致统计错误
res[a[j].id]+=qry(a[j].z);
b[now++]=a[j++];
}
for(int x=l;x<i;x++){//2,如果放到3后面会导致清空错误(多清空)
add(a[x].z,-siz[a[x].id]);
}
while(i<=mid){//3,不能放到2前面,原因见2
b[now++]=a[i++];
}//上面这3个循环的顺序不能换
for(i=l,j=0;i<=r;i++,j++){
a[i]=b[j];//归并排序
}
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y>>a[i].z;
}
sort(a+1,a+n+1);
for(int i=1;i<=n;i++){
if(a[i]!=a[i-1])b[++m]=a[i];//这里是去重
siz[m]++;//记录相同的元素个数
}
for(int i=1;i<=m;i++){
a[i]={b[i].x,b[i].y,b[i].z,i};//记录去重后的数组
}
cdq(1,m);
for(int i=1;i<=m;i++){
int id=a[i].id;
//数量为去重后统计的答案再加上其他相同的元素的数量
f[res[id]+siz[id]-1]+=siz[id];//这里减1是因为去掉自己
}
/*
这里笔者写的时候有个疑惑,为什么把id改成i是对的,原因如下:
首先,这两个写法从逻辑上来说不等价,但是结果上来说等价
因为原来的id是按照第一维排序的,上面的i是按照第二维排序的,所以两者本质不同
但是,由于id是不重复的,所以id恰好遍历1-m
所以从结果上来说两者等价
*/
for(int i=0;i<n;i++){
cout<<f[i]<<'\n';
}
return 0;
}
园丁的烦恼
这里说一下
就是比方说求的东西是
然后再考虑,把初始给定点的
但是,可以发现
于是看一下代码:
#include<bits/stdc++.h>
#define N 500005
using namespace std;
int n,m,res[N];
struct node{
int x,y,z,p,id,sign,sum;
//sign为求前缀和这个矩阵是被加还是减
bool operator<(const node &t)const{
if(x!=t.x)return x<t.x;
if(y!=t.y)return y<t.y;
return z<t.z;
}
}q[N*5],tmp[N*5];
void cdq(int l,int r){
if(l>=r)return;
int mid=l+r>>1;
cdq(l,mid);
cdq(mid+1,r);
int i=l,j=mid+1,k=0,sum=0;
while(i<=mid&&j<=r){
if(q[i].y<=q[j].y){
sum+=(q[i].z==0)*q[i].p;//如果i是初始点才计算贡献
tmp[k++]=q[i++];
}
else{
q[j].sum+=sum;//还是在区间达到极大时进行统计
tmp[k++]=q[j++];
}
}
while(i<=mid){
sum+=(q[i].z==0)*q[i].p;//没啥用,但是美观
tmp[k++]=q[i++];
}
while(j<=r){
q[j].sum+=sum;//区间已经极大,不会再扩展
tmp[k++]=q[j++];
}
for(i=l,j=0;i<=r;i++,j++){
q[i]=tmp[j];
}
}
signed main(){
cin>>n>>m;
for(int i=0;i<n;i++){
int x,y;
cin>>x>>y;
q[i]={x,y,0,1};//0代表是初始就有的
}
int k=n;
for(int i=1;i<=m;i++){
int x_1,y_1,x_2,y_2;
cin>>x_1>>y_1>>x_2>>y_2;
q[k++]={x_2,y_2,1,0,i,1};
q[k++]={x_1-1,y_2,1,0,i,-1};
q[k++]={x_2,y_1-1,1,0,i,-1};
q[k++]={x_1-1,y_1-1,1,0,i,1};//前缀和的四个部分
}
sort(q,q+k);
cdq(0,k-1);
for(int i=0;i<k;i++){
if(q[i].z==1){
res[q[i].id]+=q[i].sum*q[i].sign;//这里在求前缀和
}
}
for(int i=1;i<=m;i++){
cout<<res[i]<<'\n';
}
return 0;
}
Little Artem and Time Machine
板子题。考虑开桶维护元素个数。然后第一维就是操作顺序,第二维是操作时间,第三维是元素值。
可以发现第一维不用动,我们对第二维归并排序,相当于考虑
所以就是在发现当前
然后就是,我们为了开桶,需要把元素值离散化一下,于是就做完了,可以看一下代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,tot,b[N],res[N],cnt[N];
struct node{
int op,t,x,id;
}q[N],tmp[N];
void cdq(int l,int r){
if(l>=r)return;
int mid=l+r>>1;
cdq(l,mid);
cdq(mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r){
if(q[i].t<=q[j].t){
if(q[i].op==1)cnt[q[i].x]++;
else if(q[i].op==2)cnt[q[i].x]--;
tmp[k++]=q[i++];
}
else{
if(q[j].op==3)res[q[j].id]+=cnt[q[j].x];//这里必须是+=,因为贡献会多次累加
tmp[k++]=q[j++];
}
}
while(i<=mid){
if(q[i].op==1)cnt[q[i].x]++;
else if(q[i].op==2)cnt[q[i].x]--;
tmp[k++]=q[i++];
}
while(j<=r){
if(q[j].op==3)res[q[j].id]+=cnt[q[j].x];
tmp[k++]=q[j++];
}
for(i=l;i<=mid;i++)cnt[q[i].x]=0;
for(i=l,j=0;i<=r;i++,j++)q[i]=tmp[j];
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>q[i].op>>q[i].t>>q[i].x;
b[i]=q[i].x;
if(q[i].op==3)q[i].id=++tot;
}
sort(b+1,b+n+1);
m=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
q[i].x=lower_bound(b+1,b+m+1,q[i].x)-b;
}
cdq(1,n);
for(int i=1;i<=tot;i++){
cout<<res[i]<<'\n';
}
return 0;
}
动态逆序对
我们先设每个位置
但是这样会要求我们用树状数组维护后缀和,这个东西比前缀和难维护,所以我们把删除时间翻转一下(即最早删除的
然后我们考虑把逆序对的贡献记录到删除时间更晚的上面(但是实际的
所以现在如果我们对于
-
。 -
。
然后就做两遍统计即可,代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,tr[N],ans[N],pos[N];
struct node{
int a,t,res;
}q[N],w[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int v){
for(int i=x;i<N;i+=lowbit(i)){
tr[i]+=v;
}
}
int qry(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
void merge_sort(int l,int r){
if(l>=r)return;
int mid=l+r>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=mid,j=r;
while(i>=l&&j>=mid+1){//这里倒着统计是因为保证[i+1,l]的val都比j的val大
if(q[i].a>q[j].a){//i<j,t_i<t_j,val_i>val_j
add(q[i].t,1);
i--;
}
else{
q[j].res+=qry(q[j].t-1);
j--;
}
}
while(j>=mid+1)q[j].res+=qry(q[j].t-1),j--;//i的循环没必要做,因为不计入答案
for(int k=i+1;k<=mid;k++){//清空
add(q[k].t,-1);
}
j=l;i=mid+1;
while(j<=mid&&i<=r){//这里正着统计的原因同上
if(q[i].a<q[j].a){//i>j,t_i<t_j,val_i<val_j
add(q[i].t,1);
i++;
}
else{
q[j].res+=qry(q[j].t-1);
j++;
}
}
while(j<=mid)q[j].res+=qry(q[j].t-1),j++;
for(int k=mid+1;k<i;k++){
add(q[k].t,-1);
}
i=l;j=mid+1;//归并排序
int k=0;
while(i<=mid&&j<=r){
if(q[i].a<=q[j].a){
w[k++]=q[i++];
}
else{
w[k++]=q[j++];
}
}
while(i<=mid)w[k++]=q[i++];
while(j<=r)w[k++]=q[j++];
for(i=l,j=0;j<k;i++,j++)q[i]=w[j];
}
signed main(){
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>q[i].a;
pos[q[i].a]=i;
}
for(int i=0,j=n;i<m;i++){
int a;
cin>>a;
q[pos[a]].t=j--;//这里倒着记录,方便统计
pos[a]=-1;
}
for(int i=1,j=n-m;i<=n;i++){
if(pos[i]!=-1){
q[pos[i]].t=j--;//对于没有被删除的这样就可以
}
}
merge_sort(0,n-1);
for(int i=0;i<n;i++)ans[q[i].t]=q[i].res;//这个贡献是在这个时间上的,需要前缀和得到答案
for(int i=1;i<=n;i++)ans[i]+=ans[i-1];//对每个时间做前缀和,得到要求的答案
for(int i=0,j=n;i<m;i++,j--){
cout<<ans[j]<<'\n';//因为删除时间是倒着的,所以输出也要
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探