算法学习笔记(29):分块

算法,笔记 · 浏览次数 : 10

小编点评

## 生成内容时带简单的排版 **1. 排版思想** * 首先预处理出块两两之间的众数,方便后续两侧枚举判断是否更加优秀。 * 然后对于散块中的数,可以额外再开一个桶记录其出现次数,方便后续两侧枚举判断是否更加优秀。 **2. 排版方法** * 1. 使用离散化后的值比较原始值的大小。 * 2. 使用排序后离散化的sort(app + 1, app + 1 + apc)的app + 1, app + 1 + apc方法进行比较。 **3. 代码示例** ```python # 离散化后的值比较原始值的大小 def most(i, j): return cnt(a[k], i, j) - cnt(most, i, j) # 排序后离散化的sort(app + 1, app + 1 + apc)的app + 1, app + 1 + apc方法进行比较 def find(i, j): return sorted(app + 1, app + 1 + apc)[i:j] # 统计块两两之间的众数 def cnt(a, i, j): return f[j][v] - f[(i) - 1][v] ``` **4. 排版复杂度** * 离散化后值比较原始值的大小: O(1) * 排序后离散化的sort(app + 1, app + 1 + apc)的app + 1, app + 1 + apc方法: O(log n) * 统计块两两之间的众数: O(1) * 统计块两两之间的众数: O(1) **5. 排版优劣** * 离散化后值比较原始值的大小: 最优 * 排序后离散化的sort(app + 1, app + 1 + apc)的app + 1, app + 1 + apc方法: 最优 * 统计块两两之间的众数: 最优 * 统计块两两之间的众数: 最优 **6. 总结** 生成内容时带简单的排版可以优化代码复杂度,提高效率。

正文

分块

这是一种基于根号的算法,核心为大块标记,散块暴力,做到复杂度的平衡。

可能第一个想到于此相关的就是莫队吧,这是利用分块优化暴力的方法。

Rmq Problem / mex

这本不是莫队的题……

  • 多次询问求区间的 \(\mathrm{mex}\)\(\mathrm{mex}\) 定义为没有出现过的最小自然数。

很显然,求 \(\mathrm{mex}\) 我们需要知道那些数是出现过的,这利用莫队很容易维护。

问题在于如何找到 \(\mathrm{mex}\),很 naive 的想法是在每一次加入一个值的时候动态的找到当前的 \(\mathrm{mex}\),但是这样的话如果构造数据使得它在 \(1\)\(n\) 之间来回跳,复杂度就炸了,这很不可取。

同样的,如果我们暴力找到 \(\mathrm{mex}\) 的话,那么同样也可以被卡成每次 \(O(n)\),复杂度也不可取。

于是考虑平衡查询的复杂度,做到 \(O(\sqrt n)\).

将值域每 \(B = \lfloor \sqrt n \rfloor\) 化作一块,显然的是,如果这一块内有 \(B\) 个不同的元素,那么 \(\mathrm{mex}\) 一定不在这个块内,所以每次加入/删除一个元素,我们只需要维护其所在的块不同的元素个数即可。

inline void add(int ap) {
    if (but[ap]++ == 0) ++bbut[ap / B];
}

inline void del(int ap) {
    if (--but[ap] == 0) --bbut[ap / B];
}

这就是加入/删除一个元素的函数,\(ap\) 表示这个元素,而其所在块的编号就是 \(\lfloor ap / B \rfloor\)

而查询的时候,我们枚举块,找到第一个不足 \(B\) 个元素的块,然后在暴力在块内找没有出现过的元素即可:

int k = 0;
while (bbut[k] == B) ++k; // all number exists in block k.
// searching in block k.
k = k * B + B;
for (int i = k - B; i < k; ++i) {
    if (but[i] == 0) mex = i, i = k; // i = k to break
}

由于只有 \(\lfloor n / B \rfloor\) 个块,每个块最多 \(B\) 个元素,所以查询一次的时间复杂度为 \(O(n / B + B)\),根据均值不等式,当 \(B = \sqrt n\) 时有最优复杂度 \(O(\sqrt n)\)

于是最终复杂度变为 \(O(n\sqrt n + m \sqrt n)\),可以过。


接下来我们看非常典的一个分块题吧(这里我们知道思路即可,实现在后面谈及)。

[国家集训队] 排队 - 洛谷

  • 动态维护逆序对的个数。

虽然有树套树的做法,然而如果我只是删除或者加入一个元素,那么树套树维护就不是很容易了。

所以分块,考虑加入一个数对于整个序列逆序对的影响:

  • 前面比他大的:在前面的块中二分求即可 \(O(\frac nB \log B)\)

  • 后面比他小的:在后面的块中二分求即可 \(O(\frac nB \log B)\)

  • 块内的,暴力求即可,删除和插入类似冒泡即可 \(O(B)\)

于是将 \(B = \sqrt {n \log n}\),单次修改复杂度便为 \(O(\sqrt {n \log n})\)

然而预处理是 \(O(n \log n)\) 的,于是总的复杂度为 \(O(n \log n + m \sqrt {n \log n})\),可以过。

接下来,我们就可以完成这道题了:CF1830E,也可以转化为动态维护逆序对的问题。


可能一上来的难度太高了,现在给一个简单题

[TJOI2009] 开关 - 洛谷

本题相当于区间异或 \(1\),区间求和,所以对于散块,暴力异或,对于整块,打上懒标记即可。

