分块算法笔记

> 啥是分块? 

  简单的说, 就是给你一段长度为 n 序列, 把它分成 x 叉树的形式(每层有 x 个块), 可以分成 logx(n) 层; 

  如果采用 sqrt(n) 叉树, 就有 2 层; 如果采用 2 叉树, 就有 logn 层; 

  第一个是分块, 第二个是线段树. 

 

> 为什么要分块? 

  有 x 次操作, 每次操作复杂度为 O(a), 有 y 次询问, 每次询问复杂度为 O(b), 我们就可以通过提高一边的复杂度来降低另一边的复杂度, 使整体复杂度平衡, 以提升整个算法的效率. 

  举个例子, 给出一个序列, 进行 m 次操作: 修改某个位置的值, 查询某段区间的和. 

  如果直接做, 修改是 O(1) 的复杂度, 查询是 O(n) 的, 整体复杂度就是 O(m*n), 但我们利用线段树平衡两边的复杂度, 使修改与查询都为 O(logn), 整个算法的复杂度就是 O(mlogn) 了. 

  分块也同理, 平衡两边复杂度为 O(sqrt(n)), 这个过程称作根号平衡, 所以根号算法的复杂度就是 O(m*sqrt(n)). 

 

> 分块解决什么问题? 

  例1: 维护一个长度为 n 的序列, 进行 m 次操作: 区间加, 查询某段区间内小于 x 的数的个数. 

  如果是单点修改, 我们可以用树套树来实现, 但区间修改树套树无法快速合并信息. 

  如果信息可以快速合并, 则优先选择线段树结构, 毕竟 logn 是优于 sqrtn 的; 而对于无法快速合并信息的情况, 就可以考虑用分块实现. 

  这道题, 分块并将每块排序. 修改时, 整块打标记, 零散块枚举 + 重构(重构用归并); 查询时, 整块查询小于 x 的数, 这个整块的标记为 y(也就是说这一块所有数都加了y)则等价于查整块的排序后的数组里面小于 x - y 的数的个数, 查找可以二分, 零散块就直接暴力查询块内在查询区间内的数是否满足条件. 

  

  例2: 维护一个长度为 n 的序列, 进行 m 次操作: 区间加, 查询某段区间内第 k 小的数. 

  解法与例1类似. 查询第 k 小的数时, 需要二分答案, 然后在每个整块上用 lower_bound()求有多少个数比二分出的这个答案小, 在零散块上枚举, 累加起来, 看是不是等于 k-1. So easy? 

  诶, 等等, 每二分一个答案, 就枚举一遍零散块, 这样不是很慢么? 这样复杂度 O(logn * sqrtn)的呀. 

  想办法优化这个复杂度. 我们可以把零散块归并成一个假的整块, 在这个整块上 lower_bound(), 这样复杂度就是 O(sqrtn + logn * log(sqrtn))了. 

 

  例3: 维护一个序列, 支持 O(1)单点修改, O(sqrt(n))区间和. 

  分块维护序列. 直接修改原数组和所在的块, 查询整块 O(1), 零散块 O(sqrt(n))枚举. 

  例4: 维护一个序列, 支持 O(sqrt(n))单点修改, O(1)区间和. 

  分块维护前缀和. 维护块内序列的前缀和, 维护块的前缀和. 修改当前块内前缀和块的前缀和, 查询整块的块前缀和与零散块的块内前缀和

  

  例5: 维护一个序列, 支持 O(sqrt(n))区间加, O(1)查单点.

  分块维护序列. O(1)加整块, O(sqrt(n))枚举加零散块, O(1)查询. 

  例6: 维护一个序列, 支持 O(1)区间加, O(sqrt(n))查单点.

  差分. 把 a[i]变为 a[i] - a[i-1], 区间加相当于修改单点, 查询单点相当于求前缀和, 转变成例5.

 

  例7: 维护一个集合, 支持 O(1)插入一个数, O(sqrt(n))查询第 k 小. 

  离散化后分块维护值域. a[i]表示数字 i 出现的次数, 维护每个块内有多少个数. 查询的时候从第一个块开始往右跑, 最多走过 sqrt(n)个整块和 sqrt(n)个零散的数. 

  例8: 维护一个集合, 支持 O(sqrt(n))插入一个数, O(1)查询第 k 小. 

  这个题比较有意思, 可以先想一想再看. 

  考虑到要 O(1)查询, 可见肯定要能直接定位到第 k 小的数的位置. 

  先离线读入所有的操作, 把插入的数放在一个序列里面, 将序列排序后分块. 

  用 pos[i]记录元素 i 在哪个块中, 如果 i 有多个则记录第一个. 

  用 L[i]和 R[i]分别记录第 i 个块的左右端点, 即第 i 个块里面是排名 L[i] ~ R[i]的数. 

  用 belong[i]表示排名第 i 的数在哪个块里. 

  每插入一个数 i, 就把它放到 pos[i]这个块的尾部, O(sqrt(n))维护一下块内的大小顺序, O(sqrt(n))维护一下后面块的左右端点(因为往后移了一位). 

  询问时, 输出 Block[belong[k]][k - L[belong[k]]]. 

 

  更多题目参见标签 "分块", "题解", 不断更新. 

 

posted @ 2018-02-13 23:37  Milky-Way  阅读(1308)  评论(1编辑  收藏  举报