cdq分治 基础篇
简介
前置芝士:归并排序。
\(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分治 基础篇的更多相关文章
- [luogu3157][bzoj3295][CQOI2011]动态逆序对【cdq分治+树状数组】
题目描述 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序 ...
- 一篇自己都看不懂的CDQ分治&整体二分学习笔记
作为一个永不咕咕咕的博主,我来更笔记辣qaq CDQ分治 CDQ分治的思想还是比较简单的.它的基本流程是: \(1.\)将所有修改操作和查询操作按照时间顺序并在一起,形成一段序列.显然,会影响查询操作 ...
- 【学术篇】bzoj3262 陌上花开. cdq分治入门
花儿们已经很累了-- 无论是花形.颜色.还是气味, 都不是为了给人们摆出来欣赏的, 更不是为了当做出题的素材的, 她们并不想自己这些属性被没有生命的数字量化, 并不想和其它的花攀比, 并无意分出个三六 ...
- 【教程】简易CDQ分治教程&学习笔记
前言 辣鸡蒟蒻__stdcall终于会CDQ分治啦! CDQ分治是我们处理各类问题的重要武器.它的优势在于可以顶替复杂的高级数据结构,而且常数比较小:缺点在于必须离线操作. CDQ分治的基 ...
- bzoj3295: [Cqoi2011]动态逆序对(cdq分治)
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #i ...
- [学习笔记] CDQ分治 从感性理解到彻底晕菜
最近学了一种叫做CDQ分治的东西...用于离线处理一系列操作与查询似乎跑得很快233 CDQ的名称似乎源于金牌选手陈丹琦 概述: 对于一坨操作和询问,分成两半,单独处理左半边和处理左半边对于右半边的影 ...
- [用CDQ分治解决区间加&区间求和]【习作】
[前言] 作为一个什么数据结构都不会只会CDQ分治和分块的蒟蒻,面对区间加&区间求和这么难的问题,怎么可能会写线段树呢 于是,用CDQ分治解决区间加&区间求和这篇习作应运而生 [Par ...
- [偏序关系与CDQ分治]【学习笔记】
组合数学真是太棒了 $CDQ$真是太棒了(雾 参考资料: 1.<组合数学> 2.论文 课件 很容易查到 3.sro __stdcall 偏序关系 关系: 集合$X$上的关系是$X$与$X$ ...
- 【BZOJ4237】稻草人(CDQ分治,单调栈)
[BZOJ4237]稻草人(CDQ分治,单调栈) 题面 BZOJ 题解 \(CDQ\)分治好题呀 假设固定一个左下角的点 那么,我们可以找到的右下角长什么样子??? 发现什么? 在右侧是一个单调递减的 ...
- $CDQ$分治总结
A.\(CDQ\) 分治 特别基础的教程略. \(CDQ\)分治的优缺点: ( 1 )优点:代码量少,常数极小,可以降低处理维数. ( 2 )缺点:必须离线处理. \(CDQ\)分治与其他分治最本质的 ...
随机推荐
- Timing!!!
End or Beginning "毕业",一个令人无限憧憬的具象化名词.适逢高考结束,又有一批人将奔赴更远的地方,离开他们生活了十八年的城市,在这之中亦然有着曾经的我们.但大家把 ...
- 07-Python异常处理
什么是异常? Python无法正常处理程序时就会发生一个异常,这时Python就会抛出一个对象,表示这是一个错误. 必须处理异常,否则程序可能会停止运行,或者出现异常现象. 如:4/0就会抛出异常,因 ...
- C# pythonnet(1)_传感器数据清洗算法
Python代码如下 import pandas as pd # 读取数据 data = pd.read_csv('data_row.csv') # 检查异常值 def detect_outliers ...
- Linux gpio子系统:gpio_direction_output 与 gpio_set_value的区别
Linux gpio子系统:gpio_direction_output 与 gpio_set_value的区别 背景 最近改驱动程序,看到驱动代码中既有gpio_direction_output也有g ...
- Linux的访问权限详解
题目 解读访问权限 rw-r--r--分别代表什么东西 r:代表可读 w:可写 e:可执行 方便起见进行拆分 rw- 代表文件所属用户的权限 r-- 代表同组用户的权限 r-- 代表其他用户的权限 同 ...
- nginx面试题及答案
什么是nginx? Nginx是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器 Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代 ...
- 【SQL】晨光咖啡馆,过滤聚合的微妙碰撞
这天,小悦懒洋洋地步入办公楼下的咖啡馆,意外地与一位男子不期而遇.他显然因前一晚的辛勤工作而略显疲惫,却仍选择早到此地,寻找一丝宁静与放松.他叫逸尘,身姿挺拔,衣着简约而不失格调,晨光下更显英俊不凡, ...
- Windows 10 LTSC中个人版OneDrive失效的问题
该问题是由于LTSC注册表无onedriver的id{A52BBA46-E9E1-435f-B3D9-28DAA648C0F6}定义导致,解决方案是新建一个reg_onedrive.reg文件,并编辑 ...
- tp 模型hasOne、hasMany、belongsTo详解
首先,这3个的大致中文意思:hasOne:有一个,加上主谓语应该是 ,A 有一个 BhasMany:有很多,A 有很多 BbelongsTo:属于, A 属于 B这里我们准备3张表来理解他们的关系:u ...
- [oeasy]python0021_python虚拟机的位置_可执行文件_转化为字节形态
程序本质 回忆上次内容 \n 就是换行 他对应着 ascii 字符的代码是(10)10进制 他的英文是 LF,意思是Line Feed 我们可以在<安徒 ...