(简记)CDQ 分治

与其说是一类算法,不如说是一类 trick。CDQ 分治是一种考虑子问题及 \([l,r]\) 中划分出的两个子区间 \([l,mid]\)\([mid+1,r]\) 分别产生贡献、组合产生贡献的思维方式

主要有几个应用:

  1. 解决一类三维偏序问题,用外层排序解决第一维,用分治顺序(双指针)解决第二维,用数据结构解决第三维,内层二次排序使用归并排序,总时间复杂度 \(O(n\log^2 n)\)。采用后序遍历的顺序,利用左右子问题归并给我们排好序后再做。

  2. 解决一类 DP 优化问题,具体来说分治时我们采用中序遍历的顺序扫一遍,然后计算前面对后面(左半区间对右半区间)的贡献,可以优化的 DP 是 1d-1d 的(状态 \(1\) 维,转移 \(1\) 维),P7842 「C.E.L.U-03」探险者笔记 III这个题里面还套了个子集查询的数据结构。

  • 具体来说,我们需要注意几个细节:包括但不限于需要用 sort 给开始的左右区间分别排序而不能归并,且为了保证右区间分治正确性,跨区间统计完成后需要还原右区间的顺序。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=3e5+5,lim=9;
int n,m,w,b[N],V[N],need[N];
int hard[N],lg2[N*2],S[N];
inline int lowbit(int x){return x&-x;}
bool cmp(int x,int y){return need[S[x]]>need[S[y]];}
int id[N],ans,f[N];
struct SUB{
	struct DS{
		int mx[1<<lim];
		void modify(int p,int x){
			for(int i=p;i<(1<<lim);i=(i+1)|p)
				mx[i]=max(mx[i],x);
		}
		void clr(int p){
			for(int i=p;i<(1<<lim);i=(i+1)|p)
				mx[i]=0;
		}
	}T[1<<lim];
	void modify(int S,int x){
		int P=(S>>lim),Q=S^(P<<lim);
		T[P].modify(Q,x);
	}
	void clr(int S){
		int P=(S>>lim),Q=S^(P<<lim);
		T[P].clr(Q);
	}
	int query(int S){
		int P=(S>>lim),Q=S^(P<<lim);
		int res=T[0].mx[Q];
		for(int i=P;i;i=(i-1)&P)
			res=max(res,T[i].mx[Q]);
		return res;
	}
}T;
void CDQ(int l,int r){
	if(l==r){f[id[l]]=max(f[id[l]],V[id[l]]);ans=max(ans,f[id[l]]);return ;}
	int mid=(l+r)>>1;
	CDQ(l,mid);
	sort(id+l,id+1+mid,cmp);
	sort(id+mid+1,id+1+r,cmp);
	int pos=l;
	for(int i=mid+1;i<=r;i++){
		while(pos<=mid&need[S[id[pos]]]+w>=need[S[id[i]]])
			T.modify(S[id[pos]],f[id[pos]]),pos++;
		f[id[i]]=max(f[id[i]],T.query(S[id[i]])+V[id[i]]),
		ans=max(ans,f[id[i]]);
	}
	for(int i=l;i<pos;i++)
		T.clr(S[id[i]]);
	for(int i=mid+1;i<=r;i++)
		id[i]=i;
	CDQ(mid+1,r);
}
int main(){
	//freopen("xuehua.in","r",stdin);
	//freopen("xuehua.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>w;
	for(int i=0;i<n;i++)cin>>b[i];
	for(int i=2;i<(1<<n);i++)
		lg2[i]=lg2[i>>1]+1;
	for(int S=1;S<(1<<n);S++){
		int nS=S;
		for(int i=lg2[lowbit(nS)];nS;nS-=lowbit(nS),i=lg2[lowbit(nS)])
			need[S]+=b[i];
	}
	for(int i=1;i<=m;i++){
		id[i]=i;
		int sum;cin>>V[i]>>sum;
		for(int j=1;j<=sum;j++){
			int v;cin>>v;
			S[i]|=(1<<(v-1));
		}
	}
	CDQ(1,m);
	cout<<ans;
	return 0;
}
  1. 离线按时间处理问题。

P4690 [Ynoi Easy Round 2016] 镜中的昆虫为例,本题是区间推平区间数颜色问题,如果暴力上 ODT 由于数据不随机是 \(O(n^2)\) 的。考虑优化,根据(笔记)ODT,我们仍可以利用单独区间推平操作的特性在 ODT 上做,但不暴力查询。

