数据结构中的一块内容:$CDQ$分治算法。

$CDQ$显然是一个人的名字,陈丹琪(NOI2008金牌女选手)

这种离线分治算法被算法界称为"cdq分治"

我们知道,一个动态的问题一定是由"更改""查询"操作构成的,显然,有些“更改”会改变"查询的结果",而有些不能

如果我们合理安排一个次序,把每一个查询分成几个部分,分别计算值,最后合起来就是原来询问的值。

离线算法和在线算法的概念不用过多解释.

接下来通过几个例题将基本的$CDQ$分治算法解释明白.

A. 从逆序对开始的二维偏序问题

下面将给出逆序对的题目:

例题·A1: 逆序对的定义是对于序列a[],取第$l$个元素和第$r$个元素,满足$l <r$且$a[l]>a[r]$,对于这样数对$(l,r)$被称为一对逆序对

求解给定序列a中有多少对逆序对,你需要输出对数.

对于100%的数据 $a \leq 5 \times 10^5 $

我们都知道可以用树状数组和归并排序两种方法做,这里我们只讲归并排序(默认树状数组大家都会)

不断把$[l,r]$细分,每次取$mid=\frac{l+r}{2}$ 然后指针$i$和$j$分别指向区间$[l,mid]$和$[mid+1,r]$并且单调

先确定$i$不动,把$j$右移归并,如果满足$a[i]>a[j]$记录逆序对个数加$mid-i+1$意味着a[i],$i \in [i,mid]$和 a[j]互成逆序对,跳出把$i$移动

依次,直到完成所有区间,复杂度$O(n log_2 n)$

核心代码Code:

void sort(int l,int r,int mid)
{
for (int i=l;i<=mid;i++) a[i]=t[i];
for (int i=mid+;i<=r;i++) b[i]=t[i];
int i=l,j=mid+,k=l;
while (i<=mid&&j<=r) {
if (a[i]<=b[j]) c[k++]=a[i++];
else c[k++]=b[j++],ans+=mid-i+;
//当前的b中当前的j和剩下的ai都是一对逆序对
}
while (i<=mid) c[k++]=a[i++];
while (j<=r) c[k++]=b[j++];
for (int i=l;i<=r;i++) t[i]=c[i];
}
void merge_sort(int l,int r)
{
int mid=(l+r)/;
if (l<mid) merge_sort(l,mid);
if (r>mid) merge_sort(mid+,r);
sort(l,r,mid);
}

然后这个问题反映什么实质呢,显然我们可以造出2个维度记偏序(t,i)表示时间为t,位置为i

由于我们按照顺序读入,那么默认第一维偏序是有序的,便忽略了这一维度的记录和排序。

换句话说,在归并的时候虽然左区间和右区间里面值不一定相等,但是左区间的所有元素都排在右区间前面

这是基于第一维偏序是有序的情况下进行的.

然而,对于有些情况,第一维偏序的记录和第一维偏序的排序是不可省略的

下面给出树状数组的模板题,请用CDQ分治(二维偏序)方法AC本题。

例题A2维护含n个元素的原始序列a[],有m个操作,2种不同格式:

1 x k 含义:将第x个数加上k

2 x y 含义:输出区间[x,y]内每个数的和

请正确输出每一个2操作的答案,不强制在线。

对于100%的数据 $n,m \leq 5 \times 10^5$

Solution:把原始序列读入操作变化成插入第i个位置上加上对应的值,参与cdq分治。

首先把查询拆成两个,减去x-1的前缀和和加上y的前缀和(前缀和优化的思路)

每一个询问记录4个量type[类型],x,y(两个参数),idx(若是询问操作表示是第几个询问)

时间作为默认有序的第一维度,用cdq分治维护第二维度位置(归并按照位置归并)。

仅考虑左边的更改对右边查询的影响,更改只有在左侧有影响,当$ tl\geq mid $且$a[tl].x<a[tr].x $同时满足的情况下,将前缀和sum+=y[加上的值]

这个时候第一维已经默认有序了,也就是说明乱序下左边操作的时间一定在右边操作之前,所以更改对右边有影响,影响是sum

同时对于位置归并cdq,虽然打乱了这一部分时间,但是对于大块时间的划分是没有影响的.

我们更新的是处理过[mid,r]这一段的答案,如果tr>r不在这个区间内,需要舍去

一种通俗的写法是,将tr>r的情况归属到更改的影响中,这样不会对答案造成干扰.

这是核心代码Code:

