发现自己并没有掌握分块的正确姿势,然后于近期学习一下。
顺便提一下,分块是一个很好想,很暴力,复杂度较直接暴力比较低的算法
如果数据是随机数据那么实际复杂度会比期望要小。
然后最优分块的数目,根据不同的题目是不同的,可以通过均值不等式算出
但是大多数数据结构题目直接对数列进行$\sqrt{n}$的数列分块就可以了。
所以在下面的讨论中,我们假定所有数列分块按照最经典的$\sqrt{n}$的分块方式。
首先我们定义一些分块中普遍需要用到的东西即tr[1..num]结构体数组,其中全局变量num指的是分块的数目。
还有全局变量,block表示每一块元素的个数(除了最后一块)。
其中tr数组的类型div,含有三个元素l,r,val 分别表示块维护原数组左边界、右边界、维护该块的信息(可以适当添加描述这个块信息的参数,便于处理量询问)
首先,如果我们默认$\sqrt{n}$个元素作为一块,那么显然$block=\sqrt{n}$
如果$n$可以整除$block$,那么$num=\left \lfloor \frac{n}{block} \right \rfloor (block|n)$,否则$num=\left \lfloor \frac{n}{block} \right \rfloor+1 (block|n不成立)$
我们需要知道一个块处理是那个区间的信息,即tr[i].l,tr[i].r
那么还是给出公式: $tr_i.l=(i-1)\times block+1,tr_i.r=i\times block$ *特别的$tr_{num}.r=n$
然后我们还可能需要一个反映射,即原来数组中的哪个元素属于那个块。
那么我们可以修正一个公式$belong_i=\left \lfloor \frac{i-1}{block} \right \rfloor+1$
然后你可以预处理一些东西来维护这个块。
下面是预处理代码,
void build() { block=sqrt(n); num=n/block; if (n%block) num++; for (int i=1;i<=num;i++) tr[i].l=(i-1)*block+1, tr[i].r=i*block, tr[i].val=0; tr[num].r=n; for (int i=1;i<=n;i++) blong[i]=(i-1)/block+1; }
我们可以试着完成下面一个基础练习,来检查我们上述的建块代码是否正确。
#6277. 数列分块入门 1 给出一个长为 n 的数列,以及 n个操作,操作涉及区间加法,单点查值。
这个题目在每个块里面统计这个块的加法标记。
然后采取加法标记的可加性,累加整块的加法标记tr[i].val
然后对于剩余的部分,我们建立tag[x]数组表示第x位置多加了多少次(除了块中加的次数)
显然,由于一个点最终的值一定是在块内加的和块外加的代数和,保证不会重复。
#include <bits/stdc++.h> using namespace std; const int N = 5e4 + 10; struct rec { int l, r, val; } tr[N]; int a[N], n, tag[N], block, num, blong[N]; inline int read() { int X = 0, w = 0; char c = 0; while (c < '0' || c > '9') { w |= c == '-'; c = getchar(); } while (c >= '0' && c <= '9') X = (X << 3) + (X << 1) + (c ^ 48), c = getchar(); return w ? -X : X; } inline void write(int x) { if (x < 0) putchar('-'), x = -x; if (x > 9) write(x / 10); putchar('0' + x % 10); } void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) tr[i].l = (i - 1) * block + 1, tr[i].r = i * block, tr[i].val = 0; tr[num].r = n; for (int i = 1; i <= n; i++) blong[i] = (i - 1) / block + 1; } void update(int opl, int opr, int d) { // tag[i] //记录元素i被额外被+多少 if (blong[opl] == blong[opr]) { for (int i = opl; i <= opr; i++) tag[i] += d; return; } for (int i = opl; i <= tr[blong[opl]].r; i++) tag[i] += d; for (int i = tr[blong[opr]].l; i <= opr; i++) tag[i] += d; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) tr[i].val += d; } int query(int pos) { return a[pos] + tr[blong[pos]].val + tag[pos]; } int main() { n = read(); for (int i = 1; i <= n; i++) a[i] = read(); build(); for (int i = 1; i <= n; i++) { int opt = read(), l = read(), r = read(), c = read(); if (opt == 0) update(l, r, c); else write(query(r)), putchar('\n'); } return 0; }
对于例题1的分析中我们发现,对于一个元素,保证整个块对其贡献和块外对其贡献不重复不遗漏。
然后在统计时,就可以方便快捷的处理总贡献了。
对于本题和以下题目,我们会单独分析每到题目的时间复杂度,以确保在极限数据条件下,分块算法的时间复杂度也是适用的。
对于本题,显然,时间复杂度为$O(n \sqrt{n}) $
上述解决了,区间修改单点查询,用差分数组+树状数组可以在$O(n log_2 n)$复杂度内解决本问题。
借鉴第一道例题的说明思路我们给出以下8道题目的解题模板。
0.给出一句话题面和对应链接
1. 给出算法思想
2.给出程序实现
3.给出分块时间复杂度分析
4.给出一个更优秀的数据结构思想和复杂度(不作具体实现)
说明:8道题目来自loj的分块入门专题练习
有了上述说明,我们接着看例题2:
#loj6278. 数列分块入门 2 给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的元素个数。
算法思想:考虑分块解决,对于一个对应的块,我们希望他有序,这样就可以方便处理答案了。
显然,对于初始的块我们可以对每一块中的元素暴力排序,复杂度$O(n log_2 \sqrt{n} )$
对于更新操作,块内的数由于加上同一个数d,那么其大小顺序是不会发生变化的,所以直接更新对应块的加法标记tr[x].val即可
但是对于块外的数的加法,其在块内的排序可能发生变化,所以我们对块外元素所在的整一个块进行排序,复杂度$O(\sqrt{n} log_2 \sqrt{n})$,
显然块外元素的加法是不能更改对应整个块的加法标记的,所以我们像例题1一样,设立tag[x]表示x元素被额外加了多少。
对于查询操作,对于块外的部分可以直接暴力计算,其总增量和limit值的大小,暴力统计; 然后对于块内的部分则可以使用二分查找来询问小于limit的数量,复杂度$O(\sqrt{n} log_2 \sqrt{n})$
综上所述,若全部是更新操作,且是最劣情况,复杂度是$O(n \sqrt{n} log_2 \sqrt{n})$ , 可以卡过(请加上IO优化)
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 10; struct rec { int l, r, val; } tr[N]; int tag[N], block, num, n, blong[N]; vector<int> v[10000]; inline int read() { int X = 0, w = 0; char c = 0; while (c < '0' || c > '9') { w |= c == '-'; c = getchar(); } while (c >= '0' && c <= '9') X = (X << 3) + (X << 1) + (c ^ 48), c = getchar(); return w ? -X : X; } inline void write(int x) { if (x < 0) putchar('-'), x = -x; if (x > 9) write(x / 10); putchar('0' + x % 10); } void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) tr[i].l = (i - 1) * block + 1, tr[i].r = i * block, tr[i].val = 0; for (int i = 1; i <= n; i++) blong[i] = (i - 1) / block + 1, v[blong[i]].push_back(tag[i]); for (int i = 1; i <= num; i++) sort(v[i].begin(), v[i].end()); } void reset(int x) { v[x].clear(); for (int i = tr[x].l; i <= tr[x].r; i++) v[x].push_back(tag[i]); sort(v[x].begin(), v[x].end()); } void update(int opl, int opr, int d) { if (blong[opl] == blong[opr]) { for (int i = opl; i <= opr; i++) tag[i] += d; reset(blong[opl]); return; } for (int i = opl; i <= tr[blong[opl]].r; i++) tag[i] += d; reset(blong[opl]); for (int i = tr[blong[opr]].l; i <= opr; i++) tag[i] += d; reset(blong[opr]); for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) tr[i].val += d; } int query(int opl, int opr, int limit) { if (blong[opl] == blong[opr]) { int ret = 0; for (int i = opl; i <= opr; i++) if (tag[i] + tr[blong[opl]].val < limit) ret++; return ret; } int ret = 0; for (int i = opl; i <= tr[blong[opl]].r; i++) if (tag[i] + tr[blong[opl]].val < limit) ret++; for (int i = tr[blong[opr]].l; i <= opr; i++) if (tag[i] + tr[blong[opr]].val < limit) ret++; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) ret += lower_bound(v[i].begin(), v[i].end(), limit - tr[i].val) - v[i].begin(); return ret; } signed main() { n = read(); for (int i = 1; i <= n; i++) tag[i] = read(); build(); for (int i = 1; i <= n; i++) { int opt = read(), l = read(), r = read(), c = read(); if (opt == 0) update(l, r, c); else write(query(l, r, c * c)), putchar('\n'); } return 0; }
事实上本例题使用线段树套平衡树可以解决,复杂度可能会到达$O(n {log_2}^ 3 n)$
接着来看例题3:
#6279. 数列分块入门 3 给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。
这道例题和例题2本质是一样的,也可以采用相同的方法计算,这里不作赘述,请同学注意边界问题的处理,没有情况的处理
这里仅给出代码:
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 10; struct rec { int l, r, val; } tr[N]; int tag[N], block, num, n, blong[N]; vector<int> v[10000]; inline int read() { int X = 0, w = 0; char c = 0; while (c < '0' || c > '9') { w |= c == '-'; c = getchar(); } while (c >= '0' && c <= '9') X = (X << 3) + (X << 1) + (c ^ 48), c = getchar(); return w ? -X : X; } inline void write(int x) { if (x < 0) putchar('-'), x = -x; if (x > 9) write(x / 10); putchar('0' + x % 10); } void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) tr[i].l = (i - 1) * block + 1, tr[i].r = i * block, tr[i].val = 0; for (int i = 1; i <= n; i++) blong[i] = (i - 1) / block + 1, v[blong[i]].push_back(tag[i]); for (int i = 1; i <= num; i++) sort(v[i].begin(), v[i].end()); } void reset(int x) { v[x].clear(); for (int i = tr[x].l; i <= tr[x].r; i++) v[x].push_back(tag[i]); sort(v[x].begin(), v[x].end()); } void update(int opl, int opr, int d) { if (blong[opl] == blong[opr]) { for (int i = opl; i <= opr; i++) tag[i] += d; reset(blong[opl]); return; } for (int i = opl; i <= tr[blong[opl]].r; i++) tag[i] += d; reset(blong[opl]); for (int i = tr[blong[opr]].l; i <= opr; i++) tag[i] += d; reset(blong[opr]); for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) tr[i].val += d; } int query(int opl, int opr, int limit) { if (blong[opl] == blong[opr]) { int ret = -1; for (int i = opl; i <= opr; i++) if (tag[i] + tr[blong[opl]].val < limit) ret = max(ret, tag[i] + tr[blong[opl]].val); return ret; } int ret = -1; for (int i = opl; i <= tr[blong[opl]].r; i++) if (tag[i] + tr[blong[opl]].val < limit) ret = max(ret, tag[i] + tr[blong[opl]].val); for (int i = tr[blong[opr]].l; i <= opr; i++) if (tag[i] + tr[blong[opr]].val < limit) ret = max(ret, tag[i] + tr[blong[opr]].val); for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) { int p = lower_bound(v[i].begin(), v[i].end(), limit - tr[i].val) - v[i].begin(); if (p == 0) continue; ret = max(ret, v[i][p - 1] + tr[i].val); } return ret; } signed main() { n = read(); for (int i = 1; i <= n; i++) tag[i] = read(); build(); for (int i = 1; i <= n; i++) { int opt = read(), l = read(), r = read(), c = read(); if (opt == 0) update(l, r, c); else write(query(l, r, c)), putchar('\n'); } return 0; }
上述的三个例题部分是有需求进行区间操作的,但是没有那么显然。
对于线段树经典题目——区间修改区间求和,分块也可以解决,我们来看例题4
#6280. 数列分块入门 4 给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,区间求和。
这就是比较经典的题目了。涉及区间修改和区间求和。
我们把分块算法和线段树算法作一个类比,其实线段树对整个线段的分块是很多的,由于是二分分块下去,所以我们可以任意访问任意一个区间。
然而分块算法,仅仅只分了$\sqrt{n}$个大块,并没有对块内继续细分,所以我们无法把线段分成最小单位为1的区间,保证需求的每个区间都可以直接更行,所有是存在块内和块外的问题。我们一般的解决方式是:块内记录(增量),块外暴力维护。
所以一般的,我们仍然还需要记录全局的维护原来项目的一个数组,然后在块外更新可能会改变内容的时候,直接维护掉这一个块就行。
下面的题目可能会使用这个思想。
然而对于这道题目我们的思路就比较显然了,由于要区间查询,我们不妨维护一个块内元素和,然后维护一个块的加法标记(即add[x]表示块x被整体加过多少)。
对于整块的元素,直接维护加法标记,使用数学知识O(1)维护区间和。
我们注意到,块外元素的统计较为复杂,不能直接更新加法标记add,
所以我们只能新增一个数组tag[x]表示第x个位置被额外加过几次(不包含在整块加的)。
那么我们统计一个区间和的时候,在块内的元素直接累加块总和,块外元素i,直接累加tag[i]+add[i所在的块编号];
这样可以保证更新对元素产生的贡献被完全不重复的记录和更新,同时完成本题的维护。
复杂度显然是$O(n \sqrt{n})$
本题可以使用线段树的经典算法,复杂度更优秀,为$O(n log_2 n)$
/* 维护每个块的和,tag维护每个元素多加了多少 add[x]块x被累加的总和 每一个块中的和保证是最新的。 */ #include <bits/stdc++.h> #define int long long const int N = 5e4 + 10; using namespace std; struct rec { int l, r, val; } tr[N]; int add[N], tag[N], blong[N]; int block, num, n; inline int read() { int X = 0, w = 0; char c = 0; while (c < '0' || c > '9') { w |= c == '-'; c = getchar(); } while (c >= '0' && c <= '9') X = (X << 3) + (X << 1) + (c ^ 48), c = getchar(); return w ? -X : X; } inline void write(int x) { if (x < 0) putchar('-'), x = -x; if (x > 9) write(x / 10); putchar('0' + x % 10); } void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) tr[i].l = (i - 1) * block + 1, tr[i].r = i * block, tr[i].val = 0; tr[num].r = n; for (int i = 1; i <= n; i++) blong[i] = (i - 1) / block + 1, tr[blong[i]].val += tag[i]; } void update(int opl, int opr, int d) { if (blong[opl] == blong[opr]) { tr[blong[opl]].val += (opr - opl + 1) * d; for (int i = opl; i <= opr; i++) tag[i] += d; return; } tr[blong[opl]].val += (tr[blong[opl]].r - opl + 1) * d; tr[blong[opr]].val += (opr - tr[blong[opr]].l + 1) * d; for (int i = opl; i <= tr[blong[opl]].r; i++) tag[i] += d; for (int i = tr[blong[opr]].l; i <= opr; i++) tag[i] += d; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) { tr[i].val += (tr[i].r - tr[i].l + 1) * d; add[i] += d; } } int query(int opl, int opr, int mo) { if (blong[opl] == blong[opr]) { int ret = (opr - opl + 1) * add[blong[opl]] % mo; for (int i = opl; i <= opr; i++) ret = (ret + tag[i]) % mo; return ret; } int ret = (tr[blong[opl]].r - opl + 1) * add[blong[opl]] % mo; ret = (ret + (opr - tr[blong[opr]].l + 1) * add[blong[opr]] % mo) % mo; for (int i = opl; i <= tr[blong[opl]].r; i++) ret = (ret + tag[i]) % mo; for (int i = tr[blong[opr]].l; i <= opr; i++) ret = (ret + tag[i]) % mo; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) ret = (ret + tr[i].val) % mo; return ret % mo; } signed main() { n = read(); for (int i = 1; i <= n; i++) tag[i] = read(); build(); for (int i = 1; i <= n; i++) { int opt = read(), l = read(), r = read(), c = read(); if (opt == 0) update(l, r, c); else write(query(l, r, c + 1)), putchar('\n'); } return 0; }