学习笔记-分块
分块——优雅的暴力。
分块永远是偏解。 ——@ny_fzx
但是,在面对一些区间问题的时候,不涉及SegTree、LCT等高级数据结构,编码简单的分块能拿到相当可观的分数。
根号分块
顾名思义,这是把长度为 $n$ 的数组分成 $\sqrt{n}$ 个区间。
核心思想:对于一个区间,将其不包含在整块的范围内直接暴力处理,这一段不超过 $\sqrt{n}$ 。而中间被整体包含的块就统一处理,也不会超过 $\sqrt n$ 个块。
因此整体时间为 $O(\sqrt{n})$ 。取 $\sqrt{n}$ 作块长的目的是为了平衡整块操作和非整块的暴力操作的时间。
经典例题:
NKOJ 2297
给出一列数{Ai}(1≤i≤n),总共有m次操作,操作分如下两种:
- ADD x y z 将x到y区间的所有数字加上z
- ASK x y 将x到y区间的最大一个数字输出。
我们沿袭上面的思想,在做加法的时候也将其不包含在整块的范围内直接暴力加上,整块的,类似于线段树,开一个 Lazy-tag 处理。
区间最大值呢?我们也可以使用 Lazy-tag 。因为对于整块的修改并不会影响块内最大值是哪个,在最后查询的时候直接使用最大值加上这块的总增量。
但是对于单点查询就不一样了。单点查询有可能会影响块内的最大值是哪个,因此直接对于这个块重新暴力找一遍 max ,复杂度也是 $O(\sqrt{n})$ 。
参考核心代码:
void modify(int x, int y, int val) { int k1 = (x - 1) / s + 1, k2 = (y - 1) / s + 1; int ans = INT_MIN; if (k1 == k2) { for (int i = x; i <= y; i++) { a[i] += val; } block[k1] = INT_MIN; for (int i = (k1 - 1) * s + 1; i <= s * k1; i++) { block[k1] = max(block[k1], a[i]); } } else { for (int i = x; i <= k1 * s; i++) { a[i] += val; } block[k1] = INT_MIN; for (int i = (k1 - 1) * s + 1; i <= s * k1; i++) { block[k1] = max(block[k1], a[i]); } for (int i = (k2 - 1) * s + 1; i <= y; i++) { a[i] += val; } block[k2] = INT_MIN; for (int i = (k2 - 1) * s + 1; i <= s * k2; i++) { block[k2] = max(block[k2], a[i]); } for (int i = k1 + 1; i < k2; i++) { lazy[i] += val; } } }
int query(int x, int y) { int k1 = (x - 1) / s + 1, k2 = (y - 1) / s + 1; int ans = INT_MIN; if (k1 == k2) { for (int i = x; i <= y; i++) { ans = max(ans, a[i] + lazy[k1]); } return ans; } else { for (int i = x; i <= k1 * s; i++) { ans = max(ans, a[i] + lazy[k1]); } for (int i = (k2 - 1) * s + 1; i <= y; i++) { ans = max(ans, a[i] + lazy[k2]); } for (int i = k1 + 1; i < k2; i++) { ans = max(ans, block[i] + lazy[i]); } return ans; } return -1; }
来看看稍有复杂的Lazy-tag处理。
NKOJ 8240
给出一个长为 n 的数列,以及 n 个操作,操作有区间开方,区间求和。
区间开方?这听起来不是 Lazy_tag 能做到的事情。
但是!在经过几次开方之后,该区间的所有数都会变成1,此时再开方就没有意义了。
经过估算,最多 7 次。
因此,Lazy_tag 可以记录区间开方的次数,如果 >=7 ,就没必要再开了,直接跳过。否则暴力。
参考代码:略。
NKOJ 8240
给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。
这道题只有区间加法,很好处理。但是它的询问很奇怪。
求前驱当然不能开 Lazy。所以我们考虑把区间排序,这样便可以在 $\log n$ 的时间内求出来。
当进行非区间的暴力修改时需要重新排序。
参考代码:略。
NKOJ 8242
给出一个长为 n 的数列,以及 n 个操作,操作有区间乘法,区间加法,单点询问。
区间乘法的处理原理和区间加法是一样的。但是要注意的是区间乘法与区间加法的关系。
在更新乘法的 Lazy_tag 时,也得顺带把加法的 Lazy 也乘上。当暴力修改时,一定记得将当前块的 Lazy_tag 下放。注意:因为加法的Lazy也做了乘法,所以要先乘后加。
NKOJ 8242
给出一个长为 n 的数列,以及 n 个操作,操作有单点插入,单点询问,数据随机生成。
单点插入意味着这事没有那么简单。
如果直接暴力插入,数组后面的所有元素都要往后推一位,显然时间不能承受。
考虑用 vector 来分块,这样插入的时间复杂度就变成了 $\sqrt n$ ,查询也是$\sqrt n$。
但是考虑极限情况。如果每次查询都向同一个 vector 中插入,那么最后时间复杂度会趋近于 $O(n)$ 。
所以当某个 vector 的 size 过大的时候,我们需要重新分块,也就是俗称的拍平。
为 size 设置一个阈值,大概在 $8 \sqrt n$。
参考代码:
void rebuild() { int tmp = 0; for (int i = 1; i <= cntblock; i++) { for (auto &j : block[i]) a[++tmp] = j; } for (int i = 1; i <= cntblock; i++) block[i].clear(); blocklen = sqrt(tmp); for (int i = 1; i <= tmp; i++) block[(i - 1) / blocklen + 1].push_back(a[i]); cntblock = (tmp - 1) / blocklen + 1; } void add(int pos, int val) { int now = 0, id = 0; while (1) { id++; if (now + block[id].size() >= pos) break; else now += block[id].size(); } block[id].insert(block[id].begin() + pos - now - 1, val); if (block[id].size() > 8 * blocklen) rebuild(); } int query(int pos) { int now = 0, id = 0; while (1) { id++; if (now + block[id].size() >= pos) break; else now += block[id].size(); } return block[id][pos - now - 1]; }
总之,分块的关键在于 区间如何统一处理。
本文作者:aaaaaaqqqqqq
本文链接:https://www.cnblogs.com/aaaaaaqqqqqq/p/17976967
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步