void cdq(int l,int r)
{
if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid); cdq(mid+,r);
int tl=l,tr=mid+,sum=;
fp(i,l,r) {
if ((tl<=mid&&a[tl]<a[tr])||tr>r) {
if (a[tl].type==) sum+=a[tl].y;
b[i]=a[tl++];
} else {
if (a[tr].type==) ans[a[tr].idx]-=sum; //2代表-sum{l-1}
if (a[tr].type==) ans[a[tr].idx]+=sum;//3代表+sum{r}
b[i]=a[tr++];
}
}
fp(i,l,r) a[i]=b[i];
}
# include <bits/stdc++.h>
# define fp(i,s,t) for (int i=s;i<=t;i++)
# define int long long
using namespace std;
const int N=(5e5+)*;
struct rec{
int type,x,y,idx;
bool operator < (const rec &t) const{
if (x!=t.x) return x<t.x;
else return type<t.type;
}
}a[N],b[N];
int n,m,tot,qes,ans[N];
inline int read(int &x)
{
int X=,w=; char c=;
while(c<''||c>'') {w|=c=='-';c=getchar();}
while(c>=''&&c<='') X=(X<<)+(X<<)+(c^),c=getchar();
x=w?-X:X;
}
void write(int x)
{
if (x<) putchar('-'),x=-x;
if (x>) write(x/);
putchar(''+x%);
}
void cdq(int l,int r)
{
if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid); cdq(mid+,r);
int tl=l,tr=mid+,sum=;
fp(i,l,r) {
if ((tl<=mid&&a[tl]<a[tr])||tr>r) {
if (a[tl].type==) sum+=a[tl].y;
b[i]=a[tl++];
} else {
if (a[tr].type==) ans[a[tr].idx]-=sum;
if (a[tr].type==) ans[a[tr].idx]+=sum;
b[i]=a[tr++];
}
}
fp(i,l,r) a[i]=b[i];
}
signed main()
{
read(n); read(m);
fp(i,,n) {
tot++; a[tot].type=; a[tot].x=i;
read(a[tot].y);
}
fp(i,,m) {
int t,x,y; tot++;
read(t); a[tot].type=t;
if (t==) read(a[tot].x),read(a[tot].y);
else {
read(x),read(y);
qes++; a[tot].idx=qes; a[tot].x=x-; a[tot].type=;
tot++; a[tot].idx=qes; a[tot].x=y; a[tot].type=;
}
}
cdq(,tot);
fp(i,,qes) write(ans[i]),putchar('\n');
return ;
}

P3374 树状数组模板1 Code

B. 从二维偏序三维偏序

一维偏序直接sort

二维偏序第1维sort,第2维cdq分治

三维偏序第1维sort,第2维cdq分治,第3维 数据结构

下面给出三维偏序问题:

给定$n$个有序三元组$(a,b,c)$,求对于每个3元组$(a,b,c)$,

有多少个3元组$(a_i,b_i,c_i)$满足$a_i<a$且$b_i<b$且$c_i<c$.

仿造上面的写法,对第1维a排序,对第2维b归并cdq,

对于第3维c,我们借助权值树状数组, 每次从左边取出三元组(a,b,c),根据c值在树状数组中进行修改

从右边的序列中取出三元组(a,b,c)时,在树状数组中查询c值小于(a,b,c)的三元组的个数

注意,每次使用完树状数组要把树状数组清零

下面我们给出一个例题,即陌上花开是luogu三维偏序的模板题

例题B1:陌上花开

有$n$个元素,每个元素有$a_i,b_i,c_i$三种属性,定义两个元素大小比较为:

若$a_i>a_j$且$b_i>b_j$且$c_i>c_j$简记为$i>j$,对于不同元素i,需要统计他比多少元素大,

就是元素$i$的权$f(i)$,问对于 $d\in [0,n-1] $有多少$x\in [1,n] $的取值$f(x)=d$ 输出一个数组表示 $ d=0,1,2,3...n-1$时的答案

对于100%的数据 $n \leq 10^5 $

Solution:

首先是可能存在一些相同的元素的,我们定义元素相同为三种属性均相同。这样较为难处理,第一步我们把他们合并起来了。

于是对于每种元素,记录了四个参数a,b,c,w,id表示三种属性、数量、他的标号。

然后对其第1维a进行排序[其实在去重这一步我们事实上已经完成了这一操作]

然后对于第2维b进行cdq分治,在每一处分治的时候树状数组维护插入和查询,每次需要插入的时候在值域树状数组维护数量的前缀和,

插入的时候,对于z处加上数量w,update(z,w),查询的时候,在ans[id]+=query(z)。

