简介

前置芝士:归并排序。

\(cdq\) 分治是个离线算法,可以解决三维偏序或者优化 \(dp\)。

陌上花开

维护三维偏序有个口诀:一维排序,二维归并,三维数据结构。

考虑第一维直接排序解决掉,然后还剩两维。

我们考虑第二维用归并排序解决掉。然后假设当前区间 \([l,r]\),区间中点 \(mid\)。那么我们通过递归可以解决 \([l,mid],[mid+1,r]\) 内部的贡献,现在要考虑解决两个点在两个区间中的贡献。

我们在递归时已经按照第二维排好序了。但是有个问题,第一维的限制呢?这样不是把第一维的限制忽略掉了吗?

再想一想我们求的东西,两个端点分别在两个区间内,而这时两个区间内的第一维的相对大小仍然是满足要求的,因为这时他们还没有完成第二维的排序。

然后如果我们发现这时第二维满足了要求,我们就把第三维扔进树状数组或者权值线段树内(推荐树状数组)。否则(重点),此时的 \([l,i-1]\) 是一个极大的 \(\forall u\in[l,i-1],u\) 满足前两维的偏序关系的区间。这时我们就要统计贡献了。

然后就是,我们把没跑完的部分跑完,但是这里的循环顺序非常重要,不能更改,具体原因写在代码注释里了。

最后记得堆原数组去个重,不然会出问题。

代码:

#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;
}

园丁的烦恼

这里说一下 \(cdq\) 分治怎么写。考虑一个事情,求一个矩阵中的点的数量,我们可以转化为二维前缀和计算,就是计算一下每个点左下方有多少个点。我们假设当前点是 \(i\),左下方的点为 \(j\),则满足 \(x_j\le x_i,y_j\le y_i\)。

就是比方说求的东西是 \(A\),于是需要求出 \(A\) 这个矩形的四个顶点左下方有多少个点,然后做一个二维前缀和就可以求答案了。

然后再考虑,把初始给定点的 \(z\) 设成 \(0\),自己查询的点的 \(z\) 设成 \(1\)。于是又有 \(z_j<z_i\)。发现是一个裸的三维偏序。

但是,可以发现 \(z\) 只有 \(0,1\) 两种取值,于是不需要树状数组,拿一个变量记录即可。

于是看一下代码:

#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

板子题。考虑开桶维护元素个数。然后第一维就是操作顺序,第二维是操作时间,第三维是元素值。

可以发现第一维不用动,我们对第二维归并排序,相当于考虑 \([l,mid]\) 中的修改对 \([mid+1,r]\) 的询问的影响,其他东西我们递归解决。

所以就是在发现当前 \(i\) 位置的时间更小时,如果是个修改我们对桶进行修改;如果 \(j\) 位置是个询问,直接查询桶。

然后就是,我们为了开桶,需要把元素值离散化一下,于是就做完了,可以看一下代码:

#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;
}

动态逆序对

我们先设每个位置 \(i\) 的数被删除的时间为 \(t_i\),如果这个数没有被删除,那么就随便找一个大数(注意不要重复)。

但是这样会要求我们用树状数组维护后缀和,这个东西比前缀和难维护,所以我们把删除时间翻转一下(即最早删除的 \(t\) 数组最大)。

然后我们考虑把逆序对的贡献记录到删除时间更晚的上面(但是实际的 \(t\) 小,因为反着记录的)。

所以现在如果我们对于 \((i,j)\) 能把贡献记录到 \(j\) 上,那么有两种可能:

  • \(i<j,t_i<t_j,val_i>val_j\)。

  • \(i>j,t_i<t_j,val_i<val_j\)。

然后就做两遍统计即可,代码:

#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;
}

