分块快速入门
基本思想
有一句老话,叫“大段维护,局部朴素”。
其实就是将一些东西人为的分为若干块,然后每个块整体的维护一些东西,小范围内直接暴力做,做到时空平衡。
其实和根号分治有点像。
然后整除分块和莫队我觉得和传统分块差别有点大了,就没写。
数列分块我一般的写法是维护每个位置所在块编号,块的左端点和块的右端点。
数列分块入门题
LOJ 数列分块入门 1
区间加,单点查。
维护区间整体增加了多少,修改的时候如果覆盖了整块就加到整块增量里去,否则直接加到数上。
查询就返回数组里的数+所在块的总体增量。
时间复杂度 \(O(N\sqrt N)\)。
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 50005;
int len = 250, n, a[N];
int pos[N], add[N], L[N], R[N];
void init() {
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
for (int i = 1; i <= n; i++) {
if (pos[i] != pos[i - 1]) L[i] = i;
else L[i] = L[i - 1];
}
for (int i = n; i >= 1; i--) {
if (pos[i] != pos[i + 1]) R[i] = i;
else R[i] = R[i + 1];
}
}
void upd(int l, int r, int v) {
if (pos[l] == pos[r]) {
for (int i = l; i <= r; i++) a[i] += v;
return ;
}
int p = pos[l] + 1, q = pos[r] - 1;
for (int i = p; i <= q; i++) add[i] += v;
for (int i = l; i <= R[l]; i++) a[i] += v;
for (int i = L[r]; i <= r; i++) a[i] += v;
}
int qry(int p) {
return a[p] + add[pos[p]];
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", a + i);
init();
for (int i = 1, op, l, r, c; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
upd(l, r, c);
} else {
printf("%d\n", qry(r));
}
}
return 0;
}
LOJ 数列分块入门 2
区间加,查区间某元素排名。
还是储存整块的增量,一个位置实际的值就是访问到的+所在块的增量。
然后每块暴力预处理排一下序,修改的时候零散的也排序。每次修改最多排两次。
查询的话,对于整块的 lower_bound 一下,零散的一个一个数。
时间复杂度 \(O(N \sqrt n \log n)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 50005;
int len = 250, n, a[N], cnt;
int pos[N], add[N], L[N], R[N], rnk[N];
void init() {
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
cnt = pos[n];
for (int i = 1; i <= cnt; i++) L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
memmove(rnk, a, sizeof(int) * n);
for (int i = 1; i <= cnt; i++) sort(rnk + L[i], rnk + R[i] + 1);
}
void upd(int l, int r, int v) {
int p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) a[i] += v;
memmove(rnk + L[p], a + L[p], sizeof(int) * (R[p] - L[p] + 1));
sort(rnk + L[p], rnk + R[q] + 1);
return ;
}
for (int i = p + 1; i <= q - 1; i++) add[i] += v;
for (int i = l; i <= R[p]; i++) a[i] += v;
for (int i = L[q]; i <= r; i++) a[i] += v;
memmove(rnk + L[p], a + L[p], sizeof(int) * (R[p] - L[p] + 1));
memmove(rnk + L[q], a + L[q], sizeof(int) * (R[q] - L[q] + 1));
sort(rnk + L[p], rnk + R[p] + 1), sort(rnk + L[q], rnk + R[q] + 1);
}
int qry(int l, int r, int v) {
int ret = 0, p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) ret += ((a[i] + add[pos[i]]) < v);
return ret;
}
for (int i = p + 1; i <= q - 1; i++) {
ret += lower_bound(rnk + L[i], rnk + R[i] + 1, v - add[i]) - (rnk + L[i] - 1) - 1;
}
for (int i = l; i <= R[p]; i++) ret += ((a[i] + add[p]) < v);
for (int i = L[q]; i <= r; i++) ret += ((a[i] + add[q]) < v);
return ret;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", a + i);
init();
for (int i = 1, op, l, r, c; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
upd(l, r, c);
} else {
printf("%d\n", qry(l, r, c * c));
}
}
return 0;
}
LOJ 数列分块入门 3
区间加法,查区间前驱。
和前一道题一样,我们进行块内排序。
查询的时候 lower_bound 完以后,往回数一个就好了。
零散的还是一个个找。
时间复杂度 \(O(N\sqrt N \log N)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int len = 300, n, a[N], cnt;
int pos[N], add[N], L[N], R[N], rnk[N];
void init() {
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
cnt = pos[n];
for (int i = 1; i <= cnt; i++) L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
memmove(rnk, a, sizeof(int) * n);
for (int i = 1; i <= cnt; i++) sort(rnk + L[i], rnk + R[i] + 1);
}
void upd(int l, int r, int v) {
int p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) a[i] += v;
memmove(rnk + L[p], a + L[p], sizeof(int) * (R[p] - L[p] + 1));
sort(rnk + L[p], rnk + R[q] + 1);
return ;
}
for (int i = p + 1; i <= q - 1; i++) add[i] += v;
for (int i = l; i <= R[p]; i++) a[i] += v;
for (int i = L[q]; i <= r; i++) a[i] += v;
memmove(rnk + L[p], a + L[p], sizeof(int) * (R[p] - L[p] + 1));
memmove(rnk + L[q], a + L[q], sizeof(int) * (R[q] - L[q] + 1));
sort(rnk + L[p], rnk + R[p] + 1), sort(rnk + L[q], rnk + R[q] + 1);
}
int qry(int l, int r, int v) {
int ret = -1, p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) {
if ((a[i] + add[p]) < v) {
ret = max(ret, a[i] + add[p]);
}
}
return ret;
}
for (int i = p + 1; i <= q - 1; i++) {
int rkv = lower_bound(rnk + L[i], rnk + R[i] + 1, v - add[i]) - rnk;
if (rkv != L[i]) ret = max(ret, rnk[rkv - 1] + add[i]);
}
for (int i = l; i <= R[p]; i++)
if (a[i] + add[p] < v) ret = max(ret, a[i] + add[p]);
for (int i = L[q]; i <= r; i++)
if (a[i] + add[q] < v) ret = max(ret, a[i] + add[q]);
return ret;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", a + i);
init();
for (int i = 1, op, l, r, c; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
upd(l, r, c);
} else {
printf("%d\n", qry(l, r, c));
}
}
return 0;
}
LOJ 数列分块入门 4
经典的区间加,区间求和。
维护块内数的和、整体的增量,一个一个加的时候就直接加到和里面,整块的时候加到增量里面。
最后所求的和就是 维护的和 + 本块块长 * 区间增量。
和线段树很类似。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 5e4 + 5;
int len = 250, n, a[N], cnt;
int pos[N], L[N], R[N];
ll add[N], sum[N];
void init() {
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
for (int i = 1; i <= n; i++) sum[pos[i]] += a[i];
cnt = pos[n];
for (int i = 1; i <= cnt; i++) L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
}
void upd(int l, int r, int v) {
int p = pos[l], q = pos[r];
if (p == q) {
for (int i = l; i <= r; i++) a[i] += v, sum[p] += v;
return ;
}
for (int i = p + 1; i <= q - 1; i++) add[i] += v;
for (int i = l; i <= R[p]; i++) a[i] += v, sum[p] += v;
for (int i = L[q]; i <= r; i++) a[i] += v, sum[q] += v;
}
int qry(int l, int r, int mod) {
int p = pos[l], q = pos[r];
ll ret = 0;
if (p == q) {
for (int i = l; i <= r; i++) {
ret = (ret + a[i] + add[p]) % mod;
}
return ret;
}
for (int i = p + 1; i <= q - 1; i++) {
ret = (ret + sum[i] + add[i] * (R[i] - L[i] + 1) % mod) % mod;
}
for (int i = l; i <= R[p]; i++)
ret = (ret + a[i] + add[p]) % mod;
for (int i = L[q]; i <= r; i++)
ret = (ret + a[i] + add[q]) % mod;
return ret;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", a + i);
init();
for (int i = 1, op, l, r, c; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
upd(l, r, c);
} else {
printf("%d\n", qry(l, r, c + 1));
}
}
return 0;
}
LOJ 数列分块入门 5
区间开根,区间求和。
一个很经典的结论是,一个数被开根的次数很小。如果一个数被开到 1(或 0)了,那之后就没必要管它了。
我们维护区间最大值和区间和,如果最大值 \(\le1\),那么这个区间就可以直接跳过。
为什么是正确的呢?
-
对于长度不足 \(\sqrt N\) 的修改,总复杂度是 \(O(N\sqrt N)\)。
-
对于长度 \(\ge \sqrt N\) 的修改,每个区间最多被修改 6 次左右,因此总修改次数是 \(O(\sqrt N)\) ,区间长度在 \(O(N)\) 级别,总复杂度 \(O(N\sqrt N)\)。
因此整个算法复杂度为 \(O(N\sqrt N)\)。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 5e4 + 5;
int len = 250, n, cnt;
int pos[N], L[N], R[N];
ll mx[N], sum[N], a[N];
void init() {
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
for (int i = 1; i <= n; i++) sum[pos[i]] += a[i];
for (int i = 1; i <= n; i++) mx[pos[i]] = max(mx[pos[i]], a[i]);
cnt = pos[n];
for (int i = 1; i <= cnt; i++) L[i] = (i - 1) * len + 1, R[i] = min(n, i * len);
}
void pushup(int p) {
sum[p] = mx[p] = 0;
for (int i = L[p]; i <= R[p]; i++)
sum[p] += a[i], mx[p] = max(mx[p], a[i]);
}
void upd(int l, int r) {
int p = pos[l], q = pos[r];
if (p == q) {
if (mx[p] <= 1) return ;
for (int i = l; i <= r; i++) a[i] = sqrt(a[i]);
pushup(p);
return ;
}
for (int i = p + 1; i <= q - 1; i++) {
if (mx[i] == 1) continue;
mx[i] = sum[i] = 0;
for (int j = L[i]; j <= R[i]; j++) a[j] = sqrt(a[j]);
pushup(i);
}
for (int i = l; i <= R[p]; i++) a[i] = sqrt(a[i]);
for (int i = L[q]; i <= r; i++) a[i] = sqrt(a[i]);
pushup(p), pushup(q);
}
int qry(int l, int r) {
int p = pos[l], q = pos[r];
ll ret = 0;
if (p == q) {
for (int i = l; i <= r; i++) {
ret += a[i];
}
return ret;
}
for (int i = p + 1; i <= q - 1; i++) { ret += sum[i]; }
for (int i = l; i <= R[p]; i++) ret += a[i];
for (int i = L[q]; i <= r; i++) ret += a[i];
return ret;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", a + i);
init();
for (int i = 1, op, l, r, c; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
upd(l, r);
} else {
printf("%d\n", qry(l, r));
}
}
return 0;
}
LOJ 数列分块入门 6
单点插入,单点查询,数据随机。
平衡插入和查询的很好的方法是使用块状链表。
就是对链表分块,使跳转次数和修改次数都在 \(O(\sqrt N)\) 级别。
先放一个暴力乱搞:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 5;
int n, a[N];
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", a + i);
for (int op, l, r, c, i = 1; i <= n; i++) {
scanf("%d%d%d%d", &op, &l, &r, &c);
if (op == 0) {
memmove(a + l + 1, a + l, sizeof(int) * (n - l + 1));
a[l] = r;
} else {
printf("%d\n", a[r]);
}
}
}
| Accepted | 100 | #6282. 数列分块入门 6 LewisLi | 289 ms | 688 K | C++ / 477 B | 11/21 11:25:21 |
|---|
可以看到,非常优秀。压位还能更快。
分块代码以后再补。
LOJ 数列分块入门 7
区间加,区间乘,区间和。
题目出现这种要求时,往往需要将两种操作分开,并分别算贡献。
具体来讲,我们要维护的值有:区间和、区间加法标记、区间乘法标记。
如果我们先加了 \(d\) 再乘 \(m\) ,我们会把 \(d\) 先加到加法标记里,然后乘 \(m\) 的时候把这 \(d\) 的贡献翻 \(m\) 倍,最后查询的时候先算乘法再算加法。
更一般的,对于任何一个环 \((R,+,\cdot)\),我们都可以考虑这样维护。
然后还要考虑零散的暴力:对于加法而言,我们不能直接加到值上,因为原来是有乘法标记的。
这里需要暴力把标记下传到原序列里。

浙公网安备 33010602011771号