分块入门详解(比较全面)

最近写了几个分块,顺便做一下笔记。

什么是分块

分块是一种数据结构。。

有许多数据结构都是 \(\mathrm{log}\) 数据结构,比如线段树,树状数组等等。他们复杂度优秀,但是都是树形结构,有较大的思维难度和局限性。那么有没有什么复杂度一般,但是非树形的数据结构呢?

有的,就是分块。

分块的思路是把数据分成一个个的小块。设每块长为 \(\text{S}\)。举个栗子:

1, 1, 4, 5, 1, 4 (S = 2)

那么我们就可以把他分成

1, 1 | 4, 5 | 1, 4

我们成功把他分成了三个小块。

对于每个询问 \(\mathrm{[l, r]}\),它会跨越许多小块和整块。比如对于刚才那个数据:

1, 1 | 4, 5 | 1, 4 (l = 2, r = 5, S = 2)
   |          |
   l -------- r

l, r 跨越了第一块的后半个,第二块和第三块的前半个。

对于整块,我们一般可以用打懒标记之类的方法来搞,一共有 \(\dfrac{n}{S}\) 个整块,所以复杂度最坏也是 \(\mathrm{O(\dfrac{n}{S})}\)。对于散块来说,最多会有一左一右两个散块,长度最多为 \(\mathrm{2S}\),对他们进行暴力操作,复杂度最坏为 \(\mathrm{O(S)}\)。考虑常数,总复杂度为 \(\mathrm{O(2S + \dfrac{n}{S})}\)。(虽然 \(O\) 里面不应该加常数)。根据均值不等式,当 \(\mathrm{S = \dfrac{\sqrt{2}}{2} \sqrt{n}}\)。通常我们取 \(S = \sqrt{n}\),单次操作复杂度稳定在 \(\mathrm{O(\sqrt{n})}\) 级别。

分块怎么写

首先先说一下分块的几个重要操作:

  1. 定位操作。\(\mathrm{get(x)}\) 函数返回 \(x\) 所处的块的编号。可以这样写 int get(x) { return (int)x / S; }
  2. 查询端点。\(\mathrm{getl/getr}\),返回当前块的左、右端点。可以这样写(前提是你的 get 函数和我一样。)
int getl(int x) { return S * x + (x == 0); }
int getr(int x) { return min(n, (x + 1) * S - 1); }

数列分块入门1 举个栗子。

题意: 给定长度为 \(\mathrm{n}\) 的数列,进行 \(\mathrm{n}\) 次操作。操作涉及区间加法,单点查询。

这种题当然可以 线段树 / 树状数组,但是今天我们要用分块来写。按照上面我们说的。借鉴线段树的思路,我们可以维护一个 \(\mathrm{add}\) 懒标记,来维护当前块应该加上几。

现在假设我们修改的区间是 \(\mathrm{[l, r]}\),可能有下面几种情况。

  • 左右端点在同一个块里。我们遍历 \(\mathrm{l \sim r}\),暴力加即可。复杂度 \(\mathrm{O(\sqrt{n})}\)
  • 左右端点不在同一个块里。我们可以遍历中间的整块并打上懒标记。由于块数不超过 \(\mathrm{O(\sqrt{n})}\) 个,所以复杂度也不高。对于不在整块内的边角,直接暴力加就可以。

核心代码

int get(int x) { return x / S; } // 定位当前点在哪个块
void modify(int l, int r, int v) { // 修改操作
	int ls = get(l), rs = get(r);
	if (ls == rs) { for (int i = l; i <= r; i ++ ) w[i] += v; return; } // 在同一个整块内
	int i, j;
	for (i = l; get(i) == ls; i ++ ) w[i] += v; // 处理边角
	for (j = r; get(j) == rs; j -- ) w[j] += v;
	for (int k = get(i); k <= get(j); k ++ ) // 处理整块。
		add[k] += v;
}

int main() {
	.......

	if (query node) printf("%d\n", w[r] + add[get(r)]);
	else modify(l, r, v);
}

其他例题

数列分块入门7

题意: 给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间乘法,区间加法,单点询问。

可以对照线段树2来做。对每个块维护加、乘懒标记。注意:懒标记更新的时候注意顺序,对散块操作的时候要重构整个块。

代码示例

数列分块入门2

题意: 给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的元素个数。

分块常用的思想,就是在每个块内维护一个有序序列。加操作时可以打懒标记。查询操作可以直接块内二分。单次操作复杂度 \(\mathrm{O(\sqrt{n} \log n)}\)\(\log \sqrt{n}\)\(\log n\) 同阶)

代码示例

数列分块入门5

这道题带插入怎么办呢?块状链表隆重登场!!

块状链表可以看一下我写的博客 块状链表详解

剩下的就不讲了吧,再放几道例题。

例题

  1. 数列分块全系列

  2. 某些线段树题(比如洛谷线段树模板)

  3. 其他题目,如 Luogu 分数统计

以后可能还会加例题。先挖个坑。

posted @ 2023-01-18 20:54  Link-Cut-Y  阅读(123)  评论(0编辑  收藏  举报