很显然,这可以利用线段树做到 \(O(n + m \log n)\),然而为了分块而分块,来分个块吧!

首先块长设为 \(B = \sqrt n\),于是可以 \(O(1)\) 求出每一个点对应的块:

这里默认为 1-index

#define Bk(i) (((i) - 1) / B + 1)

于是也可以很轻易的得出块所对应的左右端点 \([L_b, R_b]\)

#define BL(b) ((b) * B - B + 1)
#define BR(b) ((b) * B)

此时我们需要修改 \([l, r]\)

  • Bk(l) == Bk(r) 那么就可以暴力做:

    // cnt[b][0/1] 表示第 b 块内 0/1 的个数
    // rev[b] 是整块的懒标记
    // val[x] 是其原本的
    int b = Bk(l);
    for (int i = l; i <= r; ++i) {
        --cnt[b][val[i] ^ rev[i]];
        val[i] ^= 1;
        --cnt[b][val[i] ^ rev[i]];
    }
    
  • 否则,\(l\)\(r\) 所在块内暴力,中间打标记即可:

    int bl = Bk(l), br = Bk(r);
    for (int i = l; i <= BR(bl); ++i) ...
    for (int i = BL(br); i <= r; ++i) ...
    for (int k = bl + 1; k < br; ++k)
        rev[k] ^= 1, swap(cnt[k][0], cnt[k][1]);
    

修改部分如上,而查询也十分类似,分两种情况即可。


那么分块的思想有了,接下来就是板子的练习了。

LibreOJ 上有很多板子,从 数列分块入门 1数列分块入门 9 都是很好的练习题。


接下来我们稍稍上升,讲一讲 [Violet] 蒲公英 - 洛谷,一道很好的分块题:

[Violet] 蒲公英 - 洛谷

  • 在线求区间众数(也就是入门 9 的题)

假定我们已经分好了块,求区间 \([l, r]\) 的众数。

依据大块标记,散块暴力的思想,我们将他看作三个部分:

整块是多个 \(\sqrt n\) 的块!

显然的是,众数要么是整块的众数,要么出现在两侧的散块中,这提示我们可以先预处理出块两两之间的众数,这一共是 \(O(\sqrt n) \times O(\sqrt n) = O(n)\) 个,然后再两侧枚举判断是否更加优秀。

然而判断需要其出现的数量,于是我们需要处理出每一个数在块中出现的次数,做前缀和,然后差分即可求出在整块中的个数。而对于散块中的数,可以额外再开一个桶记录其出现的次数即可。

不过为了开这个桶,我们可能需要对于这些数离散化一下,变成 \(O(n)\) 个连续的值,不然根本开不下……

for (int i = 1; i <= n; ++i)
	++f[bk(i)][a[i]];
			
for (int b = 2; b <= be; ++b) {
	for (int i = 1; i <= n; ++i)
		f[b][i] += f[b - 1][i];
}

现在唯一的问题就剩下了如何预处理出每个整块的众数。

\(most_{i, j}\) 表示第 \(i\) 块到第 \(j\) 块的众数(\(i \le j\)),于是类似在查询时的思想,\(most_{i, j}\) 要么是 \(most_{i, j - 1}\),要么出现在第 \(j\) 块中。

#define cnt(v, i, j) (f[j][v] - f[(i) - 1][v])
for (int i = 1; i <= be; ++i) {
    for (int j = i; j <= be; ++j) {
		int most = g[i][j - 1];
		for (int k = BL(j), ke = BR(j); k <= ke; ++k)
			if (cnt(a[k], i, j) > cnt(most, i, j) || (cnt(a[k], i, j) == cnt(most, i, j) && a[k] < most))
				most = a[k];
		g[i][j] = most;
	}
}

于是枚举本块的所有元素与之对比即可,所以求一个 \(most_{i, j}\)\(O(\sqrt n)\) 的,故预处理部分是 \(O(n \sqrt n)\) 的。

之所以可以通过离散化后的值比较原本值的大小……排序后离散化的

sort(app + 1, app + 1 + apc);
int *ae = unique(app + 1, app + 1 + apc);
for (int i = 1; i <= n; ++i)
	a[i] = lower_bound(app + 1, ae, a[i]) - app;

小小总结

所以分块在什么时候用呢?

可能满足的条件如下:

  • \(n, m \le 10^5\)

  • 没有办法,或者很难用线段树维护(信息不满足区间加)。

  • 修改和查询的数量明显不同阶,需要 \(O(1) - O(\sqrt n)\) 的数据结构平衡复杂度。

  • ……

与算法学习笔记(29):分块相似的内容:

算法学习笔记(29):分块

分块 这是一种基于根号的算法,核心为大块标记,散块暴力,做到复杂度的平衡。 可能第一个想到于此相关的就是莫队吧,这是利用分块优化暴力的方法。 目录分块Rmq Problem / mex[国家集训队] 排队 - 洛谷[TJOI2009] 开关 - 洛谷[Violet] 蒲公英 - 洛谷小小总结 Rmq

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

算法学习笔记(4): 并查集及其优化

# 并查集 并查集,Disjoint-Set,或者通俗一点,叫做MergeFind-Set,是一种可以动态维护若干个不重叠的集合,并支持集合之间的合并与查询的数据结构。 集体来说,并查集支持下列两个操作: - Find,查询元素所属集合 - Merge,将两个元素所属集合合并 一般来说,为了具体实现