【学习笔记】分块
分块
分块牛逼普拉斯
简介
基本思想
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度.
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度.
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息.
比如不满足可加性的东西
思考
分块是维护序列的数据结构,是一种“准暴力”手段,它比树形数据结构好写,也便于调试
分块的基本思想是通过适当的划分,预处理适当大小的信息并保存下来,用空间换取时间,使得修改以及查询的时间复杂度都能被接受,达到“时空平衡”。
预处理
首先定义块操作的基本要素,便于后续维护和处理:
- 块的大小:用block表示。
- 块的数量:用t表示。
- 块的左右边界:用数组st[],ed[]表示,st[i], ed[i]就是第i块的起点与终点。
- st[i]=(i-1)*block+1 ed[i]=i∗ block
- 每个元素所属的块:id[i]表示第i个元素属于哪个块。
区间修改 & 区间查询
分块九题
本文中的所有~x表示分块入门x
具体题目请在OJ上搜索
~1
本题包含分块入门1和4
Prob
给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间(单点)加法,区间求和。
分块图示
我们先把它每 \(s\) 个元素分成一块,便可以得到:
[a1 a2 a3...as][as+1...a2s][....]....[as*s-s+1 ... an]
b1 b2 b3 ... bn
注意到最后一个可能是不完整的(\(n \% s\)可能不是0)
Think
[Q]怎么实现Query&Update[1]
我们先来实现 query(l,r) 函数
- 对于完整的块,直接回答 \(b_i\) 即可
- 对于不完整的块,暴力即可
接下来是 update(l,r,x)
- 对于完整的块,修改 \(b_i\) 即可
- 对于不完整的块,暴力即可
具体代码:
void Update(int l, int r, int x)
{
int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
if (Lk == Rk)
{
for (int i = l; i <= r; i++)
A[i] += x, Sum[Lk] += x; // 算自己的贡献&对整个块的贡献
return;
}
for (int i = l; id[i] == Lk; i++) A[i] += x, Sum[Lk] += x;
for (int i = Lk + 1; i <= Rk - 1; i++) LzTag[i] += x, Sum[i] += x * len;
for (int i = r; id[i] == Rk; i--) A[i] += x, Sum[Rk] += x;
}
int Query(int l, int r, int mod)
{
int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
int Ans = 0;
if (Lk == Rk)
{
for (int i = l; i <= r; i++)
Ans = (Ans + A[i] + LzTag[Lk]) % mod;
return Ans;
}
for (int i = l; id[i] == Lk; i++) Ans = (Ans + A[i] + LzTag[Lk]) % mod;
for (int i = Lk + 1; i <= Rk - 1; i++) Ans = (Ans + Sum[i]) % mod;
for (int i = r; id[i] == Rk; i--) Ans = (Ans + A[i] + LzTag[Rk]) % mod;
return Ans;
}
[Q]怎么求S使得时间复杂度最低?
那么s怎么求呢?
所以\(S=\sqrt{N}\)
[Q]如果不是 \(\sqrt{n}\)会怎么样
如果块数过少,那么就会出现非常多的非完整块
如果块数过多,那么在就会有非常多的整块,从而没有优化的作用了
复杂度分析
所以一次操作是\(O(\sqrt{N})\)且\(\Omega(1)\)
(最好 \(O(1)\) 均摊 \(O(\sqrt{n})\) )
Code
#define int long long
static constexpr int mod = 1e9 + 7;
const int maxNOfArray = 5e4 + 10;
#define r(n) n[maxNOfArray]
int r(id), r(A), r(LzTag), r(Sum), len;
void Update(int l, int r, int x)
{
int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
if (Lk == Rk)
{
for (int i = l; i <= r; i++)
A[i] += x, Sum[Lk] += x; // 算自己的贡献&对整个块的贡献
return;
}
for (int i = l; id[i] == Lk; i++) A[i] += x, Sum[Lk] += x;
for (int i = Lk + 1; i <= Rk - 1; i++) LzTag[i] += x, Sum[i] += x * len;
for (int i = r; id[i] == Rk; i--) A[i] += x, Sum[Rk] += x;
}
int Query(int l, int r, int mod)
{
int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
int Ans = 0;
if (Lk == Rk)
{
for (int i = l; i <= r; i++)
Ans = (Ans + A[i] + LzTag[Lk]) % mod;
return Ans;
}
for (int i = l; id[i] == Lk; i++) Ans = (Ans + A[i] + LzTag[Lk]) % mod;
for (int i = Lk + 1; i <= Rk - 1; i++) Ans = (Ans + Sum[i]) % mod;
for (int i = r; id[i] == Rk; i--) Ans = (Ans + A[i] + LzTag[Rk]) % mod;
return Ans;
}
当然也可以用前缀和
注意到本题为1和4的结合
~2
Prob
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值的元素个数。
让我们开始深度睡眠思考
以下的所有查询与修改都指的是区间查询和区间修改
Think
查询
[Q]需要思考哪些问题
- 如何处理角块呢?
- 整块如何处理?
- 需要预处理哪些信息?
[A]-----[A]
我们根据之前的经验,可以大致的想到:
- 不完整的块暴力处理
- 完整的块二分即可
注意到有二分,二分的前提是Sorted
所以我们需要在修改时Sort
修改
区间加法
[Q]需要思考哪些问题
还是这三问
- 如何处理角块呢?
- 整块如何处理?
- 需要预处理哪些信息?
[A]-----[A]
对于完整的块还是直接标记即可
而对于不完整的块,修改后区间会乱序
因此需要排序即可。
Code
struct fk
{
int n, sz;
vector<long long> a, tag;
vector<int> pos, L, R;
vector<long long> v[505];
fk(int _n) : n(_n), sz(sqrt(_n)), a(_n + 1), tag(_n / sz + 2), pos(_n + 1), L(_n / sz + 2), R(_n / sz + 2)
{
for (int i = 1; i <= n; i++)
{
pos[i] = (i - 1) / sz + 1;
if (!L[pos[i]]) L[pos[i]] = i;
R[pos[i]] = i;
}
}
void reset(int p)
{
v[p].clear();
for (int i = L[p]; i <= R[p]; i++) v[p].push_back(a[i]);
sort(v[p].begin(), v[p].end());
}
void update(int l, int r, int v_val)
{
int p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) a[i] += v_val;
reset(p);
return;
}
for (int i = l; i <= R[p]; i++) a[i] += v_val; reset(p);
for (int i = L[q]; i <= r; i++) a[i] += v_val; reset(q);
for (int i = p + 1; i <= q - 1; i++) tag[i] += v_val;
}
long long query(int l, int r, long long c)
{
int p = pos[l], q = pos[r];
long long ans = 0;
if (p == q) {
for (int i = l; i <= r; i++) if (a[i] + tag[p] < c) ans++;
return ans;
}
for (int i = l; i <= R[p]; i++) if (a[i] + tag[p] < c) ans++;
for (int i = L[q]; i <= r; i++) if (a[i] + tag[q] < c) ans++;
for (int i = p + 1; i <= q - 1; i++)
ans += lower_bound(v[i].begin(), v[i].end(), c - tag[i]) - v[i].begin();
return ans;
}
};
复杂度分析
排序 \(O(n\log n)\)
查询在\(\sqrt{n}\)个块中二分,加上暴力\(2\sqrt{n}\) 这些一共\(O(n\sqrt{n}\log\sqrt{n})\)
一共的话:\(O(n\log n + n\sqrt{n}\log\sqrt{n})\)
~3
Prob
给出一个长为 n的数列,以及 n个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。
Think
发现这道题和上一道很像,所以略讲
由于是要找的是 x 的前驱,因此暴力找左右不完整块的比x 小最大值,
再二分找整块比 x 小的最大值,取其中最大即可
时间复杂度的话和上题一样。
~4
Prob
给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。
Think
这道题如果只有区间乘法,那么和第一题没有区别
[Q]但是如何处理两种标记呢?
初步思考:维护两种标记
那么如何修改呢?
先乘后加,先加后乘?
[A]-----[A]
这里先处理乘法,再处理加法。
即两种标记具有优先级关系乘法标记的优先级高于加法。
[Q]如何具体实现?
若当前的一个块乘以\(m1\)后加上\(a1\),这时进行一个乘\(m2\)的操作,则原来的标记变成\(m1*m2\),\(a1*m2\)
若当前的一个块乘以\(m1\)后加上\(a1\),这时进行一个加\(a2\)的操作,则原来的标记变成\(m1\),\(a1+a2\)
[Q]如果不这么做会怎么样?
不同计算顺序,结果不一样。
所以不行。
~5
Prob
给出一个长为 n的数列,以及 n个操作,操作涉及区间开方,区间求和。
Think
由于涉及开方,整块无法开方,需要对每一个元素进行操作后,才能知道开方后的总和。
[Q]难以维护,如何解决?
我们手搓几组数据,就会发现:
1e9也只需要5次开方就会变成1,7次变成0!!!
思路
所以我们可以模拟思路:
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成0 / 1
于是我们采取一种分块优化的暴力做法:
只要每个整块暴力开方后,开数组记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1的块即可
Code
const int MAXN = 50005;
ll a[MAXN];
ll sum[300];
ll max_val[300];
int L[300], R[300], pos[MAXN];
int n, Ksize, Kcount;
void build(int b)
{
sum[b] = 0;
max_val[b] = 0;
for (int i = L[b]; i <= R[b]; ++i)
{
sum[b] += a[i];
max_val[b] = max(max_val[b], a[i]);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
Ksize = sqrt(n);
Kcount = (n + Ksize - 1) / Ksize;
for (int b = 1; b <= Kcount; b++)
{
L[b] = (b - 1) * Ksize + 1;
R[b] = min(b * Ksize, n);
for (int i = L[b]; i <= R[b]; i++)
{
pos[i] = b;
}
build(b);
}
for (int i = 0; i < n; i++)
{
int opt, l, r;
ll c;
cin >> opt >> l >> r >> c;
int bl = pos[l];
int br = pos[r];
if (opt == 0)
{
if (bl == br)
{
for (int j = l; j <= r; j++)
a[j] = floor(sqrt(a[j]));
build(bl);
}
else
{
for (int j = l; j <= R[bl]; j++)
a[j] = floor(sqrt(a[j]));
build(bl);
for (int b = bl + 1; b < br; b++)
{
if (max_val[b] > 1)
{
for (int j = L[b]; j <= R[b]; j++)
a[j] = floor(sqrt(a[j]));
build(b);
}
}
for (int j = L[br]; j <= r; j++)
a[j] = floor(sqrt(a[j]));
build(br);
}
}
else
{
ll ans = 0;
if (bl == br)
for (int j = l; j <= r; j++)
ans += a[j];
else
{
for (int j = l; j <= R[bl]; j++)
ans += a[j];
for (int b = bl + 1; b < br; b++)
ans += sum[b];
for (int j = L[br]; j <= r; j++)
ans += a[j];
}
cout << ans << "\n";
}
}
return 0;
}
~6
Prob
给出一个长为 n的数列,以及 n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。
Think
我们发现,根本无法维护!!
[Q]如何维护?
刚开始是有不同的权值,但随着询问次数增多,可能就几种权值区间了
我们借鉴一下~5的思路:维护每一个块是否已被完全覆盖
对于同权值的一个块能在 \(O(1)\) 即可统计答案,若不是只有一种权值,那么就暴力统计答案,然后修改标记,而对于不完整块,同样使用暴力维护
[Q]这么一来,岂不是最坏 \(O(n)\)?
No,假设初始序列都是同一个值,那么查询是 \(O(n)\),如果此时进行一个区间操作,其最多破坏首尾两个块的标记,因此只能使后面的询问至多多两个块的暴力时间, 所以均摊后每次操作的复杂度仍为\(O(\sqrt{n})\)
简单来说,要想让一个操作花费 \(O(n)\) 的时间,就要先花费 \(\sqrt{n}\)个操作修改。
~7
Prob
给出一个长为 n的数列,以及 n个操作,操作涉及单点插入,单点询问,数据随机生成。
观察到数据随机生成
所以可以直接暴力即可
Ex
如果数据不是随机的
我们只需要重新分块即可
或者是说用
Pb-ds的Rope
~8
Prob
给出一个长为 n的数列,以及 n个操作,操作涉及询问区间的最小众数。
众数不满足可加性,所以本题分块最合适
Think
- 如何处理角块呢?
- 整块如何处理?
- 需要预处理哪些信息?
众数可能是 完整的所有块的众数,和不完整块中出现的数
预处理出\(f[i][j]\)表示第 i 块到第 j 块的众数,开个桶从遍历一遍
那么只要能快速得出一个数在某个区间内出现次数即可,每次只要比较至多\(2\sqrt{n} + 1\) 个元素的出现次数,这题就解决了
由于没有修改,只要离散化以后,给每个数 x 开个vector,按顺序存下x 出现的位置,每次询问x 时把区间的左右端点放进对应 vector 二分一下即可。
我们来用均值不等式算一下,就可以得到大小大概是
所以我们就解决了问题!
Ex
如何把复杂度下降呢?
用前缀和
\(cnt[i][j]\)表示:从1到第 i 块结束, 数字 j 出现的次数
对于每个候选值,用前缀和快速计算它在中间整块的出现次数,加上两个角块的出现次数
大致总结
分块比线段树更强!
牛逼普拉斯
__EOF_
具体看这里 ↩︎

浙公网安备 33010602011771号