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

小柯

2020-08-18 10:06:03

Solution

题意: 给定 $k$ 个序列,$q$ 次询问每个序列里 $x$ 的后继。 首先,~~本蒟蒻并没有学过分散层叠算法,于是去百度,又看不懂,再去 OIWiki ,又没有...于是决定自己想该怎么做...(主要是看到这道紫题还没有题解)~~ 如果还是把所有序列分开处理的话,是很难突破 $O(klogn)$ 的下界的(二分查找),因为这是强制在线的。 所以考虑将这 $k$ 个序列归并到一起,形成一个序列 $A$。 归并的时候可以采用胜/败者树提高效率,当然也可以全部丢掉一起基数排序一下,所以预处理只需要 $O(nk)$。 考虑询问 $x$ 在每个序列里的后继,很明显,如果只要 $A$ 里面的值是远远不够的,还应该知道每个值原来是属于哪个序列的。所以预处理的时候维护另一个序列 $B$,表示 $A_i$ 是属于第 $B_i$ 个序列的。 继续考虑询问 $x$,首先肯定能在 $A$ 序列中找到一个位置 $pos$,使得 $A_{pos}$ 为 $x$ 的后继(如果有解的话),那么每个序列里 $x$ 的后继在 $A$ 序列里都应该在 $pos$ 后面。 于是考虑从 $pos$ 处逐一往后找,直到找齐 $1$ ~ $k$ 的 $B_i$ ,再将对应的 $A_i$ 异或起来即可。 在数列非常均匀的情况下,大约 $k$ 次就能找齐。但是数列如果不均匀,可能退化成 $nk$ 次,所以考虑优化。 如果要从前往后逐一找 $k$ 个确定的目标,那么可以考虑维护指针,以直接跳到该位置。所以维护 $nxt_{i,j}$ 表示 $i$ 后面第一个 $B_x$ 为 $j$ 的 $x$。那么一旦确定 $pos$,那么只需要访问 $A_{nxt_{pos,1}},A_{nxt_{pos,2}},A_{nxt_{pos,3}}...$ 即可找齐 $k$ 个原序列里的后继。 但是这样做有一个问题,也就是预处理 $nxt_{i.j}$ 空间复杂度和时间复杂度都有可能吃不消,那就放弃这个算法?不,考虑冗余在哪里。 很明显 $nxt_{i,j}$ 与 $nxt_{i+1,j}$ 有很大程度上可能是相同的,也就是说 $nxt$ 中存在大量的重复,那么是否可以减少 $nxt_i$ 存储的 $j$ 呢?答案是肯定的。 定义 $Nxt_i$ 表示 $nxt_{i,i\%k+1}$ ,那么显然,将 $Nxt_{i+k-1}$ ~ $Nxt_i$ 凑起来,就能得到 $nxt_i$。但是有个问题,有可能 $A_i$ ~ $A_{i+k-1}$ 被漏掉了,那么解决方法也很简单,考虑 $A_{Nxt_i}$ ~ $A_{Nxt_{i+k-1}}$ 的时候,同时也考虑 $A_i$ ~ $A_{i+k-1}$ ,总之,先到先得即可。 单次询问的复杂度为 $O(log(nk)+k)$。 所以最终复杂度为 $O(nk+qlog(nk)+qk)$,空间复杂度为 $O(nk)$,~~好像和分散层叠算法的时间复杂度和空间复杂度一样?~~ 最后,放一下代码: ```cpp #include<algorithm> #include<iostream> #include<cstdio> #include<queue> #define INF 999999999 using namespace std; int k,n,q,d; int lastans,x; int a[105][20005]; int A[2000005]; int B[2000005]; int nxt[2000005]; //此处nxt[i]的含义为原文的A[Nxt[i]],主要是为了方便 int num[105]; //预处理nxt时用的临时数组 int rest[105]; //rest[i]表示每次询问的时候第i个原序列的后继是否已经找到 struct node{ int x,y,z; node(){} node(int _x,int _y,int _z): x(_x),y(_y),z(_z){} }now; bool operator <(node a,node b){ return a.x>b.x; } priority_queue<node>Q; int main(){ scanf("%d%d%d%d",&n,&k,&q,&d); for(int i=1;i<=k;a[i][n+1]=INF,i++)for(int j=1;j<=n;j++)scanf("%d",&a[i][j]); //在a[i][n+1]处放个极大值是为了避免等下归并的时候出现特殊情况 for(int i=1;i<=k;i++)Q.push(node(a[i][1],i,1)); for(int i=1;i<=n*k;i++){ now=Q.top();Q.pop(); A[i]=now.x,B[i]=now.y; Q.push(node(a[now.y][now.z+1],now.y,now.z+1)); } //这里的归并懒得用胜者树,直接用堆,但是用基排才是正确的复杂度,不过既然不会TEL,用堆更方便( for(int i=n*k;i;i--){ //注意倒序枚举i,因为求的是后继 num[B[i]]=A[i]; nxt[i]=num[i%k+1]; } for(int i=1;i<=q;i++){ scanf("%d",&x);x^=lastans; lastans=0; for(int j=1;j<=k;j++)rest[j]=0; //注意清零 int pos=lower_bound(A+1,A+n*k+1,x)-A; //lower_bound大法好! for(int j=pos;j<pos+k&&j<=n*k;j++){ if(!rest[B[j]])rest[B[j]]=A[j]; //考虑A[i]~A[i+k-1] if(!rest[j%k+1])rest[j%k+1]=nxt[j]; //考虑A[Nxt[i]]~A[Nxt[i+k-1]] } for(int j=1;j<=k;j++)lastans^=rest[j]; //计算答案 if(i%d==0)printf("%d\n",lastans); } return 0; } ``` 立个 flag,一定要学会分散层叠算法( (我学了半天就学会了划分树/kk)