然后就可以统计出对于每一朵花有多少元素比他大了,记录在ans[id]中,id $\in$ [1,n]

输出的时候还需要统计,别忘了加上自己的那份w和减去自己的1!

核心代码Code:

    //去重操作
sort(t+,t++n,cmp1);
int i=,j; tot=;
while (i<=n){
j=i+;
while (j<=n&&same(t[i],t[j])) j++;
j--;
a[++tot]=t[i]; a[tot].w=(j-i+); a[tot].id=tot;
i=j+;
}
void cdq(int l,int r) //cdq分治
{
if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid);cdq(mid+,r);
dfn++; //便于去重
int t1=l,t2=mid+;
for (int i=l;i<=r;i++) {
if ((t1<=mid&&a[t1].y<=a[t2].y)||t2>r) {
update(a[t1].z,a[t1].w);
b[i]=a[t1++];
} else {
ans[a[t2].id]+=query(a[t2].z);
b[i]=a[t2++];
}
}
for (int i=l;i<=r;i++) a[i]=b[i];
}
# include <bits/stdc++.h>
# define int long long
# define lowbit(x) (x&(-x))
using namespace std;
const int N=4e5+;
struct rec{ int x,y,z,w,id;}a[N],t[N],b[N];
int n,m,dfn,k,tot;
int tim[N],ans[N],c[N],out[N];
inline int read()
{
int X=,w=; char c=;
while(c<''||c>'') {w|=c=='-';c=getchar();}
while(c>=''&&c<='') X=(X<<)+(X<<)+(c^),c=getchar();
return w?-X:X;
}
bool cmp1(rec a,rec b)
{
if (a.x!=b.x) return a.x<b.x;
if (a.y!=b.y) return a.y<b.y;
if (a.z!=b.z) return a.z<b.z;
}
bool same(rec a,rec b)
{
if (a.x==b.x&&a.y==b.y&&a.z==b.z) return true;
else return false;
}
void update(int x,int y)
{
for (int i=x;i<=k;i+=lowbit(i)) {
if (tim[i]!=dfn) tim[i]=dfn,c[i]=;
c[i]+=y;
}
}
int query(int x)
{
int ret=;
for (int i=x;i;i-=lowbit(i))
if (tim[i]==dfn) ret+=c[i];
return ret;
}
void write(int x)
{
if (x>) write(x/);
putchar(''+x%);
}
void cdq(int l,int r)
{
if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid);cdq(mid+,r);
dfn++;
int t1=l,t2=mid+;
for (int i=l;i<=r;i++) {
if ((t1<=mid&&a[t1].y<=a[t2].y)||t2>r) {
update(a[t1].z,a[t1].w);
b[i]=a[t1++];
} else {
ans[a[t2].id]+=query(a[t2].z);
b[i]=a[t2++];
}
}
for (int i=l;i<=r;i++) a[i]=b[i];
}
signed main()
{
n=read();k=read();
for (int i=;i<=n;i++)
t[i].x=read(),t[i].y=read(),t[i].z=read();
sort(t+,t++n,cmp1);
int i=,j; tot=;
while (i<=n){
j=i+;
while (j<=n&&same(t[i],t[j])) j++;
j--;
a[++tot]=t[i]; a[tot].w=(j-i+); a[tot].id=tot;
i=j+;
}
cdq(,tot);
for (int i=;i<=tot;i++) out[ans[a[i].id]+a[i].w-]+=a[i].w;
for (int i=;i<n;i++) write(out[i]),putchar('\n');
return ;
}

P3810三维偏序:陌上花开

C. 从维偏序三维偏序的应用

这一专题我们安排了两道例题,请按照三维偏序的思想和方法,尝试解决。

下面对于每一个例题讲解:

例题C1:逆序对的定义:对于序列a[],取第$i$个元素和第$j$个元素,满足$i <j$且$a[l]>a[r]$,对于这样数对$(l,r)$被称为一对逆序对 ,

给出一个[1,n]的排列,按照顺序依次删除m个数,询问当前逆序对数量.不强制在线。

对于100%的数据 :$N\leq 10^5,M\leq 5\times 10^4$

Solution:可以把每次的答案分成两个部分:原先存在的逆序对+加入这个数新产生的逆序对

那么每次只要算出当前新产生的逆序对,最后算一遍前缀和即可。

考虑逆序对产生方式: 1.位置靠前且值比它大的,2.位置靠后且比它小的。

这个问题在于有插入操作,和删除操作,比较难过

对于逆序对的产生是插入比他前的满足1 or 2两个条件的元素

对于逆序对的删除是删除时间比他后面的,满足1 or 2两个条件的元素。