cdq分治 基础篇的更多相关文章

  1. [luogu3157][bzoj3295][CQOI2011]动态逆序对【cdq分治+树状数组】

    题目描述 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序 ...

  2. 一篇自己都看不懂的CDQ分治&整体二分学习笔记

    作为一个永不咕咕咕的博主,我来更笔记辣qaq CDQ分治 CDQ分治的思想还是比较简单的.它的基本流程是: \(1.\)将所有修改操作和查询操作按照时间顺序并在一起,形成一段序列.显然,会影响查询操作 ...

  3. 【学术篇】bzoj3262 陌上花开. cdq分治入门

    花儿们已经很累了-- 无论是花形.颜色.还是气味, 都不是为了给人们摆出来欣赏的, 更不是为了当做出题的素材的, 她们并不想自己这些属性被没有生命的数字量化, 并不想和其它的花攀比, 并无意分出个三六 ...

  4. 【教程】简易CDQ分治教程&学习笔记

    前言 辣鸡蒟蒻__stdcall终于会CDQ分治啦!       CDQ分治是我们处理各类问题的重要武器.它的优势在于可以顶替复杂的高级数据结构,而且常数比较小:缺点在于必须离线操作. CDQ分治的基 ...

  5. bzoj3295: [Cqoi2011]动态逆序对(cdq分治)

    #include <iostream> #include <cstdio> #include <cstring> #include <cmath> #i ...

  6. [学习笔记] CDQ分治 从感性理解到彻底晕菜

    最近学了一种叫做CDQ分治的东西...用于离线处理一系列操作与查询似乎跑得很快233 CDQ的名称似乎源于金牌选手陈丹琦 概述: 对于一坨操作和询问,分成两半,单独处理左半边和处理左半边对于右半边的影 ...

  7. [用CDQ分治解决区间加&区间求和]【习作】

    [前言] 作为一个什么数据结构都不会只会CDQ分治和分块的蒟蒻,面对区间加&区间求和这么难的问题,怎么可能会写线段树呢 于是,用CDQ分治解决区间加&区间求和这篇习作应运而生 [Par ...

  8. [偏序关系与CDQ分治]【学习笔记】

    组合数学真是太棒了 $CDQ$真是太棒了(雾 参考资料: 1.<组合数学> 2.论文 课件 很容易查到 3.sro __stdcall 偏序关系 关系: 集合$X$上的关系是$X$与$X$ ...

  9. 【BZOJ4237】稻草人(CDQ分治,单调栈)

    [BZOJ4237]稻草人(CDQ分治,单调栈) 题面 BZOJ 题解 \(CDQ\)分治好题呀 假设固定一个左下角的点 那么,我们可以找到的右下角长什么样子??? 发现什么? 在右侧是一个单调递减的 ...

  10. $CDQ$分治总结

    A.\(CDQ\) 分治 特别基础的教程略. \(CDQ\)分治的优缺点: ( 1 )优点:代码量少,常数极小,可以降低处理维数. ( 2 )缺点:必须离线处理. \(CDQ\)分治与其他分治最本质的 ...

随机推荐

  1. Java代码忽略https证书:解决No subject alternative names present问题 HttpURLConnection https请求

    Java代码忽略https证书:解决No subject alternative names present问题 import org.slf4j.Logger; import org.slf4j.L ...

  2. mysql8新版本安装注意事项及解决本地系统账户Strart the server和SQLyog客户端连接报 plugin caching_sha2_password could not be loaded

    mysql 安装包一路next安装, 到Apply Configuration,卡住在Start the server,原因是mysql8新版本的登录身份不对,需要进入服务,找到mysql80,然后属 ...

  3. json字符串忽略null,忽略字段,首字母大写等gson,jackson,fastJson实现demo,T data JSON.parseObject json转换

    json字符串忽略null,忽略字段,首字母大写等gson,jackson,fastJson实现demo package com.example.core.mydemo.json.vo; import ...

  4. Java8 多线程及并行计算demo

    Java8 多线程及并行计算demo #接口 public interface RemoteLoader { String load(); default void delay() { try { T ...

  5. 用 Visual C++ 2022 和 CMake 编译 CUnit 静态库

    准备工作 源代码获取 CUnit 是知名的 C 语言单元测框架,其源代码最初发布在 sourceforge 上,网址为:https://sourceforge.net/projects/cunit/ ...

  6. 淘宝二面:千万级数据中如何用Redis维护热点数据"?

    MySQL里有千万条数据,但是Redis中只存10万的数据,如何保证redis中的数据都是热点数据? 我是小宋, 一个只熬夜但不秃头的Java程序员.关注我,带你轻松过面试.提升简历亮点(14个dem ...

  7. 获取ImageView的触摸点所对应的UIImage的坐标

    获取ImageView的触摸点所对应的UIImage的坐标 功能描述 实现前分析 注意事项 代码 求打赏 功能描述 在imageview上触摸图片,求对应UIImage的触摸点. 实现前分析 从ima ...

  8. scarpy基础

    1. 创建项目 scrapy startproject 项目名称 2. 进入项目 cd 项目名称 3. 创建爬虫 scrapy genspider 名字 域名 4. 可能需要start_urls,修改 ...

  9. 【论文阅读】RAL2020: UFOMap An Efficient Probabilistic 3D Mapping Framework That Embraces the Unknown

    Last edited time: March 31, 2023 1:30 PM Reference and prenotes Paper link:https://ieeexplore.ieee.o ...

  10. 运行前端React框架出现node Error: bind EADDRINUSE null的解决方法

    运行前端React代码时,出现这样的错误: node Error: bind EADDRINUSE null 后来发现端口号冲突,换个端口号后问题就可以解决了.