[关闭]
@xiaoziyao 2021-06-28T12:56:07.000000Z 字数 4347 阅读 1225

分散层叠算法学习笔记

杂项 学习笔记


更好的阅读体验

前言

分散层叠算法,英文为Fractional Cascading,可以以优秀的时空复杂度在线计算出某个值在若干个序列中后继。

Fractional意为分散的,Cascading意为阶梯式渗透,而Fractional Cascading本质上就是对于若干个有序信息分散开,然后一层层渗透,然后得到最后的结果。

“它相当形象地揭示了这个名词背后的深刻含义:像瀑布一样从高到低逐渐分散成越来越细的支流,再层层叠叠地覆盖。”
—— 蒋明润 《浅谈利用分散层叠算法对经典分块问题的优化》

模板

P6466 分散层叠算法(Fractional Cascading)

给定个长度为有序数组,次查询某个数的非严格后继。(

注意,下面的后继都指的是非严格后继。

首先有一个很简单的时间空间做法,即对于每个序列都二分一遍。

还有一种做法是时间空间的,也就是将个序列归并排序成为一个长度为的序列,每个位置维护它在每个序列中的后继,询问在大序列里二分。

考虑综合两个方法分别的优势,得到更优秀的做法。

个序列为,我们按下面的方法构造个序列(设长度为):

不难得到,即有,于是预处理的时空复杂度为

查询的时候,我们在内二分得到的后继位置,则内的后继。

考察内的后继之间的距离,若其大于,则有,则存在一个不大于的值在的前面,与的后继矛盾,因此

那么我们直接暴力检查之前一个位置,判断它是否更优就可以得到内的后继。

以此类推,查询的总复杂度为

  1. #include<stdio.h>
  2. #include<algorithm>
  3. using namespace std;
  4. const int maxn=20005,maxk=105;
  5. int n,k,q,d,lastans;
  6. int a[maxk][maxn],len[maxk],c[maxn];
  7. struct node{
  8. int val,pos,lst;
  9. }b[maxk][maxn],tmp[maxn];
  10. int main(){
  11. scanf("%d%d%d%d",&n,&k,&q,&d);
  12. for(int i=1;i<=k;i++)
  13. for(int j=1;j<=n;j++)
  14. scanf("%d",&a[i][j]);
  15. len[k]=n;
  16. for(int i=1;i<=n;i++)
  17. b[k][i].val=a[k][i],b[k][i].pos=i,b[k][i].lst=0;
  18. for(int i=k-1;i>=1;i--){
  19. for(int j=2;j<=len[i+1];j+=2)
  20. tmp[j/2]=b[i+1][j],tmp[j/2].lst=j;
  21. int ptmp=1,plst=1;
  22. for(int j=1;j<=n;j++){
  23. while(ptmp<=len[i+1]/2&&tmp[ptmp].val<=a[i][j])
  24. len[i]++,b[i][len[i]]=tmp[ptmp],b[i][len[i]].pos=j,ptmp++;
  25. while(plst<=len[i+1]&&b[i+1][plst].val<a[i][j])
  26. plst++;
  27. len[i]++,b[i][len[i]].val=a[i][j],b[i][len[i]].pos=j,b[i][len[i]].lst=plst;
  28. }
  29. while(ptmp<=len[i+1]/2)
  30. len[i]++,b[i][len[i]]=tmp[ptmp],b[i][len[i]].pos=n+1,ptmp++;
  31. }
  32. for(int i=1;i<=len[1];i++)
  33. c[i]=b[1][i].val;
  34. for(int i=1;i<=q;i++){
  35. int x,pos;
  36. scanf("%d",&x),x^=lastans,pos=lower_bound(c+1,c+len[1]+1,x)-c;
  37. lastans=0;
  38. for(int i=1;i<=k;i++){
  39. if(pos>1&&b[i][pos-1].val>=x)
  40. pos--;
  41. if(pos<=len[i])
  42. lastans^=a[i][b[i][pos].pos],pos=b[i][pos].lst;
  43. else pos=len[i+1]+1;
  44. }
  45. if(i%d==0)
  46. printf("%d\n",lastans);
  47. }
  48. return 0;
  49. }

扩展

在一个每个点度数不超过的DAG上,每个节点存在一个长度为的有序序列,每次查询在一条链上每个点对应的序列中的后继。

不难发现模板的内容是DAG是一条链的情况。

该问题的解法法类似于模板,考虑在拓扑排序中每遇到一个,就让归并入(没有入度的点),每次询问从的信息得到的信息的时候可以二分查找做到

此时点的最劣长度是不大于到达的点长度最大值的,即每个点长度最大值是趋近于数列的极限的。

把所有除以就可以发现拆开就是等比数列求和的形式:

等比数列求和可得

于是这个做法的时间复杂度为,空间复杂度为的。视为常数时复杂度为

本质

实际上,上文中提取的信息是一种误差的思想(似乎在Dpair的博客内称为扰动系数)

我们考虑对于若干个序列进行分散层叠时,可以从每个序列提取的信息,做一遍分散层叠,最后直接用二分找到真实的位置,这样分散层叠复杂度会变为,这个改变有助于我们在某些题目中平衡算法的时间复杂度,减小算法的空间复杂度。(还可以在线处理)

应用

例题1

区间加,区间查询一个数的排名。

首先考虑一个很简单的做法:枚举每个块,在块内二分出小于的数的个数,复杂度为

序列分块,设块长为,误差为,再设一个阈值表示每个块拉出来跑一次分散层叠,那么我们一共跑了个分散层叠(预处理复杂度)。(这类似于在分块的基础上继续分块)

对于第一层的整块,询问直接合并分散层叠(复杂度为)修改对应的整块打标记(复杂度),对应的散块暴力重构,这样的复杂度为的。

对于第一层的散块(同时也是第二层的整块)暴力建分散层叠可以做到

对于第二层的散块跑暴力,复杂度为

总体复杂度为

经过计算可得时复杂度为

这种方法复杂度可以更小,但是没有实际意义了。

我们发现上面的方法是分块上分块的结果,于是很自然地想到分更多层,即利用线段树的结构进行更多层分块。

我们对个块建立线段树,每个非叶子节点存储两个儿子信息的归并。

对于区间修改,仍然采用整块打标记散块重构的方式,不难发现散块重构需要修改散块对应节点到线段树根节点这条路径的所有点代表的序列,序列长度之和为,当时一次修改复杂度变为了

对于区间查询,我们在根节点二分出位置,然后暴力递归儿子,由于线段树的结构,共有个节点,每个节点的误差需要的时间调整,于是一次询问复杂度为越小复杂度越小。

时复杂度明显最优,此时时间复杂度,空间复杂度,极其优秀。

例题2

区间加,区间查询第大。

P5356 [Ynoi2017] 由乃打扑克

咕咕咕。

例题3

区间加,单点修改,区间查询最大值小于等于值的子区间数量。

P6578 [Ynoi2019] 魔法少女网站

咕咕咕。

例题4

区间加,区间查询最大子段和。

P4118 [Ynoi2018] 末日时在做什么?有没有空?可以来拯救吗?

咕咕咕。

后记

分散层叠算法适用于在线算法时间复杂度的平衡以及空间复杂度的减小,在分块问题上有着广泛的应用,其思想也应用在了range tree,跳表等算法/数据结构中,但劣势是实现的细节过多。

所以做分散层叠口胡就行了,比赛还是用别的方法吧。

参考资料

分散层叠算法学习笔记——Dpair

题解 P6466 【分散层叠算法(Fractional Cascading)】——AThousandSuns

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注