显然对于把时间作为第1维进行排序是不合适的(时间在前和后有不同的影响),所以我们考虑把操作的位置作为第1维,进行排序

对于时间这1维度作为第2维度CDQ分治,

设当前删除了第i个元素,那么在[1,i]中比它大的都要减去,在[i+1,N]中比他小的都要减去。

正序循环的时候默认先加入树状数组的元素的位置都是在询问元素的前面的,所以要减去的是比询问元素大的那一个部分 [i+1,n]

倒序循环的时候默认先加入树状数组的元素的位置都是在询问元素后面的,所以要减去的是比询问元素小的那个部分[1,n-1]

同样的考虑影响还是处于左边时间修改对处于右边时间的查询造成的影响,注意同步清空树状数组.

Code :

# include <bits/stdc++.h>
//# define int long long
# define LL long long
using namespace std;
const int N=4e5+;
int n,m,tot;
int o[N];
LL ans[N];
struct node{
int t,pos,x,op,idx;
}a[N],b[N];
inline int read()
{
int X=,w=; char c=;
while(c<''||c>'') {w|=c=='-';c=getchar();}
while(c>=''&&c<='') X=(X<<)+(X<<)+(c^),c=getchar();
return w?-X:X;
}
void write(LL x)
{
if (x>) write(x/);
putchar(x%+'');
}
struct TreeArray{
# define lowbit(x) (x&(-x))
int c[N];
void update(int x,int y) { for (;x<=n;x+=lowbit(x)) c[x]+=y;}
int query(int x) { int ret=; for (;x;x-=lowbit(x)) ret+=c[x]; return ret;}
#undef lowbit
}tr;
int Query(int l,int r){return tr.query(r)-tr.query(l-);}
bool cmp(node a,node b)
{
if (a.pos!=b.pos) return a.pos<b.pos;
else return a.x<b.x;
}
void cdq(int l,int r)
{
if (l==r) return;
int mid=(l+r)>>; for (int i=l;i<=r;i++)
if (a[i].t<=mid) tr.update(a[i].x,a[i].op);
else ans[a[i].idx]+=(LL)a[i].op*Query(a[i].x+,n);
for (int i=l;i<=r;i++)
if (a[i].t<=mid) tr.update(a[i].x,-a[i].op); for (int i=r;i>=l;i--)
if (a[i].t<=mid) tr.update(a[i].x,a[i].op);
else ans[a[i].idx]+=(LL)a[i].op*Query(,a[i].x-);
for (int i=r;i>=l;i--)
if (a[i].t<=mid) tr.update(a[i].x,-a[i].op); int t1=l,t2=mid+;
for (int i=l;i<=r;i++)
if (a[i].t<=mid) b[t1++]=a[i];
else b[t2++]=a[i];
for (int i=l;i<=r;i++) a[i]=b[i];
cdq(l,mid); cdq(mid+,r);
}
signed main()
{
n=read();m=read();
for (int i=;i<=n;i++) {
int t=read(); o[t]=i;
a[++tot]=(node){tot,i,t,,};
}
for (int i=;i<=m;i++) {
int t=read(); int pos=o[t];
a[++tot]=(node){tot,pos,t,-,i};
}
sort(a+,a++tot,cmp);
cdq(,tot);
for (int i=;i<=m;i++) {
ans[i]+=ans[i-];
write(ans[i-]);
putchar('\n');
}
return ;
}

P3157 动态逆序对 代码

 例题C2:定义两点距离为麦哈顿距离$ Dist(A,B) = |A_x - B_x|+|A_y-B_y| $

给出初始n个点的坐标 $(x_i,y_i)$ 有m个操作,

1 x y : 表示在(x,y)处新增1个点

2 x y : 表示询问所以存在的点到点(x,y)最小距离

对于100%的数据 $n,m \leq 3 \times 10^5 , x_i,y_i \leq 10^6$

Solution:

如果点都在询问点的左下方就好了...(这样绝对值就直接消掉了)

However,人家有4个方向,怎么办,考虑到对称我们可以把所有点都做一次变换,称为对称

怎么个对称法呢,关于x轴对称,关于y轴对称,关于原点对称,对称的意思是所有点的坐标发生变换,

以原点对称为例,原来A在B的左下方,对称后A就在B的右上方了,那么类似于 按原点B对称的意思。

按照x轴对称,按照y轴对称同理。

接下来我们只讨论点在点左下方的情况(变换以后跑4遍不就好了!)

计算lx,ly表示x,y坐标最大取值+1

