CDQ分治 关于求点对问题(偏序问题)

写在前面

写这玩意纯就是怕自己忘了,顺带理一下思路。

CDQ 确实是个很神奇的东西啊。

把板子调出来的时候,差点没忍住从座位跳起来,然后来一句

无人扶我青云志,我自踏雪至山巅。

糖丸了。

咳咳,先从板题讲起,其实也就只会版题了

P3810 【模板】三维偏序(陌上花开)

题意

所谓偏序,就是指配备了部分排序关系的集合,也就是说按照我们自己定义的"顺序"排序。

题目要求考虑 \(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\) 小的个数要满足另两个要求:

\[\begin{cases} 1 . & a_j < a_i \\ 2 . & b_j < b_i \end{cases} \]

第一点很好满足,直接看 \(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 关于偏序问题的运用很灵活也很巧妙,总归就是很牛逼的同时维护几个限制然后得出答案。

也得加油啊以后!!!

posted @ 2025-12-26 19:01  Lywh  阅读(0)  评论(0)    收藏  举报