分块:优雅的暴力
分块是一种思想,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
分块的应用面十分广泛,包括但不限于数组、树形结构等。
1. 块状数组
块状数组是分块思想最简单的应用。
它将一个数组分成若干块,然后对数组进行区间操作。对于每一个区间操作,区间中的整块可以整体处理,左右的零散块就暴力单独处理。
一般来说,我们会将一个长度为
显然,块状数组的效率远远不及线段树、树状数组等
下面给出一种基础的建立块状数组的代码。这份代码对应下面的例题 1。
len = (int)sqrt(n); int last = 0; for(int i = 1; i <= n; i++) { cin >> a[i]; id[i] = (i - 1) / len + 1; b[id[i]].sum += a[i]; if(last != id[i]) { b[id[i - 1]].r = i - 1; b[id[i]].l = i; last = id[i]; } } b[id[n]].r = n;
例题 1 LibreOJ 6280. 数列分块入门 4
给定一个长度为
的序列 ,需要执行 次操作。操作分为两种:
区间
之间的所有数加上 ; 求
。
考虑对序列进行分块,块长为
对于修改操作,我们分为两种情况:
-
若
和 在同一个块内,直接暴力修改,单次复杂度为 。 -
若
和 不在同一个块内,则区间可分成三个部分:-
以
开头的不完整块; -
中间的若干完整块;
-
以
结尾的不完整块。
对于以
开头和 以 结尾的不完整块,直接暴力修改,复杂度为 ;对于中间的若干完整块,则修改这些完整块的整体标记
,复杂度为 ,则单次修改的复杂度为 。 -
对于查询操作,我们同样分为两种情况:
-
若
和 在同一个块内,直接暴力求和,并加上修改标记,单次复杂度为 。 -
若
和 不在同一个块内,则区间可分成三个部分:-
以
开头的不完整块; -
中间的若干完整块;
-
以
结尾的不完整块。
对于以
开头和 以 结尾的不完整块,直接暴力求和,并加上修改标记 ,复杂度为 ;对于中间的若干完整块,则将这些完整块记录的区间和
求和,加上修改标记 ,复杂度为 ,则单次查询的复杂度为 。 -
综上,我们即可在
Code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 5; int n, len, a[N], id[N]; struct node { int l, r, sum, tag; }b[N]; void update(int l, int r, int w) { int x = id[l], y = id[r]; if(x == y) { for(int i = l; i <= r; i++) a[i] += w; b[x].sum += (r - l + 1) * w; return; } for(int i = l; i <= b[x].r; i++) a[i] += w; b[x].sum += (b[x].r - l + 1) * w; for(int i = b[y].l; i <= r; i++) a[i] += w; b[y].sum += (r - b[y].l + 1) * w; for(int i = x + 1; i < y; i++) b[i].tag += w; return; } int query(int l, int r, int c) { int x = id[l], y = id[r], ans = 0; if(x == y) { for(int i = l; i <= r; i++) ans = (ans + a[i] + b[x].tag) % (c + 1); return ans; } for(int i = l; i <= b[x].r; i++) ans = (ans + a[i] + b[x].tag) % (c + 1); for(int i = b[y].l; i <= r; i++) ans = (ans + a[i] + b[y].tag) % (c + 1); for(int i = x + 1; i < y; i++) ans = (ans + b[i].sum + b[i].tag * (b[i].r - b[i].l + 1)) % (c + 1); return ans; } signed main() { cin >> n; len = (int)sqrt(n); int last = 0; for(int i = 1; i <= n; i++) { cin >> a[i]; id[i] = (i - 1) / len + 1; b[id[i]].sum += a[i]; if(last != id[i]) { b[id[i - 1]].r = i - 1; b[id[i]].l = i; last = id[i]; } } b[id[n]].r = n; for(int i = 1; i <= n; i++) { int opt, l, r, w; cin >> opt >> l >> r >> w; if(opt == 0) update(l, r, w); else cout << query(l, r, w) << "\n"; } return 0; }
例题 2 洛谷P2801 教主的魔法
给定一个长度为
的序列 ,需要执行 次操作。操作分为两种:
区间
之间的所有数加上 ; 查询区间
内大于等于 的数的个数。
, 。
这道题比上面那道题稍难一些。
对于每一个块,我们可以将块内的元素降序排序,单独存至另一个数组
对于查询,分为两种情况:
-
和 在同一个块中,直接暴力统计,复杂度为 。 -
和 不在同一个块中。-
对于散块,直接暴力查询,复杂度为
; -
对于整块,可以对
进行二分,找到 在块中从大到小的排名,即为块中大于等于 的数的个数,累加起来即可。复杂度为 。
那么对于查询操作,总复杂度为
。 -
对于修改,同样分为两种情况
-
和 在同一个块中,直接暴力修改,然后对 进行重构。复杂度为 。 -
和 不在同一个块中。-
对于散块,暴力修改,并对
进行重构,复杂度为 。 -
对于整块,直接修改标记
即可。复杂度为 。
那么修改的复杂度即为
。 -
综上,我们即可在
Code
#include<bits/stdc++.h> using namespace std; const int N = 1e6 + 5; int n, m, len, a[N], t[N], id[N]; struct node { int l, r, tag; }b[N]; void Sort(int x) { for(int i = b[x].l; i <= b[x].r; i++) t[i] = a[i]; sort(t + b[x].l, t + 1 + b[x].r); return; } void update(int l, int r, int w) { int x = id[l], y = id[r]; if(x == y) { for(int i = l; i <= r; i++) a[i] += w; Sort(x); return; } for(int i = l; i <= b[x].r; i++) a[i] += w; for(int i = b[y].l; i <= r; i++) a[i] += w; for(int i = x + 1; i < y; i++) b[i].tag += w; Sort(x), Sort(y); return; } int query(int l, int r, int c) { int ans = 0, x = id[l], y = id[r]; if(x == y) { for(int i = l; i <= r; i++) if(a[i] + b[x].tag >= c) ans++; return ans; } for(int i = l; i <= b[x].r; i++) if(a[i] + b[x].tag >= c) ans++; for(int i = b[y].l; i <= r; i++) if(a[i] + b[y].tag >= c) ans++; for(int i = x + 1; i < y; i++) ans += b[i].r - (lower_bound(t + b[i].l, t + 1 + b[i].r, c - b[i].tag) - t - 1); return ans; } int main() { cin >> n >> m; len = (int)sqrt(n); int last = 0; for(int i = 1; i <= n; i++) { cin >> a[i]; id[i] = (i - 1) / len + 1; if(last != id[i]) { b[id[i - 1]].r = i - 1; b[id[i]].l = i; last = id[i]; } } b[id[n]].r = n; for(int i = len; i <= n; i += len) Sort(id[i]); for(int i = 1; i <= m; i++) { char c; int l, r, w; cin >> c >> l >> r >> w; if(c == 'M') update(l, r, w); else cout << query(l, r, w) << "\n"; } return 0; }
以上便是块状数组最简单的应用了。感兴趣的同学可以根据下面的题单进一步加强。
2. 块状链表
由于块状链表使用的并不多,所以笔者在这里只稍微提一下。
块状链表就是将一个数组分块成若干数组,然后用一个链表存起来,每个结点指向一块。
一般来说,这里的分块块长也是
这里放出 OI Wiki 上的一张图。
块状链表支持:分裂、插入、查找。复杂度均为
3. 莫队算法
莫队算法是由莫涛提出的算法。莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
本文作者:Luckies
本文链接:https://www.cnblogs.com/Luckies/p/18322769/block
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步