把Dist绝对值打开得$ Dist(A,B) = |A_x - B_x|+|A_y-B_y| =A_x+A_y-(B_x+B_y)$

对于给定的A点,$A_x+A_y$一定,当$ Dist(A,B)$取到最小值的时候$B_x+B_y$最大

所以用树状数组维护前缀最大值就行。

这里时间t是默认有序的作为第1维,然后cdq分治第2维x,然后用树状数组维护一个y,前缀最大值

当分治求答案的时候左边时间早于右边,左边影响右边,第2维x经过归并左边小于右边,所以为了保证在左下方还需y左边小于右边

所以求的时候求y的这个点前缀最大值即可.(注意如果没有请不要更新为0)

写了一个STD码风的程序Code:

# include <cstdio>
# include <cstring>
# define fp(i,s,t) for (int i=s;i<=t;i++)
using namespace std;
const int N=1e6 + ;
struct rec{
int t,x,y,op,idx;
bool operator < (const rec &t) const {
return x < t.x;
}
}a[N],tt[N],t[N];
int tot,n,m,lx,ly,qes;
int ans[N];
int max(int x , int y) {return x > y ? x : y;}
int min(int x , int y) {return x > y ? y : x;}
inline int read()
{
int X = ,w = ; char c = ;
while(c < '' || c > '') {w |= c=='-'; c = getchar();}
while(c >= '' && c <= '') X=(X<<) + (X<<) + (c^),c=getchar();
return w ? -X :X;
}
inline void write(int x)
{
if (x > ) write(x / );
putchar('' + x % );
}
struct TreeArray{
# define lowbit(x) (x & (-x))
int c[N];
void Empty() {memset(c,,sizeof(c));}
void update(int x,int y) {
for (;x <= ly;x += lowbit(x)) c[x] = max(c[x],y);
}
int query(int x) {
int ret=;
for (;x;x -= lowbit(x)) ret = max(ret,c[x]);
return ret;
}
void clear(int x) {
for (;x <= ly;x += lowbit(x)) if (c[x]) c[x] = ;
}
# undef lowbit
}tr;//前缀最大值
void Merge(int l,int r)
{
int mid = l + r >> ;
int i = l,j = mid + ,k = l;
while (i <= mid && j <= r) {
if (a[i] < a[j]) tt[k++] = a[i++];
else tt[k++] = a[j++];
}
while (i <= mid) tt[k++] = a[i++];
while (j <= r) tt[k++] = a[j++];
}//归并a[l,mid]和a[mid+1,r]
void cdq(int l,int r)
{
if (l==r) return;
int mid = l + r>>;
cdq(l,mid); cdq(mid+,r);
int j=l;
for (int i = mid + ;i <= r; i++)
if (a[i].op==) {
for (;j <= mid&&a[j].x <= a[i].x; j++) {
if (a[j].op==) tr.update(a[j].y,a[j].x + a[j].y);
}
int tmp = tr.query(a[i].y);
if (tmp) ans[a[i].idx] = min(ans[a[i].idx],a[i].x + a[i].y - tmp); //注意考虑0情况不要更新
}
for (int i = l;i < j;i++)
if (a[i].op==) tr.clear(a[i].y);
Merge(l,r); //等价于STL merge(a+l,a+mid+1,a+mid+1,a+r+1,tt+l);
for (int i=l;i<=r;i++) a[i] = tt[i];
}
int main()
{
n = read(); m = read();
fp(i,,n) {
int x = read() + , y=read() + ; //有 0 的问题加 1 解决
t[++tot] = (rec) {tot , x , y , , };
lx = max(lx,x); ly = max(ly,y);
}
fp(i,,m) {
int op = read(),x = read() + ,y = read() + ; //有 0 的问题加 1 解决
t[++tot] = (rec) {tot , x , y , op , (op==) ? : (++qes)};
lx = max(lx,x); ly = max(ly,y);
}
lx++; ly++;
memset(ans, 0x3f ,sizeof(ans));
tr.Empty(); fp(i,,tot) a[i] = t[i];
cdq(,tot);
tr.Empty(); fp(i,,tot) a[i] = t[i] , a[i].x = lx - a[i].x; //x对称
cdq(,tot);
tr.Empty(); fp(i,,tot) a[i] = t[i] , a[i].y = ly - a[i].y; //y对称
cdq(,tot);
tr.Empty(); fp(i,,tot) a[i] = t[i] , a[i].x = lx - a[i].x , a[i].y = ly - a[i].y; //O对称
cdq(,tot);
for (int i = ;i <= qes; i++) write(ans[i]) , putchar('\n');
return ;
}

