CDQ分治学习笔记
CDQ分治学习笔记
k维偏序问题
求满足条件的二元组个数
题意描述
每个元素有 \(k\) 个值,要求满足(以 \(k=2\) 为例 ) \(a_j\le a_i,b_j\le b_i\) 的点对个数 。
分析
这实际上就是我们熟悉的逆序对问题,回忆一下我们是怎么处理的,首先来说,当 \(a,b\) 其中一个的含义是下标的时候可以直接树状数组,因为这个时候第一维已经是有序的了。
然后,对于更一般的情况,可以用归并,这其实就是 CDQ 思想的体现,将待处理的区间不断地分为小区间,直到区间长度满足可以比较为止,这时候统计答案 并且 排序,然后再递归回大区间解决问题。
思考一下,我们实际上是在消除了第一维 \(b_i,b_j\) 的影响的条件下,对 \(a_i,a_j\) 做进一步的比较。
处理到后面的维度的时候再排序可能会改变已经排序完毕的前面的维度,但是很容易地就能够想到,我们只会对于每个元素统计它左边的所有符合条件的元素,所以这不会对答案产生什么影响。
拓展
那么如何解决三维偏序 \(a_j\le a_i,b_j\le b_i,c_j\le c_i\) 呢?仍然应用上面的思想,考虑先消除 \(a\) 这一维度带来的影响,于是我们首先按照 \(a\) 的大小进行 CDQ (或者说是排序);然后再对 \(b\) 的大小进行 CDQ ;然后再对 \(c\) 的大小进行 CDQ ,这个时候我们就可以顺便开始统计答案了。
更加取巧的写法来说的话,第一维实际上可以换成一个 Sort ,最后一维可以直接用树状数组进行统计 ,那么仅仅对于三维偏序来说的话,我们可以只用写一个 CDQ 。
同理,由于在经典的 “逆序对” 问题中,第一维下标是已经有序了的,那么我们可以直接在第二维(或者说最后一维)使用一次 CDQ ,或者直接使用树状数组就把答案统计出来。
例题 三维偏序
Code
#include<bits/stdc++.h>
using namespace std;
template<typename T>inline void re(T &x)
{
T f=1;x=0;
char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c))x=(x<<3)+(x<<1)+(c^48),c=getchar();
x*=f;
}
const int N=2e5+10;
int c[N],n,recn,k;
inline int lowbit(int x){return x&(-x);}
inline void add(int x,int v)
{
for(;x<=k;x+=lowbit(x))c[x]+=v;
}
inline int query(int x)
{
int ans=0;
for(;x;x-=lowbit(x))ans+=c[x];
return ans;
}
int f[N],ans[N];
struct T
{
int a,b,c,idx,num=1;
}q[N],tmp[N],tmpt[N];
inline bool cmp(T x,T y)
{
return x.a!=y.a?x.a<y.a:(x.b!=y.b?x.b<y.b:x.c<y.c);
}
inline void debug()
{
for(register int i=1;i<=n;++i)
printf("%d %d %d %d %d\n",q[i].a,q[i].b,q[i].c,q[i].num,q[i].idx);
}
inline void cdq(int l,int r)
{
if(l==r)return ;
int mid=l+r>>1;
cdq(l,mid),cdq(mid+1,r);
int cnt=l-1,p1=l,p2=mid+1;
while(p1<=mid&&p2<=r)
{
if(q[p1].b<=q[p2].b)
{
tmpt[++cnt]=q[p1];
add(q[p1].c,q[p1].num);
p1++;
}
else
{
tmpt[++cnt]=q[p2];
f[q[p2].idx]+=query(q[p2].c);
p2++;
}
}
while(p1<=mid)
{
tmpt[++cnt]=q[p1];
add(q[p1].c,q[p1].num);
p1++;
}
while(p2<=r)
{
tmpt[++cnt]=q[p2];
f[q[p2].idx]+=query(q[p2].c);
p2++;
}
for(register int i=l;i<=mid;++i)add(q[i].c,-q[i].num);
for(register int i=l;i<=r;++i)q[i]=tmpt[i];
}
inline void pre()
{
cin>>n>>k;
recn=n;
for(register int i=1;i<=n;++i)
re(tmp[i].a),re(tmp[i].b),re(tmp[i].c);
sort(tmp+1,tmp+n+1,cmp);
int tmpn=1;
for(register int i=1;i<=n;++i)
{
if(tmp[i].a==tmp[i+1].a&&tmp[i].b==tmp[i+1].b&&tmp[i].c==tmp[i+1].c)q[tmpn].num++;
else
{
q[tmpn].a=tmp[i].a,q[tmpn].b=tmp[i].b,q[tmpn].c=tmp[i].c;
q[tmpn].idx=tmpn,f[tmpn]=q[tmpn].num-1;
tmpn++;
}
}
n=tmpn-1;
// debug();
}
inline void solve()
{
cdq(1,n);
for(register int i=1;i<=n;++i)ans[f[q[i].idx]]+=q[i].num;//应该访问的是idx,因为上面加的时候也是对下标idx进行操作
for(register int d=0;d<recn;++d)
printf("%d\n",ans[d]);
}
int main()
{
pre();
solve();
return 0;
}
/*
5 2
1 2 2
2 1 1
1 2 1
2 2 1
2 1 1
2 2 0 1 0
*/
这个题写了还是有挺久的,主要是要注意去重的问题,还要注意访问的下标。
总结
对于 K维偏序 的问题来说,我们要做的就是一维一维地消除其对答案统计的影响,直到可以直接统计答案为止。
其中:
第一维可以换成 sort。
最后一维可以放在倒数第二维的时候用树状数组顺便统计。
具体嵌套流程如下 :
void cdq(int l,int r,int x)//x代表维度
{
if(l==r)return ;
int mid=l+r>>1;
if(x==k)
{
cdq(l,mid,x),cdq(mid+1,r,x);
//sort the array and meanwhile calculate the answer
return ;
}
else
{
cdq(l,mid,x),cdq(mid+1,r,x);
//sort the array and do some other work
cdq(l,r,x+1)
}
}
int main()
{
input();
cdq(1,n,1);
output();
}
带有偏序性质的统计答案问题
问题
P5094 Moofest G
分析
题目要求所有 \(\sum\max(v_i,v_j)\times dis(x_i,x_j)\) ,其中两个值实际上都带有偏序的性质。
比如 \(\max\) 就与大小有关,如果一个序列按照 \(v\) 有序,那对于每一个元素我们就只用统计它左边的元素的贡献了;
如果要让一个序列按照坐标大小 \(x\) 有序,那我们就可以在归并 \(x\) 的过程中同时计算答案了。
所以算法流程就很明显了,我们先对于第一维 \(v\) 进行归并(或者直接sort),然后再对第二维进行归并,同时统计答案即可。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename T>inline void re(T &x)
{
x=0;
int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
x*=f;
}
int n;
const int N=5e4+10;
struct cow
{
int v,x;
}a[N],tmp[N];
inline bool cmp(cow a,cow b)
{
return a.v!=b.v?a.v<b.v:a.x<b.x;
}
inline bool cmp1(cow a,cow b)
{
return a.x<b.x;
}
long long ans=0;
int sumx[N];
inline void cdq(int l,int r)
{
if(l==r)return;
int mid=l+r>>1;
cdq(l,mid),cdq(mid+1,r);
int p1=l,p2=mid+1;
int cnt=l-1;sumx[cnt]=0;
for(int i=l;i<=mid;++i)sumx[i]=sumx[i-1]+a[i].x;
while(p1<=mid&&p2<=r)
{
if(a[p1].x>a[p2].x)
{
tmp[++cnt]=a[p2];
ans+=a[p2].v*((p1-l)*a[p2].x-sumx[p1-1] + sumx[mid]-sumx[p1-1]-(mid-p1+1)*a[p2].x);
p2++;
}
else
tmp[++cnt]=a[p1++];
}
while(p1<=mid)tmp[++cnt]=a[p1++];
while(p2<=r)
{
tmp[++cnt]=a[p2];
ans+=a[p2].v*((mid-l+1)*a[p2].x-sumx[mid]);
p2++;
}
for(int i=l;i<=r;++i)a[i]=tmp[i];
}
inline void pre()
{
cin>>n;
for(register int i=1;i<=n;i++)re(a[i].v),re(a[i].x);
sort(a+1,a+n+1,cmp);
}
signed main()
{
pre();
cdq(1,n);
cout<<ans;
return 0;
}
二维数点问题
顾名思义,就是解决平面上的点的包含关系,比如说给定若干个点,然后询问某个点 \((X,Y)\) ,要算出所有的被 \((X,Y)\) 包含的点的个数,也就是 \((x,y),x\le X,y\le Y\) 的个数
解决方法
可以用二维前缀和 ,但是 \(O(n^2)\) 的复杂度有些时候是不可接受的,思考一下,发现这个东西实际上就是一个偏序问题,那么CDQ就可以很好的解决,先分治一下第一维,再分治一下第二维。
但是由于这个问题只有二维,所以实际上也不一定要用CDQ,而且当每个点被赋予特殊的属性之后,只能统计和当前点属性不同的点数,CDQ反而还会更加难写一点。
那么完全可以用树状数组,把所有询问都先离线下来,按照其中一维排序,然后把第二维插到树状数组里面,并同时进行询问。
例题
- P2163 这个是纯板子
- P10550 这个要通过贪心转化一下再写板子
- BJTU2063 starsilk出的神仙题(至少对我而言),先通过贪心来转化问题,然后在单调栈上面二分的同时,去做这样一个二维数点
Code_BJTU2063
#include<bits/stdc++.h>
#define R register
using namespace std;
const int N=5e5+10;
int n,Q;
int a[N],ans[N];
int s[N],top;
struct Query
{
int l,r,idx;
const bool operator<(Query tmp)const{return l<tmp.l;}
}q[N];
inline int lowbit(int x){return x&-x;}
int c[N];
inline void add(int x,int v){for(;x<=n;x+=lowbit(x))c[x]+=v;}
inline int query(int x){int ans=0;for(;x;x-=lowbit(x))ans+=c[x];return ans;}
inline int find(int l,int r,int x)
{
if(l==r)return s[l];
int mid=(l+r+1)>>1;
if(a[s[mid]]>=x)return find(l,mid-1,x);
else return find(mid,r,x);
}
inline void pre()
{
cin>>n;cin>>Q;
for(R int i=1;i<=n;++i)cin>>a[i];
for(R int i=1;i<=Q;++i)cin>>q[i].l>>q[i].r,q[i].idx=i;
sort(q+1,q+Q+1);
int now=n;
for(R int i=Q;i>=1;--i)
{
while(now>=q[i].l)
{
if(top&&a[now]>a[s[1]])
{
int pos=find(1,top,a[now]);
// cout<<"now:"<<now<<' '<<"pos:"<<pos<<'\n';
add(pos,1);
}
while(top&&a[now]<=a[s[top]])top--;
s[++top]=now;
now--;
}
ans[q[i].idx]=query(q[i].r);
}
}
inline void solve()
{
for(int i=1;i<=Q;++i)cout<<ans[i]<<'\n';
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
pre();
solve();
return 0;
}
CDQ优化DP转移
这里是指把 DP 的下标当做其中的一维,但是我还没学到那个程度,所以暂时挖个坑,以后来填。
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/18440789