听上去十分的高大上,但是究其本质就是二分答案加强版,对有些题目,我们单次二分答案的代价可能无法做到 O(logn) O ( log n ) 。但是对于每个询问它做的操作是几乎相同的,我们认为这类询问是可以合并在一起做的,那么大可不必对于每个询问都去二分答案,我们可以对询问进行分类,划到一个区间去再进行二分答案。
可以看一看2013年XHR的论文。要注意,分了区间之后,对于这个区间的时间代价不能是与原长n成正比,而应该是和当前区间长度成正比,否则时间复杂度就不是log的了。也就是说不能每次都memset,而应该将原来做过的操作全部回退。
板子题一道。对于一个二分函数,去二分这个值,怎么统计?把数字改成01序列,然后区间求和。但其实这样是不行的,因为改成01序列的时间复杂度是和数组原长相关的。
给出一种比较好的想法,把序列改为插入操作,不妨去扫描当前被划入这一二分函数的操作,如果插入的值小于等于mid,我们就把它加进去,那么当我们要知道当前的答案与mid的大小关系,就只需要统计区间中插入了多少个数就可以了!用树状数组即可实现。
时间复杂度: O(nlognlogmaxv) O ( n log n log m a x v )
核心代码如下。
void binary(int ql,int qr,int l,int r)
{
if(ql>qr) return ;
if(l==r)
{
for(int i=ql;i<=qr;i++)
if(q[i].op)
ans[q[i].id]=l;
return ;
}
int m=(l+r)>>1,x1=0,x2=0;
for(int i=ql,tmp;i<=qr;i++)
{
if(q[i].op)
{
tmp=sum(q[i].r)-sum(q[i].l-1);
if(q[i].k<=tmp) q1[++x1]=q[i];
else q2[++x2]=q[i],q2[x2].k-=tmp;
}
else if(q[i].k<=m) add(q[i].id,1),q1[++x1]=q[i];
else q2[++x2]=q[i];
}
for(int i=1;i<=x1;i++)
if(!q1[i].op)
add(q1[i].id,-1);
for(int i=1;i<=x1;i++)
q[ql+i-1]=q1[i];
for(int i=1;i<=x2;i++)
q[ql+x1+i-1]=q2[i];
binary(ql,ql+x1-1,l,m);binary(ql+x1,qr,m+1,r);
}
我们将修改操作改为删除+插入即可。
时间复杂度: O(nlognlogmaxv) O ( n log n log m a x v )
二分每个国家满足需求的时间,这个显然可以二分的。注意到所有国家的空间站总和为m,那么我们如果势能分析一下,每次询问国家时,直接去找(我是说用链式前向星找)其空间站统计收集的份数的时间复杂度是 O(nlognlogk) O ( n log n log k ) 的。注意一点,在最坏情况下总和可能爆long long,所以在累加的时候注意判一下。由于是区间修改,单调查询,用树状数组维护差分序列即可。
也算是一道板子题了,树状数组改成线段树即可,把插入的数字离散化可以大幅度减小二分的值域,大概可以优化出一个1/3的常数。
k维偏序问题就是给你n个点,及其k个关键信息。询问你对于每个点,有多少个点满足k个关键信息均小于等于它的关键信息。
一维偏序:……排序
二维偏序:先把第一维排序,用树状数组维护y坐标出现次数,然后按顺序扫,先询问y坐标更小的有多少,再把每个点的y坐标插入一个树状数组。排序保证了先插入的点必定x坐标是小于等于当前点的,所以这样是没有毛病的。时间复杂度 O(nlogn) O ( n log n ) 。
三维偏序:我们可以在第二维上用cdq来解决,cdq的过程有点类似归并排序,它的核心思想是把区间都拆开,每次计算跨立左右两子区间的贡献。再做对y轴进行归并排序时我们可以很好地统计贡献。
我们第一维仍然先排序。然后进行cdq,对y坐标进行归并排序,然后用树状数组插入z坐标。这就可以保证先插入的点x坐标和y坐标更小。
注意先修改后查询。注意初始化排序的时候x,y,z三维都要排。时间复杂度 O(nlog2n) O ( n log 2 n ) 。核心代码如下:
void cdq(int l,int r)
{
if(r<=l) return ;
int m=(l+r)>>1,p=l,q=m+1;
cdq(l,m);cdq(m+1,r);top=0;
while(p<=m&&q<=r)
{
if(op[p]if(op[p].tp==1) update(op[p].c,1),que.push(p);
tmp[++top]=op[p++];
}
else
{
if(op[q].tp==2) ans[op[q].id]+=query(op[q].c);
tmp[++top]=op[q++];
}
}
while(p<=m) tmp[++top]=op[p++];
while(q<=r)
{
if(op[q].tp==2) ans[op[q].id]+=query(op[q].c);
tmp[++top]=op[q++];
}
while(!que.empty()) update(op[que.front()].c,-1),que.pop();
for(int i=1;i<=top;i++) op[i+l-1]=tmp[i];
}
k维偏序:其实并不在我们讨论的范围之中,因为这个比较麻烦了,可能要各种套,时间复杂度达到了 O(nlogk−1n) O ( n log k − 1 n ) 。但是我们会发现有种东西叫kd-tree,可以做到 O(n2−1k) O ( n 2 − 1 k ) 的复杂度。再然后还有用bitset的 O(nn‾√) O ( n n ) 的做法。以及k很大时, O(n2) O ( n 2 ) 的暴力。
讲到这里就可以对cdq的基本思想有个基本的了解了。
cdq分治是在普通的分治中一种特殊的分治方法,它可以解决一些带修改和询问的操作。在分治的过程中,它会先解决左子区间的问题,然后考虑左边操作对右边问题的影响再来计算答案。每次计算跨立两个区间的贡献,同时左子区间要考虑对右子区间答案的影响。
还是改成插入和询问操作,矩形的询问可以拆成四个以(0,0)开始的四个不同询问。然后记到同一个答案数组里即可。
不妨用tp=1表示插入操作,tp=2表示要加入贡献的询问,tp=3表示要减的询问。核心代码如下。
void cdq(int l,int r)
{
if(r<=l) return ;
int m=(l+r)>>1,p=l,q=m+1,sum=0;
cdq(l,m);cdq(m+1,r);top=0;
while(p<=m&&q<=r)
{
if(op[p]if(op[p].tp==1) sum+=1;
tmp[++top]=op[p++];
}
else
{
if(op[q].tp==2) ans[op[q].id]+=sum;
else if(op[q].tp==3) ans[op[q].id]-=sum;
tmp[++top]=op[q++];
}
}
while(p<=m) tmp[++top]=op[p++];
while(q<=r)
{
if(op[q].tp==2) ans[op[q].id]+=sum;
else if(op[q].tp==3) ans[op[q].id]-=sum;
tmp[++top]=op[q++];
}
for(int i=1;i<=top;i++) op[l+i-1]=tmp[i];
}
请戳解题报告