P4169 [Violet]天使玩偶/SJY摆棋子

D.四维偏序(CDQ套CDQ)

下面给出四维偏序的模板题:

例题D1:现在有$n$个三元组$(a_i,b_i,c_i)$ 求有多少组三元组对$(i,j)$满足 $ i<j, a_i<a_j , b_i<b_j , c_i<c_j$

你的程序必须输出三元组对总对数。 数据保证a,b,c三个数组都是$[1,n]$排列。

对于100%的数据 $n \leq 5 \times 10^4$

(数据来自ljc20020730)

solution:我们类比二维偏序、三维偏序的方法考虑四维偏序的求解方法

首先把第1维排序,显然是位置(这里没记,原数组就是按照顺序位置排布的)

然后对第2维进行CDQ分治,但是剩下还有2维怎么办。

我们不妨再对第3维进行CDQ分治,然后第4维使用数据结构统计。

显然当我们归并第2维以后数组变成了按照第2维度排序的有序数组但是第1维时间是杂乱无章的。

然而我们只关心第1维是在左边还是右边,并不关心具体值,于是我们可以用Left和Right来标记a值(part)

既然b已经有序在CDQ套的cdq下面(不妨把正式求解的分治算法称之为CDQ,CDQ中套如的子分治算法叫做cdq)

把原来CDQ中第2、3、4维换成之前做过三维偏序的1、2、3维就行了(同时注意更新时判断,对于CDQ是不是完成了左边更改对右边查询的影响)

cdq完成的目的是:在CDQ后前面部分对后面部分的影响。

step 1: 对第一维进行排序。
step 2:对第1维重新标号(left或right),然后对第2维分治,递归解决子问题,按照第2维的顺序合并。此时只是单纯的合并,并不进行统计。
step 3: 把合并后的序列复制一份,在复制的那一份中进行cdq分治。(这时第2维默认有序)即对第3维分治,递归解决子问题,按照第3维的顺序合并。合并过程中用树状数组维护第4维的信息。

Code:

void cdq(int l,int r) //对于2,3,4维度重新cdq分治
{ if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid); cdq(mid+,r);
int i=l,j=mid+,k=l;
while (i<=mid&&j<=r) {
if (t1[i].b<t1[j].b) { //按照第3维归并
if (t1[i].part==Left) update(t1[i].c); //更新的前提条件第1维在左边,影响第1维的右边
t2[k++]=t1[i++];
} else {
if (t1[j].part==Right) ans+=query(t1[j].c); //答案累加的前提条件是第1维在右边受到第1维左边影响
t2[k++]=t1[j++];
}
}
while (i<=mid) t2[k++]=t1[i++];
while (j<=r) {
if (t1[j].part==Right) ans+=query(t1[j].c);
t2[k++]=t1[j++];
}
fp(i,l,r) {
if (t2[i].part==Left) clear(t2[i].c);//清空树状数组
t1[i]=t2[i];
}
}
void CDQ(int l,int r) //注意此时已经默认第1维有序我们不必记录
{ if (l==r) return;
int mid=(l+r)>>;
CDQ(l,mid); CDQ(mid+,r);
int i=l,j=mid+,k=l;
while (i<=mid&&j<=r) {
if (a[i].a<a[j].a) {
a[i].part=Left; //对于第2维度a归并,并记录第1维是左边(left)还是右边(right)
t1[k++]=a[i++];
} else {
a[j].part=Right;
t1[k++]=a[j++];
}
}
while (i<=mid) a[i].part=Left,t1[k++]=a[i++];
while (j<=r) a[j].part=Right,t1[k++]=a[j++];
fp(i,l,r) a[i]=t1[i]; //t1就是复制过去的数组
cdq(l,r);
}
# include<bits/stdc++.h>
# define int long long
# define fp(i,s,t) for (int i=s;i<=t;i++)
# define Left
# define Right
using namespace std;
const int N=5e4+;
struct rec{
int a,b,c,part;
}a[N],t1[N],t2[N];
int c[N];
int n,ans;
# define lowbit(x) (x&(-x))
void update(int x){for (;x<=n;x+=lowbit(x))c[x]++;}
int query(int x) { int ret=; for (;x;x-=lowbit(x)) ret+=c[x]; return ret;}
void clear(int x){for (;x<=n;x+=lowbit(x)) c[x]=;}
inline int read()
{
int X=,w=; char c=;
while(c<''||c>'') {w|=c=='-';c=getchar();}
while(c>=''&&c<='') X=(X<<)+(X<<)+(c^),c=getchar();
return w?-X:X;
}
void cdq(int l,int r) //对于2,3,4维度重新cdq分治
{ if (l==r) return;
int mid=(l+r)>>;
cdq(l,mid); cdq(mid+,r);
int i=l,j=mid+,k=l;
while (i<=mid&&j<=r) {
if (t1[i].b<t1[j].b) { //按照第3维归并
if (t1[i].part==Left) update(t1[i].c); //更新的前提条件第1维在左边,影响第1维的右边
t2[k++]=t1[i++];
} else {
if (t1[j].part==Right) ans+=query(t1[j].c); //答案累加的前提条件是第1维在右边受到第1维左边影响
t2[k++]=t1[j++];
}
}
while (i<=mid) t2[k++]=t1[i++];
while (j<=r) {
if (t1[j].part==Right) ans+=query(t1[j].c);
t2[k++]=t1[j++];
}
fp(i,l,r) {
if (t2[i].part==Left) clear(t2[i].c);//清空树状数组
t1[i]=t2[i];
}
}
void CDQ(int l,int r) //注意此时已经默认第1维有序我们不必记录
{ if (l==r) return;
int mid=(l+r)>>;
CDQ(l,mid); CDQ(mid+,r);
int i=l,j=mid+,k=l;
while (i<=mid&&j<=r) {
if (a[i].a<a[j].a) {
a[i].part=Left; //对于第2维度a归并,并记录第1维是左边(left)还是右边(right)
t1[k++]=a[i++];
} else {
a[j].part=Right;
t1[k++]=a[j++];
}
}
while (i<=mid) a[i].part=Left,t1[k++]=a[i++];
while (j<=r) a[j].part=Right,t1[k++]=a[j++];
fp(i,l,r) a[i]=t1[i]; //t1就是复制过去的数组
cdq(l,r);
}
signed main()
{
n=read();
fp(i,,n) a[i].a=read();
fp(i,,n) a[i].b=read();
fp(i,,n) a[i].c=read();
CDQ(,n);
printf("%lld\n",ans);
return ;
}

