CDQ分治 关于求点对问题(偏序问题)
写在前面
写这玩意纯就是怕自己忘了,顺带理一下思路。
CDQ 确实是个很神奇的东西啊。
把板子调出来的时候,差点没忍住从座位跳起来,然后来一句。
无人扶我青云志,我自踏雪至山巅。
糖丸了。
咳咳,先从板题讲起,其实也就只会版题了。
题意
所谓偏序,就是指配备了部分排序关系的集合,也就是说按照我们自己定义的"顺序"排序。
题目要求考虑 \(a_i\) , \(b_i\) , \(c_i\) 三个限制,满足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $,然后问对于每个 \(i\) 满足这样的 \(j\) 有多少个。
感性理解。
解
要满足三个限制,那就一个一个来,对 \(a\) 先排序, 其实可以发现:分治算法的实现,相当于是在对已满足 \(a\) 有序的序列上做操作 (发现不出来也没关系)。
e.g:
原序:
id1: 3 2 3
id2: 1 2 1
id3: 2 3 2
对 \(a\) 排序后:
id2: 1 2 1
id3: 2 3 2
id1: 3 2 3
形象多了,
然后不断取 \(mid\),
a b c
id2: 1 2 1 l
- - - - - - - mid
id3: 2 3 2
id1: 3 2 3 r
对于 \(r\) 边的每个 \(i\) 来说,要想求到比 \(c_i\) 小的个数要满足另两个要求:
第一点很好满足,直接看 \(l\) 里有多少 \(c_j < c_i\) 就行了。因为 \(l\) 里的所有 \(a_j\) 一定是小于 \(r\) 里的 \(a_i\) 的。
那这个时候就有问题了:"那我 \(r\) 这边也有 \(c_j < c_i\)" 的啊,这样不就忽略了吗。
因为是递归,\(r\) 边的贡献递归到 \(id3\) 、 \(id1\) 时才会计算,那时 \(mid\) 就在 \(id3\) 、 \(id2\) 之间了,也就会回到刚刚说的情况。
捋一捋目的,所以我们是要在 \(l\) 中找小于 \(c_i\) 的数的个数。
可以用权值树状数组,把 \(l\) 边的 \(c_j\) 都放进树状数组中,然后对于 \(r\) 这边的每个 \(c_i\) 寻找数组中比 \(c_i\) 小的个数,统计答案。
那第二点,怎样才能做到在满足 \(a_j < a_i\) 的情况下查询 \(c_j < c_i\) 时保证 \(b_j < b_i\) 呢?
其实也不难,我要在 \(l\) 这边的树状数组中查询比 \(c_i\) 小的个数,那我只要保证我要查找的树状数组里所有 \(id\) 的 \(b\) 都小于当前的 \(b\) 不就行了?
如何实现呢,关于树状数组,我按照 \(b\) 从小到大的顺序做查询加入操作(CDQ 递归时分别以关键字 \(b\) 快排 \(l\) ~ \(mid\) 和 \(mid+1\) ~ \(r\) ,保证 \(b\) 在两边的顺序排列)。
如果当前 \(b\) 在 \(l\) 里,我就把它的 \(c\) 加进树状数组里,如果当前的 \(b\) 在 \(r\) 里,我就查询数组中比它所对应的 \(c\) 小的个数,累加答案。
这样就能保证,要查询的 \(b\) 一定大于被搜索的 \(b\) (也就是树状数组里的 \(b\) )。
关键部分 CDQ 的代码:
void CDQ(int l,int r){
if(l>=r) return ;
int mid=(l+r)/2;
CDQ(l,mid);
CDQ(mid+1,r);
int l1=l,r1=mid,l2=mid+1,r2=r;
sort(a+l1,a+r1+1,cmp2);
sort(a+l2,a+r2+1,cmp2);
while(l1<=r1&&l2<=r2){
if(a[l1].b<=a[l2].b){
gai(a[l1].c,a[l1].cnt);
l1++;
}
if(a[l1].b>a[l2].b){
f[a[l2].id]+=cha(a[l2].c);
l2++;
}
}
while(l1<=r1){
gai(a[l1].c,a[l1].cnt);
l1++;
}
while(l2<=r2){
f[a[l2].id]+=cha(a[l2].c);
l2++;
}
for(int i=l;i<=mid;i++)
gai(a[i].c,-a[i].cnt);
}
这个 \(cnt\) 是什么呢,\(cnt\) 是相同序列 \(i\) 出现的次数,为什么要统计出现次数呢?我不能每个单独加一吗?有什么不同呢。
举个例子:
id1: 1 1 1
id2: 3 2 1
id3: 3 2 1
如果对于 \(id2\) 和 \(id3\) 分别加一的话,那么
$ f(2) = 1 \(,\) f(3) = 2 $
然而实际上应该是:
$ f(2) = f(3) = 2 $
因为对于一个 \(id\) ,它的贡献只可能由前面的推导过来,这样相同的就考虑不全,倒也不是不能加一,主要是不能有重复的。
所以记一下出现次数,再去一下重,最后 \(f()\) 的答案每个都要加上 \(a[i].cnt - 1\) 。
然后就没了...... (其实细节还是很多的,不然鬼知道我为什么调了那么久)。
还是把代码贴上吧,万一以后回来找不着了,顺便通过注释反应留恋一下调代码的艰辛。qwq
#include<bits/stdc++.h>
using namespace std;
#define N 200500
int n,k,vis,cn;
int ans[N],f[N],c[N];
struct l{
int a,b,c,id,cnt=1;
bool operator==(const l& other) const {
return a==other.a&&b==other.b&&c==other.c;
}
}a[N*4],tmp;
int lowbit(int n){
return n&(-n);
}
void gai(int i,int kk){
while(i<=k){
c[i]+=kk;
i+=lowbit(i);
}
}
int cha(int k){
int ans=0;
while(k!=0){
ans+=c[k];
k-=lowbit(k);
}
return ans;
}
int cmp(l x,l y){
if(x.a==y.a)
if(x.b==y.b) return x.c<y.c;
else return x.b<y.b;
return x.a<y.a;
}
int cmp2(l x,l y){
return x.b<y.b;
}
void CDQ(int l,int r){
if(l>=r) return ;
int mid=(l+r)/2;
CDQ(l,mid);
CDQ(mid+1,r);
// cout<<l<<" "<<r<<endl;
int l1=l,r1=mid,l2=mid+1,r2=r;
sort(a+l1,a+r1+1,cmp2);
// for(int i=l1;i<=r1;i++){
// cout<<a[i].b<<" ";
// }
// cout<<endl;
sort(a+l2,a+r2+1,cmp2);
// for(int i=l2;i<=r2;i++){
// cout<<i<<" "<<a[i].b<<" ";
// }
// cout<<endl;
while(l1<=r1&&l2<=r2){
if(a[l1].b<=a[l2].b){
gai(a[l1].c,a[l1].cnt);
// cout<<" + "<<l1<<" "<<a[l1].cnt<<" ";
l1++;
}
if(a[l1].b>a[l2].b){
f[a[l2].id]+=cha(a[l2].c);
// cout<<" c "<<l2<<" g ";
l2++;
}
}
while(l1<=r1){
gai(a[l1].c,a[l1].cnt);
// cout<<" + "<<l1<<" "<<a[l1].cnt<<" ";
l1++;
}
while(l2<=r2){
f[a[l2].id]+=cha(a[l2].c);
// cout<<" c "<<l1<<" ";
l2++;
}
for(int i=l;i<=mid;i++){
gai(a[i].c,-a[i].cnt);
}
// cout<<endl;
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i].a>>a[i].b>>a[i].c;
a[i].id=i;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++){
if(a[i]==tmp){
a[vis].cnt++;
a[i].a=0;a[i].b=0;a[i].c=0;
continue ;
}
else{
vis=i;
tmp=a[i];
}
}
for(int i=1;i<=n;i++){
if(a[i].a) a[++cn]=a[i];
}
int nn=cn;
CDQ(1,nn);
for(int i=1;i<=nn;i++){
f[a[i].id]+=a[i].cnt-1;
}
for(int i=1;i<=nn;i++){
int cn=a[i].cnt;
while(cn--)
ans[f[a[i].id]]++;
}
for(int i=0;i<n;i++){
cout<<ans[i]<<endl;
}
// for(int i=1;i<=n;i++){
// while(a[i].cnt--){
// cout<<a[i].a<<" "<<a[i].b<<" "<<a[i].c<<" ";//<<a[i].id<<" ";
// cout<<f[a[i].id]<<endl;
// }
// }
return 0;
}
综上得 总结
CDQ 关于偏序问题的运用很灵活也很巧妙,总归就是很牛逼的同时维护几个限制然后得出答案。
也得加油啊以后!!!

浙公网安备 33010602011771号