根号算法学习笔记

codesonic

2018-03-12 18:08:48

Personal

## 分块 ### 简介 分块是一种根号数据结构 就是把一个序列划分成长度为$sqrt(n)$段处理 然后来处理问题 比如区间加和区间和问题就像线段树一样维护 ### 实现 例题:[教主的魔法](https://www.luogu.org/problemnew/show/P2801) #### block 我们先设block是每个块的大小,显然 ```cpp block=sqrt(n); ``` 然后设m是块的数量,显然可能有一个块不满block个 则 ```cpp m=n/block+!!(n%block); ``` 其中"!!"的作用显然是把非零数改为1 #### pos数组 为了不在使用时计算,我们再设一个数组pos,其中pos[i]表示i所在的块编号 显然对于任何一个点i ```cpp pos[i]=(i-1)/block+1; ``` 这样在调用时就方便了 #### tag数组 这个数组的作用和线段树一样 #### build函数 为了方便统计大于x的数的个数,我们可以维护每个块排序后的状态,因此可以设计一个build函数: ```cpp inline void build(int x){ int l=(x-1)*block+1,r=min(x*block,n); for(register int i=l;i<=r;i++) b[i]=a[i]; sort(b+l,b+r+1); } ``` 其中x是要维护的块编号,a数组是原序列,b数组是排序后的序列。 #### 修改 修改时,若一个块包含了要修改的区间,只要O(sqrt(n))对这个区间进行遍历即可 ```cpp if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) a[i]+=v;//对单个块进行操作 ``` 修改区间可能不仅包含整数个块,区间左右都有可能被放在某个块的中间,所以我们需要将这左右两个块分别O(sqrt(n))遍历修改一遍,则代码如下: ```cpp for(register int i=l;i<=pos[l]*block;i++) a[i]+=v;//对左边的零散块进行加 for(register int i=(pos[r]-1)*block+1;i<=r;i++) a[i]+=v;//对右边的块零散进行加 ``` 接着我们修改中间的整块,仅需要加tag ```cpp for(register int i=(pos[l]+1);i<pos[r];i++) tag[i]+=v;//对整块进行tag操作 ``` 我们发现经过修改后,修改区间的左右两个块不是有序的了,所以我们要在函数最后重新重构块 所以修改函数的代码为: ```cpp inline void modify(int l,int r,int v)//修改 { if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) a[i]+=v;//对单个块进行操作 else { for(register int i=l;i<=pos[l]*block;i++) a[i]+=v;//对左边的零散块进行加 for(register int i=(pos[r]-1)*block+1;i<=r;i++) a[i]+=v;//对右边的块零散进行加 for(register int i=(pos[l]+1);i<pos[r];i++) tag[i]+=v;//对整块进行tag操作 } build(pos[l]); build(pos[r]);//重构左右两个块 } ``` #### 询问函数 若一个块包含了要询问的区间,显然像刚才修改一样直接遍历 ```cpp if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) if(a[i]+tag[pos[i]]>=v) ans++;//单个块,直接O(sqrt(n))处理 ``` 还是按照修改的思路,对左右两个块直接遍历处理 ```cpp for(register int i=l;i<=pos[l]*block;i++ ) if(a[i]+tag[pos[i]]>=v) ans++; for(register int i=(pos[r]-1)*block+1;i<=r;i++) if(a[i]+tag[pos[i]]>=v) ans++; ``` 接着是对整块处理 ```cpp for(register int i=pos[l]+1;i<pos[r];i++) if(b[min(i*block,n)]>=v-tag[i])//因为b经过排序了 ans+=b+min(i*block,n)-lower_bound(b+(i-1)*block+1,b+min(i*block,n),v-tag[i])+1; ``` lower_bound函数是STL中的函数,用于求一个有序区间中能够插入把一个数插入的位置,详情[百度](baidu.com) 那么整个find函数如下: ```cpp inline int find(int l,int r,int v)//查询大于v个数 { register int ans=0; if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) ans+=a[i]+tag[pos[i]]>=v;//单个块,直接O(sqrt(n))处理 else { for(register int i=l;i<=pos[l]*block;i++ ) ans+=a[i]+tag[pos[i]]>=v;//零散块 for(register int i=(pos[r]-1)*block+1;i<=r;i++) ans+=a[i]+tag[pos[i]]>=v;//零散块 for(register int i=pos[l]+1;i<pos[r];i++) if(b[min(i*block,n)]>=v-tag[i])//因为b经过排序了 ans+=b+min(i*block,n)-lower_bound(b+(i-1)*block+1,b+min(i*block,n),v-tag[i]) + 1; } return ans; } ``` 接下来是整道题的代码 ```cpp #include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; const int maxn=1000010; int pos[maxn],a[maxn],b[maxn],tag[maxn];//a:原数组,b:保存每个块排序后的情况;pos:在哪个块内,tag:标签,其中block和tag可以只开sqrt(n) int block,m; int n,q; inline void build(int x){ int l=(x-1)*block+1,r=min(x*block,n); for(register int i=l;i<=r;i++) b[i]=a[i]; sort(b+l,b+r+1);//维护排序后的块 } inline void modify(int l,int r,int v)//修改 { if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) a[i]+=v;//对单个块进行操作 else { for(register int i=l;i<=pos[l]*block;i++) a[i]+=v;//对左边的零散块进行加 for(register int i=(pos[r]-1)*block+1;i<=r;i++) a[i]+=v;//对右边的块零散进行加 for(register int i=(pos[l]+1);i<pos[r];i++) tag[i]+=v;//对整块进行tag操作 } build(pos[l]); build(pos[r]);//重构左右两个块 } inline int find(int l,int r,int v)//查询大于v个数 { register int ans=0; if(pos[l]==pos[r]) for(register int i=l;i<=r;i++) ans+=a[i]+tag[pos[i]]>=v;//单个块,直接O(sqrt(n))处理 else { for(register int i=l;i<=pos[l]*block;i++ ) ans+=a[i]+tag[pos[i]]>=v;//零散块 for(register int i=(pos[r]-1)*block+1;i<=r;i++) ans+=a[i]+tag[pos[i]]>=v;//零散块 for(register int i=pos[l]+1;i<pos[r];i++) if(b[min(i*block,n)]>=v-tag[i])//因为b经过排序了 ans+=b+min(i*block,n)-lower_bound(b+(i-1)*block+1,b+min(i*block,n),v-tag[i]) + 1; } return ans; } char opt[3]; int main() { scanf("%d %d",&n,&q); block=sqrt(n);//每块的大小 for(register int i=1;i<=n;i++) { scanf("%d",&a[i]); pos[i]=(i-1)/block+1; } m=n/block+!!(n%block);//统计块数 for(register int i=1;i<=m;i++) build(i); while(q--) { scanf("%s",opt); int l,r,w; scanf("%d %d %d",&l,&r,&w); if(opt[0]=='M') modify(l,r,w); else printf("%d\n",find(l,r,w)); } return 0; } ``` 下一道例题是[弹飞绵羊](https://www.luogu.org/problemnew/show/P3203),一道显然是lct的题目,但是lct太难了所以我们用分块写 这一题首先还是把序列分成sqrt(n)段,然后处理即可 对于每一道分块的题目,都需要维护一些量,这些量可能是属于节点的,也可能是属于每个块的。 对于这道题,可以对每个节点维护两个变量,即还要跳几次出它所在的块和跳出块后到的节点 如何构造这两个量呢?因为是向右跳的,所以我们可以考虑递推的思想,从后往前构造: ```cpp for(int i=n;i>=1;i--){ to[i]=i+a[i]; if(to[i]>r[pos[i]]) far[i]=1; else { far[i]=far[to[i]]+1; to[i]=to[to[i]]; } } ``` 那么询问操作就很简单了,从前往后跳即可 ```cpp int ans=0; int x; scanf("%d",&x); x++; while(x<=n) { ans+=far[x]; x=to[x]; } printf("%d\n",ans); ``` 修改操作,我们同上题一样要重新build一遍,再次对该块构造一遍那两个变量即可 ``` int x,y; scanf("%d%d",&x,&y); x++; a[x]=y; for(int i=r[pos[x]];i>=l[pos[x]];i--){ to[i]=i+a[i]; if(to[i]>r[pos[i]]) far[i]=1; else { far[i]=far[to[i]]+1; to[i]=to[to[i]]; } }//重构 ``` 除此之外就没有什么了 ## 莫队 莫队是一个暴力的算法 我们考虑一个问题 > 给出一个序列,每次询问区间中有多少不同的数 我们直接的暴力做法是$O(mn)$的,直接暴力搜索。 但是我们来想另一种做法,标记两个数左指针和右指针,每次将指针挪一个位置再统计答案 提供大致代码: ```cpp int l=0,r=0; for(int i=1;i<=m;i++){ int ql,qr; scanf("%d%d",&ql,&qr); while(l<ql) del(l++); while(l>ql) add(--l); while(r<qr) add(++r); while(r>qr) del(r--); anss[q[i].id]=ans; } ``` 因为每次移动最多移动n格,所以复杂度还是$O(mn)$,还多了辅助空间而且常数较大。 我们尝试让让他每次移动的格子少一些,于是我们考虑先将左端点排序再跑 这样的复杂度确实是降低了一些,但是还是不够低 这样 标准的莫队只能处理离线问题 (当然有带修改莫队) 其思路是将询问的区间通过某种办法排序来加快速度,听起来好像很不可靠,但其实加速起来会快很多 例题:[HH的项链](https://www.luogu.org/problemnew/show/P1972) 代码如下: ```cpp #include<cstdio> #include<algorithm> #include<cmath> using namespace std; const int maxn=500010; struct query{ int l,r,ans,id; }q[maxn]; int ans; int anss[maxn]; int check[maxn]={0},color[maxn]={0}; int block; bool cmp(query a,query b){ return (a.l/block)==(b.l/block)?a.r<b.r:a.l<b.l; } inline void del(int o){ --check[color[o]]; if(!check[color[o]]) ans--; } inline void add(int o){ if(!check[color[o]]) ans++; ++check[color[o]]; } int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&color[i]); } block=sqrt(n); int m; scanf("%d",&m); for(int i=1;i<=m;i++){ scanf("%d%d",&q[i].l,&q[i].r); q[i].id=i; } sort(q+1,q+1+m,cmp); int l=0,r=0; for(int i=1;i<=m;i++){ int ql=q[i].l,qr=q[i].r; while(l<ql) del(l++); while(l>ql) add(--l); while(r<qr) add(++r); while(r>qr) del(r--); anss[q[i].id]=ans; } for(int i=1;i<=m;i++){ printf("%d\n",anss[i]); } return 0; } ``` 至于这个cmp函数为什么这么写,原因是尽量使每次移动得少,取平均最快的方法开sqrt(n) 可能是因为这样大家才说根号算法是毒瘤算法 复杂度是nsqrtm的