Loading

分块快速入门

基本思想

有一句老话,叫“大段维护,局部朴素”。

其实就是将一些东西人为的分为若干块,然后每个块整体的维护一些东西,小范围内直接暴力做,做到时空平衡。

其实和根号分治有点像。

然后整除分块和莫队我觉得和传统分块差别有点大了,就没写。

数列分块我一般的写法是维护每个位置所在块编号,块的左端点和块的右端点。

数列分块入门题

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)\),我们都可以考虑这样维护。

然后还要考虑零散的暴力:对于加法而言,我们不能直接加到值上,因为原来是有乘法标记的。

这里需要暴力把标记下传到原序列里。

posted @ 2022-11-20 23:33  LewisLi  阅读(42)  评论(0)    收藏  举报