CDQ 分治

算法原理

左半边贡献右半边。

注意普通偏序问题中可以把左右都递归再算贡献,但是 cdq 优化 dp 的问题中则需要严格按照递归左半边+算贡献+递归右半边的流程走。

板子
//不写边界条件 
//偏序条件没有明确
//不移动指针 
//排序数组初始指针与指针移动错误 
//树状数组清空范围错误 
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5;
int n,k;
int tree[N<<1],ans[N];
struct QUERY{
	int a,b,c,res,cnt;
}q[N],tmp[N];

bool cmp(QUERY &x,QUERY &y){
	if(x.a!=y.a)
		return x.a<y.a;
	if(x.b!=y.b)
		return x.b<y.b;
	return x.c<y.c;
}
void upd(int x,int y){
	for(int i=x;i<=k;i+=(i&-i))
		tree[i]+=y;
}
int qry(int x){
	int res=0;
	for(int i=x;i;i-=(i&-i))
		res+=tree[i];
	return res;
}
void cdq(int lt,int rt){
	if(lt==rt)
		return;
	int mid=(lt+rt)>>1;
	cdq(lt,mid),cdq(mid+1,rt);
	int i=lt,j=mid+1,tot=lt;
	while(i<=mid&&j<=rt){
		if(q[i].b<=q[j].b){
			upd(q[i].c,q[i].cnt);
			tmp[tot++]=q[i++];
		}
		else{
			q[j].res+=qry(q[j].c);
			tmp[tot++]=q[j++];
		}
	}
	while(i<=mid){
		upd(q[i].c,q[i].cnt);
		tmp[tot++]=q[i++];
	}
	while(j<=rt){
		q[j].res+=qry(q[j].c);
		tmp[tot++]=q[j++];
	}
	for(int i=lt;i<=mid;i++)
		upd(q[i].c,-q[i].cnt);
	for(int i=lt;i<=rt;i++)
		q[i]=tmp[i];
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>q[i].a>>q[i].b>>q[i].c;
		q[i].res=0,q[i].cnt=1;
	}
	sort(q+1,q+n+1,cmp);
	int tot=1;
	for(int i=2;i<=n;i++){
		if(q[i].a==q[tot].a&&q[i].b==q[tot].b&&q[i].c==q[tot].c)
			q[tot].cnt++;
		else
			q[++tot]=q[i];
	}
	cdq(1,tot);
	for(int i=1;i<=tot;i++)
		ans[q[i].res+q[i].cnt-1]+=q[i].cnt;
	for(int i=0;i<n;i++)
		cout<<ans[i]<<'\n';
	return 0;
} 

例题选讲

  • P3157(构造偏序关系 + 时光倒流 trick

思维断层:没想到如何构造偏序。

直接倒过来做,删除操作变为添加。考虑添加一个数的贡献,显然它有三个属性 \(i,a_i,t_i\)\(t_i\) 为插入时间),它显然会对所有满足 \(i<j,a_i>a_j,t_i \ge t_j\)\(i>j,a_i<a_j,t_i \ge t_j\) 的所有 \(j\) 作出贡献,做两遍 cdq 即可。

  • P4169(推式子的两个技巧 + 矩阵转置

思维断层:没想到推式子这一步。

考虑去掉绝对值(技巧点一),我们考虑左上方的点,\(dis=x_i-x_j+y_i-y_j=(x_i+y_i)-(x_j+y_j)\)技巧点二),于是这相当于一个二维偏序,我们可以轻易维护 \(x_i+y_i\) 的最大值,然后做一遍 cdq(甚至不需要排序),对所有 \(dis\) 取最小值即为答案。剩余左下方、右上方和右下方都可以通过坐标变换来变为左上方处理。

  • P4093(cdq 优化 dp + 恒成立思想

思维断层:没有写出偏序条件。

\(dp_i\) 表示以 \(i\) 结尾的最长不下降子序列的长度,显然有转移:

\[dp_i = \max_{j<i} dp_j + 1 \]

这个寻找决策点 \(j\) 的过程可看作一个三维偏序问题,运用一下数学里的恒成立思想,不难写出以下偏序条件:

\[j < i \\ mx_j \le a_i \\ a_j \le mn_i \]

\(mx_i,mn_i\) 分别表示 \(i\) 位置出现的最大与最小的值)

这样,排序维护第一维、双指针维护第二维、树状数组维护第三维即可。

  • P4755(分类讨论 + 分治思想

思维断层:没有想到最大值未知如何处理。

这个最大值的约束可以转化为 \(a_j \le \frac{\max}{a_i}\),然后你就可以做三维偏序了。但是比较麻烦的一点就是你并不知道这个 \(\max\) 到底在左半边区间还是右半边,所以需要一点分类讨论。具体而言,可以维护一下 \([l,mid]\) 的后缀最大值和 \([mid+1,r]\) 的前缀最大值来判定最大值在哪边(\(l,r\) 是你双指针移动到的点,这么维护相当于就是根据 \(\max\) 的位置为界对 \([l,r]\) 进行分治)。假设 \(\max\) 在左半边,然后就让右半边贡献它,反之同理。

  • CF762E(去除 \(\min/\max\) 的限制技巧 + 双指针维护单调区间的技巧

思维断层;没有想到去除 \(\min\) 的限制,以及没有想到用双指针维护区间。

我们可以先对 \(r_i\) 排序去掉 \(\min\) 的限制,然后通过双指针维护频率差 \(\le k\),最后树状数组维护坐标差的限制即可。

  • P14426(画图 + 单调栈维护单调序列

思维断层:没有画图分析找出单调性。

本题可以转化为选择一对点构成矩形。考虑 cdq 分治,对于右半边的一个点 \(R\),我们需要找到左半边能对他做贡献的 \(L\)。记 \(R\) 左下方第一个比他低的一个点为 \(low\),我们显然需要找到满足 \(y_L \ge y_{low}\)\(L\),这个 \(low\) 可以用一个单调不上升的单调栈维护,每次在弹出栈顶后、插入之前来计算贡献。

然后我们需要找出 \(y_L \ge y_{low}\)\(L\) 的个数,这个则可以再使用一个单调不下降的单调栈维护这个 \(L\),然后查询的时候二分一下分界点即可。

这时候你可能会问:为啥我们不用树状数组或者其他什么数据结构维护这个 \(low\)\(L\) 呢?因为我们 cdq 之前首先按照 \(y\) 升序排列了,为了使每个矩形内部都没有点,我们必须使得新的决策点(\(L\) 或者 \(low\))不能和前面选过的有交集,所以说我们的 \(low\) 就必须保证 \(x\) 单调不上升,\(L\) 必须保证 \(x\) 单调不下降(这个自行画图可以理解),于是维护这样的单调序列就可以使用单调栈啦。

  • AT_arc159_f(刻画&发掘性质 + 摩尔投票
posted @ 2026-08-31 23:26  _KidA  阅读(8)  评论(0)    收藏  举报