CDQ分治与整体二分
两种做法本质上都是划分子问题后进行解决,基于分治思想,但要求离线。
CDQ分治
偏序问题
有一个 count stars 的题,说有一片星星(不超过\(6e4\)),给出所有坐标(整数,大概\(3e4\)范围),求出对于每个星星,有多少个星星在它的左下方。
这个叫二维偏序,可以先对着横坐标\(x\)排序,然后依次扔进树状数组里(以纵坐标\(y\)为下标),每个星星扔进去之前在树状数组里求一下不超过\(y\)的数量和即可。在这个过程中,最开始的排序保证了\(x\)的顺序,树状数组保证了\(y\)的顺序。
那么三维偏序呢?
有\(n\)点,每个点有\(u,v,w\)三种属性,点\(i\)的等级定义为所有满足\(u_j\leq u_i,v_j\leq v_i,w_j\leq w_i\)的\(j\)点的个数,求从\(0\)到\(n-1\)每个等级的点数。
这就需要用到CDQ陈丹琦分治了。
归并排序
我太依赖 sort 了,现在不得不再来说说归并排序,它是 CDQ 分治的重要组成部分。
归并排序本质上以分治思想为基础。先把区间\([L,R]\)从中间划分开,我们假装他的左右区间已经通过递归排好序了,现在只需要把左右区间混一块使整个区间有序。
考虑来两个指针\(l,r\),一开始\(l\)指着\(L\),\(r\)指着\(mid+1\),比较\(l,r\)所指的数,\(l\)指的数小就把这个数扔进答案数组,然后让\(l\)向后移一位,\(r\)指的数小就把这个数扔进答案数组,然后让\(r\)向后移一位。\(l\)一直到\(mid\)停止,\(r\)一直到\(R\)停止。
递归分治的复杂度为\(log\),合并左右区间的复杂度为\(O(n)\),因此总复杂度为\(O(nlogn)\)。
void merge(int L, int R){
if(L >= R) return;
int mid = (L + R) >> 1;
merge(L, mid);
merge(mid + 1, R);
int l = L, r = mid + 1, cnt = 0;
for(; l <= mid || r <= R; ){
if((r == R + 1) || (a[l] <= a[r] && l <= mid && r <= R)){
b[++cnt] = a[l];
l++;
}else{
b[++cnt] = a[r];
r++;
}
}
for(int i = 1; i <= cnt; i++) a[L + i - 1] = b[i];
}
merge(1, n);
CDQ分治的实现
拿上面那个三维偏序问题来说。
首先,对着\(u\)排序,\(u\)有序了。
然后进入归并排序的递归分治过程,用它来对着\(v\)排序。
现在我们来到一个区间\([L,R]\),假装\([L,mid]\)和\([mid+1,R]\)内部的答案已经统计完了,现在只需要统计整个大区间的贡献。由于事先给\(u\)排过序了,所以左区间的\(u\)肯定都小于右区间的\(u\),因此只能是左区间给右区间做贡献。
现在分别给\([L,mid]\)和\([mid+1,R]\)按\(v\)升序排序。虽然此时\(u\)被打乱了,但\([L,mid]\)的\(u\)全部小于\([mid+1,R]\)的\(u\)不会变,这样就够了,因为左右两个区间内部的答案我们已经计算过了,不用再考虑了。
在拿着\(l,r\)两个指针扫的时候,如果要放一个左区间的数,不会有人给他贡献答案了,他只可能给后面右区间的数做贡献,因此把它扔进树状数组,以\(w\)为下标。如果要放一个右区间的数,现在树状数组里面存的都是左区间的数,\(u\)一定比他的小,我们归并时按\(v\)从小到大扫, 里面的\(v\)也一定比他的小,所以他能拿到的贡献就要加上树状数组里小于等于他的\(w\)的数的个数。
这一层做完以后清空树状数组,再进入下一层。最后就能求出答案。
当然还有一个细节问题,对于所有\(u,v,w\)三种属性都相等的数,它们是可以相互贡献的,然而我们在归并时只能计算左边对右边的贡献。于是要先把序列去重,算完以后再让\(res_i\)加上\(cnt_i-1\)即可。
这样,我们通过三种排序或统计手段的合理交错计算出了正确的答案。
int lowbit(int x){
return x & -x;
}
bool s1(node a, node b){
if(a.u != b.u) return a.u < b.u;
if(a.v != b.v) return a.v < b.v;
return a.w < b.w;
}
bool s2(node a, node b){
if(a.v != b.v) return a.v < b.v;
return a.w < b.w;
}
void add(int x, int kk){
if(x <= 0) return c[0] = 0, void();
for(; x <= k; x += lowbit(x)) c[x] += kk;
return;
}
int ask(int x){
if(x <= 0) return 0;
int ans = 0;
for(; x; x -= lowbit(x)) ans += c[x];
return ans;
}
void calc(int L, int R){
if(L >= R) return;
int mid = (L + R) / 2;
calc(L, mid);
calc(mid + 1, R);
int l = L, r = mid + 1, cnt = 0;
sort(p + L, p + mid + 1, s2);
sort(p + mid + 1, p + R + 1, s2);
for(;l <= mid || r <= R; ){
if((r == R + 1) || (p[l].v <= p[r].v && l <= mid && r <= R)){
a[++cnt] = l;
add(p[l].w, p[l].jud);
l++;
}else{
a[++cnt] = r;
p[r].res += ask(p[r].w);
r++;
}
}
for(int i = L; i <= mid; i++) add(p[i].w, -p[i].jud);
}
signed main(){
n = read();
k = read();
for(int i = 1; i <= n; i++) s[i].u = read(), s[i].v = read(), s[i].w = read();
sort(s + 1, s + n + 1, s1);
tot = 0;
for(int i = 1; i <= n; i++){
if(s[i].u == p[tot].u && s[i].v == p[tot].v && s[i].w == p[tot].w){
p[tot].jud += 1;
continue;
}
p[++tot] = s[i];
p[tot].jud = 1;
}
sort(p + 1, p + tot + 1, s1);
calc(1, tot);
for(int i = 1; i <= n; i++) p[i].res += (p[i].jud - 1);
for(int i = 1; i <= n; i++) Res[p[i].res] += p[i].jud;
for(int i = 0; i < n; i++) cout << Res[i] << '\n';
return 0;
}
CDQ分治的简单应用
一些较难处理的动态问题,或许可以通过CDQ分治转化为静态问题。
比如这个。
对于每个元素\(i\),我们记录三样东西:位置\(p_i\),值\(v_i\),删除时间\(d_i\)(如果不删就令其\(d_i=m+1\),表示一直都在)。删掉\(i\)以后,逆序对减少的数量就是所有原本与\(i\)构成逆序对的元素\(j\)的数量。
这样的\(j\)到底有多少个?不难发现它们都满足以下两个条件之一。
\(1.p_j<p_i,v_j>v_i,d_j>d_i.\)
\(2.p_j>p_i,v_j<v_i,d_j>d_i.\)
那这不就是裸的三维偏序吗。
还有一个小trick。为了代码好写,只用写一个CDQ分治,可以考虑比如令\(p_i=n+1-i\),就可以把\(p_j<p_i\)转化为\(p_j>p_i\),不过别忘了做完以后再换回来。
其它扩展应用静待\(update\)
整体二分
经典应用与模版实现
比较经典的一个应用是查询区间第\(k\)大的问题,当然在权值线段树上二分也可以解决此类问题,不过如果询问可以离线的话,就可以拿整体二分来解决。
为了方便,先从不带修改的静态区间第\(k\)大问题入手。参见例题。
首先,把所有操作存起来。把输入序列看做是加数操作,记为操作类型一,记录下标、值和操作类型。把查询操作记为操作类型二,记录查询的左右端点,\(k\)值和操作类型。这些操作都存在同一个结构体数组里,类型一在类型二前,二者都按序排列。
然后开始二分。整体二分的过程类似于分治,也就是子问题划分的过程。
二分两个东西:当前的操作区间\([l,r]\),即将要处理的一段操作,和答案所在区间\([L,R]\)。令\(mid=(L+R)/2\)。先处理当前操作区间内的类型一操作,如果一个元素的值大于\(mid\),让它准备滚到右边去,如果它的值小于等于\(mid\),就把它扔进树状数组,并准备去左边。这样处理完以后,树状数组上\([1,x]\)的前缀就表示原序列中下标在\([1,x]\)且值小于等于\(mid\)的元素数量。然后拿着类型二的操作,假如它的询问区间是\([ql,qr]\),那么看看这个区间内小于等于\(mid\)的数有多少个,假如有\(sum\)个,要是\(sum>k\),说明它的答案一定在\([mid+1,R]\)内,让它的\(k\)减去\(sum\)然后准备滚到右边去,否则就直接准备去左边。
做完以后,把操作区间\([l,r]\)重排一下,将要去左边的操作放在左边,将要去右边的操作放在右边,然后左右分开,各自进入下一个子问题。在这之前别忘了清空树状数组。这样一直做下去,直到\(L=R\),也就是当前\([l,r]\)操作区间内的询问操作的答案都已经确定为\(L\)了,统计完以后往回走即可。
不难发现,与普通二分不同,整体二分始终在整个元素序列上操作,它二分的是答案的值域,并以此来划分操作序列。也正是因为要对着操作序列统一处理,所以要求必须离线。
因为涉及到树状数组,所以复杂度是\(O(nlog^2n)\)的。
struct node{
int li, ri, k, type, id;
}q[N];
int res[N], c[N];
void add(int x, int k){
if(! x) return;
for(; x <= n; x += lowbit(x)) c[x] += k;
}
int ask(int x){
if(! x) return 0;
int ans = 0;
for(; x; x -= lowbit(x)) ans += c[x];
return ans;
}
void solve(int ql, int qr, int l, int r){//ql,qr表示操作区间,l,r表示答案所在区间(值域)
if(ql > qr) return;
if(l == r){
for(int i = ql; i <= qr; i++){
if(q[i].type == 2) res[q[i].id] = l;
}
return;
}
int mid = (l + r) >> 1;
vector<node>q1, q2;//q1表示将要去左边的,q2表示将要去右边的
for(int i = ql; i <= qr; i++){
if(q[i].type == 1){
if(q[i].k <= mid){
add(q[i].id, 1);
q1.pb(q[i]);
}
else q2.pb(q[i]);
}else{
int sum = ask(q[i].ri) - ask(q[i].li - 1);
if(sum >= q[i].k) q1.pb(q[i]);
else{
q[i].k -= sum;
q2.pb(q[i]);
}
}//能这么写是因为不管怎么分类型一定排在类型二前面
}
for(int i = ql; i <= qr; i++){
if(q[i].type == 1 && q[i].k <= mid) add(q[i].id, -1);
else if(q[i].type == 2) break;
}
int tt = -1, Mid = 0;
for(auto x : q1) q[++tt + ql] = x;//操作序列重排
Mid = tt + ql;//操作区间的左右分界点
for(auto x : q2) q[++tt + ql] = x;
solve(ql, Mid, l, mid);//递归子问题
solve(Mid + 1, qr, mid + 1, r);
}
signed main(){
n = read();
m = read();
for(int i = 1; i <= n; i++) q[i] = {0, 0, read(), 1, i};
for(int i = 1; i <= m; i++) q[n + i] = {read(), read(), read(), 2, i};
solve(1, n + m, 0, 1e9);
for(int i = 1; i <= m; i++) cout << res[i] << '\n';
return 0;
}
那要是动态带修改呢?把这个操作分成两个:先减去原来的,再加上后来的就可以了。
其他应用
等待\(update\)
警钟撅烂
一种错误写法:
int Mid = s1.size(), t = 0;
for(auto x : s1) q[ql + t] = x, t++;
for(auto x : s2) q[ql + t] = x, t++;
for(int i = ql; i <= qr; i++){
if(q[i].type == 1){
if(q[i].id <= mid) add(q[i].x, -1);
} else break;
}
正确写法:
for(int i = ql; i <= qr; i++){
if(q[i].type == 1){
if(q[i].id <= mid) add(q[i].x, -1);
} else break;
}
int Mid = s1.size(), t = 0;
for(auto x : s1) q[ql + t] = x, t++;
for(auto x : s2) q[ql + t] = x, t++;
一种错误写法:
Mid = s1.size();
solve(ql, Mid, l, mid);
solve(Mid + 1, qr, mid + 1, r);
正确写法:
Mid = s1.size();
solve(ql, ql + Mid - 1, l, mid);
solve(ql + Mid, qr, mid + 1, r);

浙公网安备 33010602011771号