题解:P13977 数列分块入门 2
省流:没过审。
P13977 数列分块入门 2
一点鲜花
分块,不得不说是一种非常可爱的数据结构了。
正文
为什么用分块?
这一部分不想看是可以不看的,跳到下一节就行。
有一个很重要的事情,那就是它为什么可以只用分块来做?同样维护区间的线段树为什么不可以单独完成这一任务?
太深奥的咱不讲也不会讲,大概理解一下,就是因为线段树是在细分这一个序列,用上层维护下层的信息,这样子的信息是精确的,下层的一切都被传递到了上层,看起来很好,然而,哪怕下层的部分受到了修改,最上层的代表整个区间的信息的节点也要更新,也就相当于更新了整个序列,是 \(n\) 级别的,这在维护每个区间内数与数间的关系时,其缺陷是致命的。
分块,它是一个看似“粗陋”的东西,我们用整块代表这一个区间,不用仔细考虑区间内部实际上长成什么样子,这点和线段树是很类似的,然而,不同在于每一次修改时我们只会对收到散块的修改时改变其所在整块内数与数的关系,这时实际操作的区间大小相当于只有 \(\sqrt n\) 级别,相对于线段树的多次上传,甚至影响整个序列,分块是可以接受的程度。
分块怎么写?
上面说了分块单次更新的部分是 \(\sqrt n\) 级别的,那么就可以考虑直接对一个整块的全部内容进行操作,具体的讲,大概是以下几步。
- 分块并对每块内部排序,与查询复杂度有关。
- 整块修改只打标记,因为整个区间的加减不会影响内部大小关系,复杂度是 \(O(\sqrt n)\) 的。
- 散块修改重构所在整块,因为不完整的加减会影响整块的内部顺序,复杂度是 \(O(\sqrt n \log \sqrt n) 的\)。
- 查询对整块二分查询,散块暴力处理,复杂度总的是 \(O(\sqrt n \log \sqrt n) 的\)。
整体的复杂度不很优秀,但是实际上不慢,以下给出实现(因为用了一些比较省事的写法,其实我代码偏慢)。
首先是预处理,包括每块的起始和结束点,每块包含的序列(有序),每个点属于的块,自认为我的预处理方式是很简短的了,代码如下。
// B 是块长,为了方便设置成常量了,L、R 分别为块的左右端点
// k[i] 表示第 i 个块的有序序列,pos[i] 表示 i 所在的块
for(int i = 1; R[i - 1] != n; i ++) {
L[i] = (i - 1) * B + 1, R[i] = min(n, B * i);
for(int j = L[i]; j <= R[i]; j ++)
pos[j] = i, k[i].push_back(a[j]);
sort(k[i].begin(), k[i].end());
}
其次是修改操作,对于两端的散块,直接暴力修改即可,之后再将其覆盖并排序(这里要注意判断一下两个端点是否在同一个散块内),整块的话直接打上标记即可,操作起来不难的说,个人感觉用 vector 虽然略慢但是比用数组区间排序好操作太多了。
if(pos[l] != pos[r]) {
// 授先散块暴力修改
for(int j = l; j <= R[pos[l]]; j ++) a[j] += c;
// 然后重构其所在块
k[pos[l]].clear();
for(int j = L[pos[l]]; j <= R[pos[l]]; j ++) k[pos[j]].push_back(a[j]);
sort(k[pos[l]].begin(), k[pos[l]].end());
// 右面的散块也一样
for(int j = r; j >= L[pos[r]]; j --) a[j] += c;
k[pos[r]].clear();
for(int j = L[pos[r]]; j <= R[pos[r]]; j ++) k[pos[j]].push_back(a[j]);
sort(k[pos[r]].begin(), k[pos[r]].end());
// 整块只打标记就行
for(int j = pos[l] + 1; j < pos[r]; j ++) tag[j] += c;
}
else {
// 也是散块
for(int j = l; j <= r; j ++) a[j] += c;
k[pos[l]].clear();
for(int j = L[pos[l]]; j <= R[pos[r]]; j ++) k[pos[l]].push_back(a[j]);
sort(k[pos[r]].begin(), k[pos[r]].end());
}
最后是查询操作,与线段树的标记永久化类似,分块在这里不需要下放标记,对于整块,在查询每个整块时,因为整个块的标记是一样的,所以可以改变查询值,把查询值减该块的标记即可,然后直接查询散块再将排名加和就可以得到最终结果了,说着很复杂,其实还挺好实现的。
int ans = 0;
c = c * c;
if(pos[l] != pos[r]) {
// 散块直接查
for(int j = l; j <= R[pos[l]]; j ++) ans += ((a[j] + tag[pos[j]]) < c);
for(int j = r; j >= L[pos[r]]; j --) ans += ((a[j] + tag[pos[j]]) < c);
// 整块内二分
for(int j = pos[l] + 1; j < pos[r]; j ++)
ans += lower_bound(k[j].begin(), k[j].end(), c - tag[j]) - k[j].begin();
}
else
for(int j = l; j <= r; j ++) ans += ((a[j] + tag[pos[j]]) < c);
out(ans), en_;
复杂度与块长二三事
这一部分不想看也可以不看,跳到下一节就行。
很多时候我们都看到块长设置为 \(\sqrt n\),其实不尽然,设 \(B\) 为块长,假设有一道题有着 \(O(n\times B^2)\) 的预处理和 \(O(q \times \frac n B)\) 的查询,选择 \(\sqrt n\) 作为块长会收获一个 \(n^2\) 的复杂度,而选择 \(n^\frac 1 3\)。却可以获得 \(O(n ^ \frac 5 3)\) 的复杂度。所以建议具体分析,不盲目选择块长,下面以本题为例进行块长的计算和复杂度分析,讲的不细,勉强能看。
列出各个操作的时间复杂度
这道题中,我们有以下操作:
- 预处理 \(O(n \log n)\)。
- 修改 \(O(B \log B + \frac nB)\)。
- 查询 \(O(\frac nB \log B + B)\)。
适当省略与合并
省略预处理的时间复杂度,因为那是不太能够改变的了的。
有的时候一些项真的非常小或者相对小也是可以省略的,这里没有,就算了。
一般这么做就可以将多项式转化为二项式了(比如本题),如果不行还尝试可以合并。
一些变量的数据范围相近或相等,可以化成同一个变量进行复杂度计算,比如本题的序列长度和操作数,额,真的就是一模一样呢。
再不行的话,其实多项式也没什么不好对吧(心虚)。
加等号求解
一般可以直接在复杂度的两项之间加上等号,求出最优块长。如本题 \(B \log B + \frac nB = \frac nB \log B + B\)。
容易解出 \(B = \sqrt n\) 时取等。
综上,块长取 \(\sqrt n\) 时时间复杂度最优,为 \(n \sqrt n\)。
最后放上完整代码罢:
// code by 樓影沫瞬_Hz17
#include <bits/stdc++.h>
#define en_ putchar_unlocked('\n')
#define e_ putchar_unlocked(' ')
#define int long long
using namespace std;
template<typename T> inline T in() {
T n = 0; char p = getchar_unlocked();
while (p < '-') p = getchar_unlocked();
bool f = p == '-' ? p = getchar_unlocked() : 0;
do n = n * 10 + (p ^ 48), p = getchar_unlocked();
while (isdigit(p));
return f ? -n : n;
}
template<typename T> inline T in(T &a) { return a = in<T>(); }
template<typename T> inline void out(T n) {
if(n < 0) putchar_unlocked('-'), n = -n;
if(n > 9) out(n / 10);
putchar_unlocked(n % 10 + '0');
}
const int N = 2e5 + 10, B = 500;
using pii = pair<int, int>;
int L[N], R[N], pos[N], a[N], tag[N];
vector<int> k[N];
int n;
signed main() {
#ifndef ONLINE_JUDGE
freopen("i", "r", stdin);
freopen("o", "w", stdout);
#endif
in(n);
for(int i = 1; i <= n; i ++) in(a[i]);
for(int i = 1; R[i - 1] != n; i ++) {
L[i] = (i - 1) * B + 1, R[i] = min(n, B * i);
for(int j = L[i]; j <= R[i]; j ++) pos[j] = i, k[i].push_back(a[j]);
sort(k[i].begin(), k[i].end());
}
for(int i = 1, op, l, r, c; i <= n; i ++) {
in(op), in(l), in(r), in(c);
if(op == 0) {
if(pos[l] != pos[r]) {
for(int j = l; j <= R[pos[l]]; j ++) a[j] += c;
k[pos[l]].clear();
for(int j = L[pos[l]]; j <= R[pos[l]]; j ++) k[pos[j]].push_back(a[j]);
sort(k[pos[l]].begin(), k[pos[l]].end());
for(int j = r; j >= L[pos[r]]; j --) a[j] += c;
k[pos[r]].clear();
for(int j = L[pos[r]]; j <= R[pos[r]]; j ++) k[pos[j]].push_back(a[j]);
sort(k[pos[r]].begin(), k[pos[r]].end());
for(int j = pos[l] + 1; j < pos[r]; j ++) tag[j] += c;
}
else {
for(int j = l; j <= r; j ++) a[j] += c;
k[pos[l]].clear();
for(int j = L[pos[l]]; j <= R[pos[r]]; j ++) k[pos[l]].push_back(a[j]);
sort(k[pos[r]].begin(), k[pos[r]].end());
}
}
if(op == 1) {
int ans = 0;
c = c * c;
if(pos[l] != pos[r]) {
for(int j = l; j <= R[pos[l]]; j ++) ans += ((a[j] + tag[pos[j]]) < c);
for(int j = r; j >= L[pos[r]]; j --) ans += ((a[j] + tag[pos[j]]) < c);
for(int j = pos[l] + 1; j < pos[r]; j ++)
ans += lower_bound(k[j].begin(), k[j].end(), c - tag[j]) - k[j].begin();
}
else
for(int j = l; j <= r; j ++) ans += ((a[j] + tag[pos[j]]) < c);
out(ans), en_;
}
}
}
// 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~

浙公网安备 33010602011771号