数列分块(数据结构)学习笔记
发现自己并没有掌握分块的正确姿势,然后于近期学习一下。
顺便提一下,分块是一个很好想,很暴力,复杂度较直接暴力比较低的算法
如果数据是随机数据那么实际复杂度会比期望要小。
然后最优分块的数目,根据不同的题目是不同的,可以通过均值不等式算出
但是大多数数据结构题目直接对数列进行$\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; }
下面一道例题是关于技巧的实现。显然会比线段树方便。
#6281. 数列分块入门 5 给出一个长度为n的数列ai,以及n个操作,操作涉及区间开方、区间求和
其实这道题目,直接使用线段树对于开方运算这种没有结合律的操作形式是不适用的。
也就是说只有满足$\sum\limits_{i=l}^{r} Calc(a_i) = Change( Calc(\sum\limits_{i=l}^{r} a_i) )$说明Change()函数必须运行复杂度是常数级别。
才能使用线段树直接维护父亲和儿子的关系。
对于这道题目,有个很显然的性质,若$x=0$或者$x=1$那么$\sqrt{x}$就不再进行任何改变,那么x对于答案的新贡献等于0
显然这道题目直接并查集暴力是不可行的,你还需要数据结构需要支持区间查询的操作。
我们考虑分块的操作,显然考虑代码难度成本,我们直接在块中记录一个布尔flag变量,表示这个块是不是全部到达0或者1了。
这样对于所有块进行暴力更新,当且仅当flag=false的时候,这样可以减少大量无效运算。
没错分块就是那么暴力。
可以证明,对于一个属于int范围的数,进行5次不断开方以后会变成0或者1,所以复杂度是正确的,
可以近似于$k n \sqrt{n}(k是一个较小的常数)$
显然,对线段树每个节点也建立一个flag标记也可以实现维护,复杂度为$k n log_2 n(k是一个较小的常数)$
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 5e4 + 10; int tag[N], blong[N]; int n, block, num; struct rec { int l, r, val; bool flag; } tr[305]; 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].flag = true, tr[i].val = 0; tr[num].r = n; for (int i = 1; i <= n; i++) { blong[i] = (i - 1) / block + 1; if (tag[i] != 1 && tag[i] != 0) tr[blong[i]].flag = false; tr[blong[i]].val += tag[i]; } } bool check(int x) { for (int i = tr[x].l; i <= tr[x].r; i++) if (tag[i] != 0 && tag[i] != 1) return false; return true; } void update(int opl, int opr) { if (blong[opl] == blong[opr]) { if (tr[blong[opl]].flag) return; for (int i = opl; i <= opr; i++) { int t = sqrt(tag[i]); tr[blong[opl]].val -= tag[i] - t; tag[i] = t; } tr[blong[opl]].flag = check(blong[opl]); return; } if (!tr[blong[opl]].flag) { for (int i = opl; i <= tr[blong[opl]].r; i++) { int t = sqrt(tag[i]); tr[blong[opl]].val -= tag[i] - t; tag[i] = t; } tr[blong[opl]].flag = check(blong[opl]); } if (!tr[blong[opr]].flag) { for (int i = tr[blong[opr]].l; i <= opr; i++) { int t = sqrt(tag[i]); tr[blong[opr]].val -= tag[i] - t; tag[i] = t; } tr[blong[opr]].flag = check(blong[opr]); } for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) { if (tr[i].flag) continue; for (int j = tr[i].l; j <= tr[i].r; j++) { int t = sqrt(tag[j]); tr[i].val -= tag[j] - t; tag[j] = t; } tr[i].flag = check(i); } } int query(int opl, int opr) { if (blong[opl] == blong[opr]) { int ret = 0; for (int i = opl; i <= opr; i++) ret += tag[i]; return ret; } int ret = 0; for (int i = opl; i <= tr[blong[opl]].r; i++) ret += tag[i]; for (int i = tr[blong[opr]].l; i <= opr; i++) ret += tag[i]; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) ret += tr[i].val; return ret; } signed main() { scanf("%lld", &n); for (int i = 1; i <= n; i++) scanf("%lld", &tag[i]); build(); for (int i = 1; i <= n; i++) { int opt, l, r, c; scanf("%lld%lld%lld%lld", &opt, &l, &r, &c); if (!opt) update(l, r); else printf("%lld\n", query(l, r)); } return 0; }
我们上面使用分块解决了一些关于线段树维护的题目,我们接下来讨论一个插入型问题。
#6282. 数列分块入门 6 给出一个长为 n 的数列,以及 n 个操作,操作涉及单点插入,单点询问,部分数据随机生成。
这道题目,对于数据随机生成的部分,我们直接对一个vector分块,tr[x].vec表示属于块x的每个元素。然后我们可以根据vector的size()用$O(\sqrt{n})$复杂度来找出数据下标为x的元素是在哪个块的第哪个位置。
然后对于不随机的数据怎么处理,假定所有的元素都是插入在同一个位置,那么那个块的元素个数会达到n个,大大超过$\sqrt{n}$的临界点,所以我们考虑如果每插入操作$\sqrt{n}$次,对块进行一次复杂度为O(n)的重构,这样就可以保证元素相对均匀分布了。
复杂度$O(n \sqrt{n})$
其实还是需要注意一些细节的,比如vector的下标是从0开始的,还有vector如果交换到第size个了那个第size个自动忽略,所以每一个插入一个元素的时候需要在原来的vector的末尾插入1个数0,才能保证末尾元素不被删除(这样删除的不就是插入的那个0了吗)。
/* query(x)用sqrt(n)复杂度询问第x个元素是哪个块的哪个位置 每个块维护一个vector存储这个块里面的所有元素 pi.first表示所属块的编号,pi.second表示该块中的位置(注意下标) 每sqrt(n)个询问重新分块 暴力插入第i个块中 */ #include <bits/stdc++.h> #define pi pair<int, int> using namespace std; const int N = 2e5 + 10; struct rec { vector<int> vec; } tr[505]; int n, num, block; int a[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) x = -x, putchar('-'); 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].vec.clear(); for (int i = 1; i <= n; i++) { int t = (i - 1) / block + 1; tr[t].vec.push_back(a[i]); } } pi query(int pos) { pi t; int x = 1; while (pos > tr[x].vec.size()) { pos -= tr[x].vec.size(); x++; } return make_pair(x, pos - 1); } void rebuild() { n = 0; for (int i = 1; i <= num; i++) for (int j = 0; j < tr[i].vec.size(); j++) a[++n] = tr[i].vec[j]; build(); } void insert(int pos, int x) { pi p = query(pos); tr[p.first].vec.push_back(0); for (int i = tr[p.first].vec.size() - 1; i > p.second; i--) tr[p.first].vec[i] = tr[p.first].vec[i - 1]; tr[p.first].vec[p.second] = x; } int main() { n = read(); int tmp = n; for (int i = 1; i <= n; i++) a[i] = read(); build(); int times = n / block; int cnt = 0; for (int i = 1; i <= tmp; i++) { int opt = read(), l = read(), r = read(), c = read(); if (!opt) insert(l, r); else { pi t = query(r); write(tr[t.first].vec[t.second]); putchar('\n'); } cnt++; if (cnt == times) cnt = 0, rebuild(); } return 0; }
上面我们维护的标记可能只含有add标记而不会考虑两个标记的兼容,事实上,分块是可以实现两个标记的兼容。
我们需要知道两个标记之间的联系,经典的加法标记好乘法标记的联系。
#6283. 数列分块入门 7 给出一个长为 n 的数列,以及 n 个操作,操作涉及区间乘法,区间加法,单点询问。
这里就需要维护标记之间的关系,我们定义加法标记为a,乘法标记为m,那个当前这个元素确定的值就是$N=mx+a$(乘法的优先级较高)
如果对这个元素$+d$,那个这个元素就变成了$N'=N+d=mx+(a+d)$显然,一个单独的加法标记,只对加法标记产生+d贡献
如果对这个元素$*d$,那么这个元素就变成了$N'=N*d=(mx+a)d=mdx+ad$,显然一个单独的乘法标记,对加法标记有*d,对乘法标记有*d的贡献
显然这里的标记永远是指该块的整体的加法和乘法标记,最终来维护一个a数组表示元素的值。
对于整块外元素的处理,我们先把当前区间更新,然后对于更新完毕的数组a进行暴力更新。
对于整块内元素的处理,这里统计标记的贡献。
每一个查询的时候,注意先把该区间的标记下放到该区间,保证a在这一区间是保持同步的。
复杂度$O(n \sqrt{n})$,使用线段树维护两个lazy标记可以实现$O(n log_2 n)$
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 10; const int mo = 10007; struct rec { int l, r, add, mul; } tr[N]; int n, num, block, blong[N], a[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].add = 0, tr[i].mul = 1; tr[num].r = n; for (int i = 1; i <= n; i++) blong[i] = (i - 1) / block + 1; } void reset(int x) { for (int i = tr[x].l; i <= tr[x].r; i++) a[i] = ((tr[x].mul * a[i]) % mo + tr[x].add) % mo; tr[x].add = 0; tr[x].mul = 1; } void update_add(int opl, int opr, int d) { if (blong[opl] == blong[opr]) { reset(blong[opl]); for (int i = opl; i <= opr; i++) a[i] = (a[i] + d) % mo; return; } reset(blong[opl]); for (int i = opl; i <= tr[blong[opl]].r; i++) a[i] = (a[i] + d) % mo; reset(blong[opr]); for (int i = tr[blong[opr]].l; i <= opr; i++) a[i] = (a[i] + d) % mo; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) tr[i].add = (tr[i].add + d) % mo; } void update_mul(int opl, int opr, int d) { if (blong[opl] == blong[opr]) { reset(blong[opl]); for (int i = opl; i <= opr; i++) a[i] = (a[i] * d) % mo; return; } reset(blong[opl]); for (int i = opl; i <= tr[blong[opl]].r; i++) a[i] = a[i] * d % mo; reset(blong[opr]); for (int i = tr[blong[opr]].l; i <= opr; i++) a[i] = a[i] * d % mo; for (int i = blong[opl] + 1; i <= blong[opr] - 1; i++) tr[i].mul = tr[i].mul * d % mo, tr[i].add = tr[i].add * d % mo; } int query(int x) { reset(blong[x]); return a[x] % mo; } signed main() { n = read(); for (int i = 1; i <= n; i++) a[i] = read() % mo; build(); for (int i = 1; i <= n; i++) { int opt = read(), l = read(), r = read(), c = read(); if (opt == 0) update_add(l, r, c); else if (opt == 1) update_mul(l, r, c); else if (opt == 2) write(query(r)), putchar('\n'); } return 0; }
数列分块几乎可以解决所有的维护数列的任务,其适用范围其实和线段树一样广泛。
希望大家好好练习,可以掌握正确姿势,也希望这篇学习笔记可以帮到各位理解、运用分块算法。
下面是上述所有练习的汇总:https://loj.ac/
#6277. 数列分块入门 1 区间加单点值
#6278. 数列分块入门 2 区间加求小的个数
#6279. 数列分块入门 3 区间加求前驱
#6280. 数列分块入门 4 区间加区间和
#6281. 数列分块入门 5 区间开方区间和
#6282. 数列分块入门 6 单点插入单点值
#6283. 数列分块入门 7 加乘修改单点和
(The End)