四维偏序 code

E. CDQ系列问题时间复杂度分析:

给出 Master 定理:

定义T(n)为算法时间复杂度,对于规模为n的问题可以分为a个规模为$\frac{n}{b}$的子问题,

在在每一个子问题的解决中,其他运算的时间复杂度是$f(n)$,$c$是一个由$a,b$决定的数

定义$T(n)=aT(\frac{n}{b})+f(n)$ , 令$crit=log_b \ a$

$f(n)=O(n^c)$且$c<crit$时 , $T(n)=O(n^{crit})$

$\exists k \geq 0$使得$f(n)=O(n^{crit} {log_2}^k n)$那么$T(n)=O(n^{crit}{log_2}^{k+1} n)$

对于2维偏序问题时间复杂度是$O(n log_2 n)$ 如归并排序求逆序对

对于3维偏序问题由于加上树状数组的维护时间复杂度是$O(n {log_2}^2 n)$ 如陌上花开

对于4维偏序的问题由于CDQ套CDQ还加上了树状数组维护复杂度是$O(n {log_2}^3 n)$ 如偏序[HZOI2016]

对于一般k维偏序问题,利用CDQ套...求解是时间复杂度是$O(n {log_2}^{k-1} n)$

上述结论可由Master定理求证。

尾声:  

到此处为止,$CDQ$分治的算法介绍已经基本完毕。

已经有了离线处理动态问题的思路:把动态问题按照时间、位置分治进行cdq优化

使时间复杂度降低,并且不需要高级数据结构如$K-D Tree$在线维护,简便快速解决问题。

每降低一维的时间复杂度只需要用$log_n$的复杂度。

当然,只掌握这些内容是远远不够的,需要强调cdq算法的限制:

1、必须离线操作

2、对区间更新问题解决有困难

3、代码调试细节较多,请积极对拍验证

(The End)