数颜色联想到类似 HH 的项链的套路,对 \(i\) 维护 \(pre_i\) 表示前面第一个同色点,没有就为 \(0\)。那么对于区间 \([l,r]\) 的询问就是一个二维数点(\(x\in[l,r],pre_x\in[0,l-1]\))。考虑到还有一个时间维,这就是一个三维偏序问题。对于 \(x\in[l,r]\) 那一维我们用数据结构维护,\(pre_x\) 分治中解决,外面已经按照时间排好序。

结论:若干次区间推平操作改变的 \(pre_x\) 个数是 \(O(n+m)\) 级别的。

证明是简单的,每次推平最多分裂两个区间,而不同的区间(ODT 上的三元组 \((l,r,k)\))最多只会出现一次然后被删除一次,且对于连续段加删每次只需要管其 \(pre_l\) 的更新和其某个同颜色后继的更新(这个也可以用颜色个 set 维护),即常数次,改变的 \(pre_x\) 就是 \(O(n+m)\) 级别的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=2e5+5;
int n,qn,a[N],lsh[N],cnt,qcnt,pre[N];
LL ans[N];
struct Q{int op,l,r,lim,id;}q[N<<2],dt[N<<2];
struct Rcd{int op,l,r,x;}rcd[N];
struct BIT{
	int av[N];
	inline int lowbit(int x){return x&-x;}
	void add(int p,int x){for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;}
	int que(int p){
		int res=0;
		for(int i=p;i;i-=lowbit(i))
			res+=av[i];
		return res;
	}
}T;
struct Node{
	int l,r,v;
	bool operator <(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s,S[N];
IT split(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r,v=it->v;
	S[v].erase(*it);s.erase(it);
	S[v].insert(Node{l,pos-1,v});
	s.insert(Node{l,pos-1,v});
	S[v].insert(Node{pos,r,v});
	return s.insert(Node{pos,r,v}).first;
}
inline void upd(int &pos,int &x){
	if(pre[pos]!=x){
		q[++qcnt]=Q{1,-pos,-pos,pre[pos],0};
		q[++qcnt]=Q{1,pos,pos,pre[pos]=x,0};
	}
}
void assign(int l,int r,int x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++){
		int L=it->l,R=it->r,v=it->v;
		IT p=S[v].find(*it);
		assert(p!=S[v].end());
		if(it==itl){
			int V=0;
			IT newp=S[x].lower_bound(Node{l,0,0});
			if(newp!=S[x].begin())V=prev(newp)->r;
			upd(L,V);
		}
		else {
			int V=L-1;
			upd(L,V);
		}
		if(next(p)!=S[v].end()&&next(p)->l>r){
			int dL=next(p)->l;
			int V=0;
			if(p!=S[v].begin())
				V=prev(p)->r;
			upd(dL,V);
		}
		S[v].erase(p);
	}
	IT newp=S[x].lower_bound(Node{r+1,0,0});
	if(newp!=S[x].end()){
		int dL=newp->l,V=r;
		upd(dL,V);
	}
	s.erase(itl,itr);
	S[x].insert(Node{l,r,x});
	s.insert(Node{l,r,x});
}
void solve(int l,int r){
	if(l==r)return ;
	int mid=(l+r)>>1;
	solve(l,mid);solve(mid+1,r);
	int pos=l,now=l;
	for(int i=mid+1;i<=r;i++){
		while(pos<=mid&&q[pos].lim<=q[i].lim){
			if(q[pos].op==1){
				if(q[pos].l<0)
					T.add(-q[pos].l,-1);
				else T.add(q[pos].l,1);
			}
			dt[now++]=q[pos++];
		}
		if(q[i].op==2)ans[q[i].id]+=T.que(q[i].r)-T.que(q[i].l-1);
		dt[now++]=q[i];
	}
	for(int i=l;i<pos;i++)
		if(q[i].op==1){
			if(q[i].l<0)
				T.add(-q[i].l,1);
			else T.add(q[i].l,-1);
		}
	while(pos<=mid)dt[now++]=q[pos++];
	for(int i=l;i<=r;i++)q[i]=dt[i];
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>qn;
	for(int i=1;i<=n;i++)
		cin>>a[i],lsh[++cnt]=a[i];
	for(int i=1;i<=qn;i++){
		int op,l,r,x=0;cin>>op>>l>>r;
		if(op==1)cin>>x,lsh[++cnt]=x;
		rcd[i]=Rcd{op,l,r,x};
	}
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh,
		s.insert(Node{i,i,a[i]}),S[a[i]].insert(Node{i,i,a[i]});
	for(int i=1;i<=n;i++){
		IT p=S[a[i]].find(Node{i,i,a[i]});
		if(p==S[a[i]].begin())
			q[++qcnt]=Q{1,i,i,pre[i]=0,0};
		else q[++qcnt]=Q{1,i,i,pre[i]=prev(p)->r,0};
	}
	int Qcnt=0;
	for(int i=1;i<=qn;i++){
		int op=rcd[i].op,l=rcd[i].l,r=rcd[i].r;
		if(op==1){
			int x=lower_bound(lsh+1,lsh+1+cnt,rcd[i].x)-lsh;
			assign(l,r,x);
		}
		else q[++qcnt]=Q{2,l,r,l-1,++Qcnt};
	}
	solve(1,qcnt);
	for(int i=1;i<=Qcnt;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

想学习三维偏序不妨打开下面的折叠栏看看我以前拉的史。

留档先前讲解

主要求解问题:

  1. 偏序问题,有 \(i\)\(j\) 分别不相等,常常要求其两项满足一定条件(如P3810 陌上花开)。

  2. 离线查询二维区间问题(如P3755 老C的任务)。

  3. 连续子段和等限制性问题(如P2717 寒假作业)。

通常需要一定变化再加以处理得出答案。

以 P3810 为例,以三维偏序为代表的 CDQ 分治问题主要分为以下几个部分:

注意:原题需要去重并计数处理。

  1. 按照优先度排序,去掉一维优先

  2. 分治在左右区间满足如下条件

  • 区间一 \(a_{le}\)\(a_{mid}\) 与区间二 \(a_{mid+1}\)\(a_{ri}\) ,在初始排序时保证任意 \(i<j\)\(i\) 在区间一,\(j\) 在区间二有 \(a_i\le a_j\)

  • 在分治过程中保证有 \(i<j\) ,条件同上,有 \(b_i\le b_j\)

  • 最后,在树状数组中存储优先级保证读取到的 \(c_i\le c_j\)

  1. 树状数组用完清零

  2. 区间 \([l,r]\) 处理完毕后按照第二关键字进行排序(在这里是 \(b\)),以保证分治正确性

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,k;
struct tre{
	int fe[N];
	inline int lowbit(int x){return x&-x;}
	void add(int x,int v){for(x;x<=k;x+=lowbit(x))fe[x]+=v;}
	int sum(int x){
		int ans=0;
		for(x;x>0;x-=lowbit(x))ans+=fe[x];
		return ans;
	}
}T;
int res[N];
struct op{
	int x,y,z,cnt,ans;
}a[N],b[N];
bool cmp(op x,op y){
	if(x.x!=y.x)return x.x<y.x;//step 1
	if(x.y!=y.y)return x.y<y.y;
	return x.z<y.z;
}
void cdq(int l,int r){
	if(l>=r)return ;
	int tem=0;
	int mid=(l+r)>>1;
	cdq(l,mid);cdq(mid+1,r);
	int i=l,j=mid+1;
	while(i<=mid&&j<=r){
		if(a[i].y<=a[j].y){//step 2
			T.add(a[i].z,a[i].cnt);
			b[++tem]=a[i++];
		}
		else {
			a[j].ans+=T.sum(a[j].z);
			b[++tem]=a[j++];
		}
	}
	while(i<=mid)T.add(a[i].z,a[i].cnt),b[++tem]=a[i++];
	while(j<=r)a[j].ans+=T.sum(a[j].z),b[++tem]=a[j++];
	for(int i=l;i<=mid;i++)T.add(a[i].z,-a[i].cnt);//step 3
	for(int i=l;i<=r;i++)a[i]=b[i-l+1];//step 4
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y>>a[i].z;
	}
	sort(a+1,a+1+n,cmp);
	int tc=0;
	for(int i=1;i<=n;i++){
		if(a[i].x==a[i-1].x&&a[i].y==a[i-1].y&&a[i].z==a[i-1].z)a[tc].cnt++;
		else a[++tc]=a[i],a[tc].cnt++;
	}
	cdq(1,tc);
	for(int i=1;i<=tc;i++)res[a[i].ans+a[i].cnt-1]+=a[i].cnt;
	for(int i=0;i<n;i++)cout<<res[i]<<"\n";
	return 0;
}
posted @ 2025-04-24 14:49  TBSF_0207  阅读(26)  评论(0)    收藏  举报