最近学习了一波CDQ,发现很巧妙啊。也算是给非常规的数据结构解法起了一个头。
还是首先简单说一说CDQ是什么。就是说对于一些不要求强制在线的题目,我们使用分治的思想,花费单
log
l
o
g
的时间把它变成离线问题。正好有些题目的离线问题是比较简单的。
具体是什么意思呢?我们对于每一层分治,只考虑前一半对于后一半的影响,然后在每个询问当中记录下来影响。最后把所有影响合并就可以得到每一个询问的答案。
举个例子:区间修改区间查询。
首先,在时间轴上离线分治。每一层分治后把询问和查询都拆成两个(相当于全部变成修改(查询)前缀和),分别对于数轴从小到大排序。开一个记录和的变量,对于排好序的依次修改,查询也依次查询,只要保证先处理的操作在数轴上小于后处理的操作。
并且发现这个分治方法特别像归并排序,正好可以顺便处理归并排序,做到总时间复杂度
nlogn
n
l
o
g
n
当然会问:你为啥不用线段树呢?
那么请尝试一道树套树的题:【模板】三维偏序(陌上花开)
虽然可以各种数据结构套数据结构过去,然而亲测确实CDQ分治要快得多(原谅我线段树写得太傻了,速度比较感人)。
具体的CDQ方法是什么呢?
按一维作为时间轴,分治当中第二维排序,第三维使用树状数组维护(每次查询比第三维小的有多少个)。
可以发现其实这个三维问题只是比二维问题多了一个树状数组。
所以CDQ的好处大概能体会了:使用分治去掉一维,避免使用树套树和KDtree。
code:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int b[200005];
int ans[100005],n,k;
bool key;
struct lxy{
int x,y,z,ans;
bool operator < (const lxy &s)const
{
if(key==0)
{
if(x==s.x&&y==s.y&&z==s.z) return ans<s.ans;
else if(x==s.x&&y==s.y) return z<s.z;
else if(x==s.x) return y<s.y;
else return x<s.x;
}
else return y<s.y;
}
}a[100005];
int lowbit(int x){return x&(-x);}
void insert(int x,int type)
{
while(x<=k)
{
b[x]+=type;
x+=lowbit(x);
}
}
int ques(int x)
{
int ret=0;
while(x>0)
{
ret+=b[x];
x-=lowbit(x);
}
return ret;
}
void sovle(int l,int r)
{
if(l==r) return;
int mid=(l+r)/2;
key=1;sort(a+l,a+mid+1);sort(a+mid+1,a+r+1);
int p1=l;int p2=mid+1;
while(p2<=r)
{
if(a[p1].y<=a[p2].y&&p1<=mid) insert(a[p1].z,1),p1++;
else a[p2].ans+=ques(a[p2].z),p2++;
}
while(p1>l) p1--,insert(a[p1].z,-1);
key=0;sort(a+l,a+mid+1);sort(a+mid+1,a+r+1);
sovle(l,mid);sovle(mid+1,r);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
key=0;sort(a+1,a+1+n);
sovle(1,n);
key=0;sort(a+1,a+1+n);
a[n].ans++;ans[a[n].ans]++;
for(int i=n-1;i>=1;i--)
{
if(a[i].x==a[i+1].x&&a[i].y==a[i+1].y&&a[i].z==a[i+1].z) a[i].ans=a[i+1].ans;
else a[i].ans++;
ans[a[i].ans]++;
}
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
}
由于当时比较傻,总复杂度是
nlog2
n
l
o
g
2
,所以就没有写归并排序。
其实是比较懒。
好吧,其实我当时不会。
啦啦啦,再看一道题我就下线了[Violet 3]天使玩偶
据说是一道KDtree裸题???
然而我们的标题是CDQ啊。
一篇比较详细的博客
【这道题考虑每个点的周围四个方向上的点(左下、左上、右下、右上)时不能放在一起讨论,不然就没办法分治了。所以每个点每次只考虑一个方向,做四次(赶脚时间会很可怕啊。。。)d=|x-x’|+|y-y’|,当在左下时,d=x+y-(x’+y’),这时只需查找(x’+y’)最大即可,依然以x为关键字排序,以y为下标维护树状数组,每次查找最大值。 由于左下位置是最好考虑的,那么我们考虑把另三个方向通过轴对称变换改变其与当前点的相对位置,将它们搞到左下来再处理】
【其实这道题最大的问题在于如何优化时间,如果按照上两道题的方法,每次分治到一个区间就重新把左右区间排序,那么时间上肯定不允许,毕竟按照这种方法,在每次改变点的相对位置后,还需要把操作的顺序归为初始,这样就坐等TLE吧。。。 实际上,我们可以每次分治前,把整个操作序列按x为关键字排序,然后再分治到每一区间时,调整这个区间的操作顺序,这样就可以缩短时间】
大概就是这个思想,对于某些难以比较的东西,我们可以把它拆分成本质相同的东西进行比较。我们也可以发现很有意思的东西:CDQ对于区间似乎不是很兼容,它可能对于前缀的操作更加熟练。当然对于很多题目我们是可以把区间转化成前缀的。
这里多说一句:这种求最优解划分成多个最优解再合并的思想实在oi中用的很多的。比如scoi的D2T1:大概是一个数列,每次询问连续3个数与
u
u
差的绝对值的最大值是多少?支持单点修改。询问给定,求哪3个数符合上述条件。
感觉小学并没有学好,绝对值的另一种表示方法:
max(u−x,x−u)
m
a
x
(
u
−
x
,
x
−
u
)
$
我们只需要建8棵线段树分别维护:
+a+b+c
+
a
+
b
+
c
−a+b+c
−
a
+
b
+
c
+a−b+c
+
a
−
b
+
c
+a+b−c
+
a
+
b
−
c
+a−b−c
+
a
−
b
−
c
−a+b−c
−
a
+
b
−
c
−a−b+c
−
a
−
b
+
c
−a−b−c
−
a
−
b
−
c
然后在这里面分别取最大值,再加上对应的
u
u
的个数再取。
由于取绝对值和的情况在8种里面是最大的。所以不合法情况不会成为最优解。另一种说法,如果一个不合法情况成为最优解,一定会有这种情况对应的合法情况比它更优。而且这种做法包含了所有合法情况,不会出现漏解。
当然,今天我们并不是讲这道题,只是突然想起来了。
如果不理解天使玩偶的话,看一看代码,详细解释一下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<stack>
#define lowbit(x) (x&(-x))
using namespace std;
bool key=0;
struct lxy{//一般用结构体存比较好排序
int x,y,type,tim,ans;
bool operator < (const lxy &k)const{
if(key==1){
if(x!=k.x) return x<k.x;
return tim<k.tim;
}
return tim<k.tim;
}
}q[2000005],tax[2000005];
int n,m,cnt,maxx,maxy;
int b[4000005];
stack <int> d;
inline int readit()//卡常题标志
{
int a=0;char s;
s=getchar();
while(s<'0'||s>'9') s=getchar();
while(s>='0'&&s<='9')
{
a=a*10+s-'0';
s=getchar();
}
return a;
}
inline int query(int x)//树状数组求最值
{
int ret=-0x3f3f3f3f;
while(x>0)
{
ret=max(ret,b[x]);
x=x-lowbit(x);
}
return ret;
}
inline void modify(int x,int k)//树状数组修改
{
while(x<=maxx+maxy)
{
if(b[x]==-0x3f3f3f3f) d.push(x);
b[x]=max(b[x],k);
x=x+lowbit(x);
}
}
void cdq(int l,int r)//cdq分治
{
if(l==r) return;
if(r<=n){//由于前n个全是修改,就直接返回
key=1;sort(q+l,q+r+1);
return;
}
int mid=(l+r)>>1;
cdq(l,mid);cdq(mid+1,r);//先向下分治
int t1=l,t2=mid+1;
for(int i=l;i<=r;i++)
{//这里是归并排序,先放到另一个数组里,然后再拿出来赋一遍值
if(((q[t1].x<q[t2].x||(q[t1].x==q[t2].x&&q[t1].type<q[t2].type))&&t1<=mid)||t2>r){
tax[i]=q[t1];t1++;
}
else{
tax[i]=q[t2];t2++;
}
}
for(int i=l;i<=r;i++)
q[i]=tax[i];
for(int i=l;i<=r;i++)
{//本来两段都是按x排序的看谁x小先操作谁,特判又一边没有和两边相等先修改后查询
if(q[i].type==1&&q[i].tim<=mid) modify(q[i].y,q[i].x+q[i].y);//在q[i].y插入值q[i].x+q[i].y
if(q[i].type==2&&q[i].tim>mid) q[i].ans=min(q[i].ans,q[i].x+q[i].y-query(q[i].y));//查询q[i].y前缀的最大值,并更新答案
}
while(!d.empty())
b[d.top()]=-0x3f3f3f3f,d.pop();//清空树状数组
}
int main()
{
n=readit();m=readit();
for(int i=1;i<=n;i++)
{
cnt++;q[cnt].tim=cnt;
q[cnt].x=readit();q[cnt].y=readit();
++q[cnt].x;++q[cnt].y;
maxx=max(q[cnt].x,maxx);
maxy=max(q[cnt].y,maxy);
q[cnt].type=1;
}
for(int i=1;i<=m;i++)
{
cnt++;q[cnt].tim=cnt;
q[cnt].type=readit();q[cnt].x=readit();q[cnt].y=readit();
++q[cnt].x;++q[cnt].y;
maxx=max(q[cnt].x,maxx);
maxy=max(q[cnt].y,maxy);
q[cnt].ans=0x3f3f3f3f;
}
maxx++;maxy++;
for(int i=1;i<=maxx+maxy;i++)
b[i]=-0x3f3f3f3f;
//做4遍,分别对应查询点的4个方位
for(int i=1;i<=cnt;i++) q[i].x=maxx-q[i].x;cdq(1,cnt);
key=0;sort(q+1,q+1+cnt);
for(int i=1;i<=cnt;i++) q[i].y=maxy-q[i].y;cdq(1,cnt);
key=0;sort(q+1,q+1+cnt);
for(int i=1;i<=cnt;i++) q[i].x=maxx-q[i].x;cdq(1,cnt);
key=0;sort(q+1,q+1+cnt);
for(int i=1;i<=cnt;i++) q[i].y=maxy-q[i].y;cdq(1,cnt);
key=0;sort(q+1,q+1+cnt);
for(int i=1;i<=cnt;i++)
if(q[i].type==2)
printf("%d\n",q[i].ans);
}