Part1:前置知识
归并排序,逆序对,二维偏序,树状数组
Part 2:CDQ分治
【模板题】三维偏序
题目大意
有 n n n 个元素,第 i i i 个元素有 a i a_i ai, b i b_i bi, c i c_i ci 三个属性,设 f ( i ) f(i) f(i) 表示满足 a j ≤ a i a_j \leq a_i aj≤ai 且 b j ≤ b i b_j \leq b_i bj≤bi 且 c j ≤ c i c_j \leq c_i cj≤ci 且 j ≠ i j \ne i j=i 的 j j j 数量。
对于 d ∈ [ 0 , n ) d \in [0, n) d∈[0,n),求 f ( i ) = d f(i) = d f(i)=d 的数量。
解题思路
-
同“二维偏序”,先按 a a a 数组从小到大排序
-
现在考虑当 n = 8 n=8 n=8 时,首先将数组一分为二,递归左边 [ 1 , 4 ] [1,4] [1,4] ,递归右边 [ 5 , 8 ] [5,8] [5,8], 再计算左边对右边的影响(即左边是否有元素能被右边的元素统计进它的 f f f )
-
我们假设左边和右边内部的答案都已经计算得出,那么再来考虑左边对右边的贡献(影响)。此时问题就又变成了一个二维偏序,我们可以在左右两个区间内部按 b b b 的大小排序(因为内部答案已算出,内部排序不影响最终结果),再利用树状数组求出左边对右边的贡献.
-
那么我们不断地对一个区间一分为二地递归,再计算左边对右边的影响,就可以计算出答案
注意事项
-
因为题目的 f ( i ) f(i) f(i) 的判断条件可以取等号,所以我们排序完后需要将数组去重
-
每层递归后,我们需要将树状数组清空。但用 m e m s e t memset memset 可能会超时,所以需要一个数组来记录修改的元素
代码
#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=200010;
struct node
{
int a,b,c,cnt,num; //cnt表示f(i)的大小(不取等号),num表示该元素的个数
}t[N],f[N];
int n,k,tot,c[M],ans[N];
bool cmp1(node x,node y)
{
return x.a<y.a || (x.a==y.a && x.b<y.b) || (x.a==y.a && x.b==y.b && x.c<y.c);
}
bool cmp2(node x,node y)
{
return x.b<y.b || (x.b==y.b && x.c<y.c);
}
void add(int x,int y)
{
for(x; x<=k; x+=(x&-x))
c[x]+=y;
}
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s+=c[x];
return s;
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid); //一分为二地递归
solve(mid+1,r);
int len1=mid-l+1,len2=r-mid;
sort(f+l,f+l+len1,cmp2); //按b的大小排序
sort(f+mid+1,f+mid+1+len2,cmp2);
vector <int> rec; //统计树状数组修改了哪个元素
for(int i=l,j=mid+1; j<=r; j++)
{
while(i<=mid && f[i].b<=f[j].b)
{
add(f[i].c,f[i].num);
rec.push_back(i);
i++;
}
f[j].cnt+=ask(f[j].c); //更新答案
}
for(int i=0; i<rec.size(); i++) //清空树状数组
add(f[rec[i]].c,-f[rec[i]].num);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1; i<=n; i++)
scanf("%d%d%d",&t[i].a,&t[i].b,&t[i].c);
sort(t+1,t+1+n,cmp1); //按照a的大小排序
int tt=0;
for(int i=1; i<=n; i++) //去重
{
tt++;
if(t[i].a!=t[i+1].a || t[i].b!=t[i+1].b || t[i].c!=t[i+1].c)
{
f[++tot].a=t[i].a;
f[tot].b=t[i].b;
f[tot].c=t[i].c;
f[tot].num=tt;
tt=0;
}
}
solve(1,tot); //CDQ分治
for(int i=1; i<=tot; i++) //统计答案
ans[f[i].cnt+f[i].num-1]+=f[i].num;
for(int i=0; i<n; i++)
printf("%d\n",ans[i]);
return 0;
}
*总结:CDQ分治的模型
对于区间
[
1
,
L
]
[1,L]
[1,L]
1.设
m
i
d
=
(
l
+
r
)
>
>
1
mid=(l+r)>>1
mid=(l+r)>>1,递归计算
s
o
l
v
e
(
l
,
r
)
solve(l,r)
solve(l,r)
2.递归计算
s
o
l
v
e
(
m
i
d
+
1
,
r
)
solve(mid+1,r)
solve(mid+1,r)
3.计算第
l
−
m
i
d
l-mid
l−mid 项操作对第
m
i
d
+
1
−
r
mid+1-r
mid+1−r 项操作的影响
时间复杂度: O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
关于上述模型的正确性,大家可自行证明
【练习题】Mokia
(题目传送门)
题目大意
维护一个 w ∗ w w*w w∗w 的矩阵,初始值均为 0 0 0 。
每次操作可以增加某格子的权值,或询问某子矩阵的总权值。
解题思路
-
首先,如二维前缀和一般,对于左下角为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ,右上角为 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 的询问,我们可以把它转化为四个询问: s u m ( x 1 − 1 , y 1 − 1 ) sum(x_1-1,y_1-1) sum(x1−1,y1−1), s u m ( x 1 − 1 , y 2 ) sum(x_1-1,y_2) sum(x1−1,y2), s u m ( x 2 , y 1 − 1 ) sum(x_2,y_1-1) sum(x2,y1−1), s u m ( x 2 , y 2 ) sum(x_2,y_2) sum(x2,y2)
-
之后,我们发现,对于第 i i i 项查询,必定会受到前面修改操作的影响,因此,我们可以考虑CDQ分治。类似三维偏序,此问题的查询也有三维:时间 t t t ,行 x x x ,列 y y y 。所以,我们只需要寻找并计算 t j < t i t_j<t_i tj<ti 且 x j ≤ x i x_j\le x_i xj≤xi 且 y j ≤ y i y_j\le y_i yj≤yi 的第 j j j 项修改对第 i i i 项查询的影响
-
我们先对整个区间一分为二,对于两个独立的区间在里面进行CDQ分治,虽然右边的修改不会对左边的查询产生影响,但左边的修改会对右边的查询产生影响,所以我们还需计算左对右的影响
-
计算左对右的影响时,因为左边的 t t t 始终小于右边的 t t t ,所以问题就变成了一个二维偏序:对 x j ≤ x i x_j\le x_i xj≤xi 且 y j ≤ y i y_j\le y_i yj≤yi 的第 j j j 项修改进行计算
代码
#include<bits/stdc++.h>
using namespace std;
const int N=2000000,M=200010;
struct node
{
int op,x,y; //op表示操作类型,x,y表示坐标
int val,id; //val对于操作1来说就是增加量,对于操作2来说就是前缀和运算时是加还是减
//id是对于操作2来说的,表示是第几个查询操作
}a[M];
int s,w,n,cnt,ans[M]; //n表示操作序列长度,cnt表示查询操作个数
int c[N]; //树状数组
bool cmp(node a,node b)
{
return a.x<b.x || (a.x==b.x && a.y<b.y);
}
void add(int x,int y)
{
for(x; x<=w; x+=(x&-x))
c[x]+=y;
}
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s+=c[x];
return s;
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid);
solve(mid+1,r);
int len1=mid-l+1,len2=r-mid;
sort(a+l,a+l+len1,cmp); //对左半边和右半边排序
sort(a+mid+1,a+mid+1+len2,cmp);
vector <int> rec;
for(int i=l,j=mid+1; j<=r; j++)
{
while(i<=mid && a[i].x<=a[j].x) //寻找xi<=xj
{
if(a[i].op==1) //如果是操作1
{
add(a[i].y,a[i].val);
rec.push_back(i);
}
i++;
}
ans[a[j].id]+=a[j].val*ask(a[j].y); //更新答案
}
for(int i=0; i<rec.size(); i++) //恢复树状数组
add(a[rec[i]].y,-a[rec[i]].val);
}
int main()
{
cin>>s>>w; //s无用
int temp;
while(cin>>temp && temp!=3)
{
if(temp==1)
{
int x,y,v;
cin>>x>>y>>v;
a[++n]=(node){1,x,y,v,0};
}
else
{
int x,y,xx,yy;
cin>>x>>y>>xx>>yy;
cnt++; //记录查询操作个数
a[++n]=(node){2,x-1,y-1,1,cnt}; //拆分成4次查询操作
a[++n]=(node){2,xx,y-1,-1,cnt};
a[++n]=(node){2,x-1,yy,-1,cnt};
a[++n]=(node){2,xx,yy,1,cnt};
}
}
solve(1,n);
for(int i=1; i<=cnt; i++)
cout<<ans[i]<<endl;
return 0;
}
【练习题】天使玩偶
(题目传送门)
题目大意
- 定义两个点之间的距离为 dist ( A , B ) = ∣ A x − B x ∣ + ∣ A y − B y ∣ \operatorname{dist}(A,B)=|A_x-B_x|+|A_y-B_y| dist(A,B)=∣Ax−Bx∣+∣Ay−By∣
- 在刚开始时,Ayu 已经知道有 n n n 个点可能埋着天使玩偶
- 再接下来
m
m
m 行,每行三个非负整数
t
,
x
i
,
y
i
t,x_i,y_i
t,xi,yi
- 如果 t = 1 t=1 t=1,则表示 Ayu 又回忆起了一个可能埋着玩偶的点 ( x i , y i ) (x_i,y_i) (xi,yi)
- 如果 t = 2 t=2 t=2,则表示 Ayu 询问如果她在点 ( x i , y i ) (x_i,y_i) (xi,yi) 那么在已经回忆出来的点里,离她近的那个点有多远
解题思路
-
首先来看问题的简化版——假设没有 t = 1 t=1 t=1 的操作,这时问题的答案很明显为
m i n { ∣ x − x i ∣ + ∣ y − y i ∣ } , 1 ≤ i ≤ n min\{|x-x_i|+|y-y_i|\},1\le i\le n min{∣x−xi∣+∣y−yi∣},1≤i≤n -
为了去掉绝对值符号,我们不妨把原来的询问分为 4 4 4 个,分别询问在 ( x , y ) (x,y) (x,y) 的左下、左上、右上、右下方向上距离最近的点有多远, 4 4 4 个结果取最小值即为答案。
以左下方向为例,此时要求的式子变为:
m i n { ( x − x i ) + ( y − y i ) } , 1 ≤ i ≤ n min\{(x-x_i)+(y-y_i)\},1\le i\le n min{(x−xi)+(y−yi)},1≤i≤n
进一步化简为:
( x + y ) − m a x { x i + y i } , 1 ≤ i ≤ n , x i ≤ x , y i ≤ y (x+y)-max\{x_i+y_i\},1\le i\le n,x_i\le x,y_i\le y (x+y)−max{xi+yi},1≤i≤n,xi≤x,yi≤y -
所以,对于左下方向的点,我们可以先将所有点按横坐标从小到大排序,再利用树状数组去求出 m a x { x i + y i } max\{x_i+y_i\} max{xi+yi} ,那么就完成了对左下方向的求解
-
对于其它三个方向,我们可以通过坐标的变换,把它们均转化为左下方向
-
那么现在,我们就要来考虑带有 t = 1 t=1 t=1 的操作,我们可以把输入变成一个长度为 n + m n+m n+m 的序列,进行 4 4 4 次CDQ分治,即可求出答案
注意事项
-
如果在 4 4 4 次CDQ里的每一层都进行sort排序,那么复杂度会变得非常大,所以我们在操作的过程中顺便进行归并排序,这样便大大节省了时间
-
由于此题是一个平面直角坐标系,坐标可能会取到0,但树状数组在进行 l o w b i t ( 0 ) lowbit(0) lowbit(0) 运算时会出错,所以要将输入的所有坐标+1
-
再进行坐标变换时,坐标会变成负数,此时需要给坐标加上一个偏移量,这个偏移量= m a x { x , y } + 1 max\{x,y\}+1 max{x,y}+1,注意要+1,否则最大的那个坐标变换后会变为0
-
有一种特殊情况:某一点非常靠近边界,导致某次变换时,没有点在它的左下。这样查询时默认返回了0,最终的距离就成了这个点到原点的距离,但原点是不存在的(经过刚刚的更改,已经没有横坐标或纵坐标为0的点)。为避免这种情况,在树状数组查询时需要特判,若为0则返回 − I N F -INF −INF。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1000010,M=1000010,INF=1e9;
struct node
{
int op,x,y;
int ans,id;
}a[N],b[N],s[N]; //b用于CDQ里的操作,s用于归并排序
int n,m,mx,c[M];
bool cmp(node a,node b)
{
return a.x<b.x;
}
void add(int x,int y)
{
for(; x<=mx; x+=(x&-x))
c[x]=max(c[x],y);
} //树状数组查询最大值
int ask(int x)
{
int s=0;
for(; x; x-=(x&-x))
s=max(c[x],s);
return s==0? -INF:s; //特殊情况
}
void clear_(int x)
{
for(; x<=mx; x+=(x&-x))
c[x]=0; //清空树状数组
}
void solve(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
solve(l,mid);
solve(mid+1,r);
int i=l,k=l;
for(int j=mid+1; j<=r; j++)
{
while(i<=mid && b[i].x<=b[j].x)
{
if(b[i].op==1)
add(b[i].y,b[i].x+b[i].y);
s[k++]=b[i++]; //顺便进行归并排序
}
if(b[j].op==2)
a[b[j].id].ans=min(a[b[j].id].ans,b[j].x+b[j].y-ask(b[j].y)); //更新答案
s[k++]=b[j];
}
for(int j=l; j<i; j++) //清空树状数组
if(b[j].op==1)
clear_(b[j].y);
while(i<=mid) //归并排序的善后工作
s[k++]=b[i++];
for(int i=l; i<=r; i++) //将排好序的s数组赋值给b数组
b[i]=s[i];
}
void cdq(int xx,int yy) //xx和yy控制x和y坐标是否变换
{
for(int i=1; i<=n+m; i++)
{
b[i]=a[i];
if(!xx)
b[i].x=-b[i].x+mx;
if(!yy)
b[i].y=-b[i].y+mx;
}
solve(1,n+m);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
int xx,yy;
scanf("%d%d",&xx,&yy);
a[i].x=++xx; a[i].y=++yy;
a[i].op=1; a[i].id=i;
mx=max(mx,max(xx,yy));
}
for(int i=n+1; i<=n+m; i++)
{
int t,xx,yy;
scanf("%d%d%d",&t,&xx,&yy);
a[i].x=++xx; a[i].y=++yy;
a[i].op=t; a[i].id=i;
a[i].ans=INF;
mx=max(mx,max(xx,yy));
}
mx++;
cdq(1,1); cdq(1,0); cdq(0,1); cdq(0,0); //4次cdq
for(int i=n+1; i<=n+m; i++)
if(a[i].op==2)
printf("%d\n",a[i].ans);
return 0;
}