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\) 结尾的最长不下降子序列的长度,显然有转移:
这个寻找决策点 \(j\) 的过程可看作一个三维偏序问题,运用一下数学里的恒成立思想,不难写出以下偏序条件:
(\(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(刻画&发掘性质 + 摩尔投票)
浙公网安备 33010602011771号