CDQ分治学习笔记的更多相关文章

  1. 初学cdq分治学习笔记(可能有第二次的学习笔记)

    前言骚话 本人蒟蒻,一开始看到模板题就非常的懵逼,链接,学到后面就越来越清楚了. 吐槽,cdq,超短裙分治....(尴尬) 正片开始 思想 和普通的分治,还是分而治之,但是有一点不一样的是一般的分治在 ...

  2. [摸鱼]cdq分治 && 学习笔记

    待我玩会游戏整理下思绪(分明是想摸鱼 cdq分治是一种用于降维和处理对不同子区间有贡献的离线分治算法 对于常见的操作查询题目而言,时间总是有序的,而cdq分治则是耗费\(O(logq)\)的代价使动态 ...

  3. CDQ分治学习笔记(三维偏序题解)

    首先肯定是要膜拜CDQ大佬的. 题目背景 这是一道模板题 可以使用bitset,CDQ分治,K-DTree等方式解决. 题目描述 有 nn 个元素,第 ii 个元素有 a_iai​.b_ibi​.c_ ...

  4. 三维偏序[cdq分治学习笔记]

    三维偏序 就是让第一维有序 然后归并+树状数组求两维 cdq+cdq不会 告辞 #include <bits/stdc++.h> // #define int long long #def ...

  5. CDQ分治学习思考

    先挂上个大佬讲解,sunyutian1998学长给我推荐的mlystdcall大佬的[教程]简易CDQ分治教程&学习笔记 还有个B站小姐姐讲解的概念https://www.bilibili.c ...

  6. cdq分治学习

    看了stdcall大佬的博客 传送门: http://www.cnblogs.com/mlystdcall/p/6219421.html 感觉cdq分治似乎很多时候都要用到归并的思想

  7. [Updating]点分治学习笔记

    Upd \(2020/2/15\),又补了一题 LuoguP2664 树上游戏 \(2020/2/14\),补了一道例题 LuoguP3085 [USACO13OPEN]阴和阳Yin and Yang ...

  8. 点分治&&动态点分治学习笔记

    突然发现网上关于点分和动态点分的教程好像很少……蒟蒻开篇blog记录一下吧……因为这是个大傻逼,可能有很多地方写错,欢迎在下面提出 参考文献:https://www.cnblogs.com/LadyL ...

  9. 学习笔记 | CDQ分治

    目录 前言 啥是CDQ啊(它的基本思想) 例题 后记 参考博文 前言 博主太菜了 学习快一年的OI了 好像没有什么会的算法 更寒碜的是 学一样还不精一样TAT 如有什么错误请各位路过的大佬指出啊感谢! ...

随机推荐

  1. [Oracle]如何查看 10046 trace 中的 tim= ... 的具体时刻

    可以在  Linux 下,用下列方式: 如10046 trace 文件中如果有如下的内容:... tim = 1503032923 可以用 date 命令加 option 来看它的时刻: date - ...

  2. JDK8漫谈——代码更优雅

    简介 lambda表达式,又称闭包(Closure)或称匿名方法(anonymous method).将Lambda表达式引入JAVA中的动机源于一个叫"行为参数"的模式.这种模式 ...

  3. 实例解析forEach、for...in与for...of

    在开发过程中经常需要循环遍历数组或者对象,js也为我们提供了不少方法供使用,其中就有三兄弟forEach.for...in.for...of,这三个方法应该是使用频率最高的,但很多人却一值傻傻分不清, ...

  4. Codeforces Round #503 (by SIS, Div. 2)-C. Elections

    枚举每个获胜的可能的票数+按照花费排序 #include<iostream> #include<stdio.h> #include<string.h> #inclu ...

  5. Linux内核分析——进程的切换和系统的一般执行过程

    进程的切换和系统的一般执行过程 一.进程切换的关键代码switch_to分析 (一)进程调度与进程调度的时机分析 1.不同类型的进程有不同的调度需求 第一种分类: (1)I/O-bound:频繁进行I ...

  6. Linux内核分析 期末总结

    Linux内核分析 期末总结 一.知识概要 1. 计算机是如何工作的 存储程序计算机工作模型:冯诺依曼体系结构 X86汇编基础 会变一个简单的C程序分析其汇编指令执行过程 2. 操作系统是如何工作的 ...

  7. 素数问题三步曲_HDOJ2098

    偶然间OJ上敲到一题素数问题便查询了相关算法.对于该类问题我个人学习分为三步曲:最笨的方法(TLE毫无疑问)->Eratosthrnes筛选法->欧拉线性筛选法 针对HDOJ2098这道题 ...

  8. 用delete和trancate删除表记录的区别

    首先说相同点,就是他们都能删除表中的数据,区别有两点: 第一点: delete语句在删除记录的时候可以有选择的删除某些数据(使用where子句),当然,如果不添加where子句,就是删除所有记录 而t ...

  9. Spring事务银行转账示例

    https://www.imooc.com/video/9331 声明式事务 @Transactiona() 编程式事务 非模板式(不使用TransactionTemplate) http://cai ...

  10. Android控件第3类——AdapterView

    AdapterView这一类控件的最大特点,在绝大多数的情况下,它们的数据都由Adapter的子类提供(有时可以在控件的entries属性上直接设置显示的数据). 调用AdapterView的setA ...