分块算法

通常把长度为n的数列分成\sqrt{n}

在进行区间操作前通常需要预处理以下数据:

  1. 数组l,l[i]表示第i块的最左边元素的位置
  2. 数组r,r[i]表示第i块的最右边元素的位置
  3. 数组pos,pos[i]表示第i个元素在第几个块内

注:若是有多余元素,则多余元素算入最后一个块内,如:有10个元素,则块数为\sqrt{10}下取整,也就是3个块,此时分块情况为{1,2,3},{4,5,6},{7,8,9,10}

分块算法的精髓在于,对于一次区间操作,对区间内部的整块进行整体的操作,对区间边缘的零散块单独暴力处理

区间加法,单点查值:数列分块入门 1 

在对区间[left,right]进行加法操作时,我们可以枚举所有与该区间有交集的块,第i个块与修改区间应该有以下两种关系:

  1. left <= l[i] and r[i] <= right,当前块完全包含于修改区间,我们使add[i] += c,表示第i个块全体元素都加上了c

  2. 当前块与修改区间有部分交集,我们直接对这块零散区间进行暴力修改即可,修改次数不会超过\sqrt{n}

#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            add[i] += c;
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                d[j] += c;
            }
        }
    }
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (int i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
    }
    while (n--) {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r, c);
        } else {
            cout << d[r] + add[pos[r]] << endl;
        }
    }
    return 0;
}

查询区间小于某个值的个数:数列分块入门 2

维护一个对块内元素进行排序的数组sd

进行区间加法操作时会出现以下两种情况:

  1. left <= l[i] and r[i] <= right,当前块完全包含于修改区间,我们使add[i] += c,表示第i个块全体元素都加上了c
  2. 当前块与修改区间有部分交集,我们直接对这块零散区间进行暴力修改,然后重新将它赋值到sd数组中,对sd数组中这个块所在位置重新排序

进行询问操作时,同样会出现两种情况:

  1. left <= l[i] and r[i] <= right,当前块完全包含于查询区间,我们在块内二分出小于查询元素的个数,并计入总计数中
  2. 当前块与查询区间有部分交集,我们直接对这块零散区间进行暴力计数,并计入总计数中
#include <bits/stdc++.h>
using namespace std;
int d[100005], sd[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            add[i] += c;
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                d[j] += c;
            }
            for (int j = l[i]; j <= r[i]; j++) {
                sd[j] = d[j];
            }
            sort(sd + l[i], sd + r[i] + 1);
        }
    }
}
int ask(int left, int right, int c) {
    int cnt = 0;
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            cnt += lower_bound(sd + l[i], sd + r[i] + 1, c * c - add[i]) - (sd + l[i]);
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                if (d[j] + add[i] < c * c) {
                    cnt++;
                }
            }
        }
    }
    return cnt;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
        sd[i] = d[i];
    }
    m = sqrt(n);
    for (int i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
        sort(sd + l[i], sd + r[i] + 1);
    }
    while (n--) {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r, c);
        } else {
            cout << ask(l, r, c) << endl;
        }
    }
    return 0;
}

区间查询某个值的前驱:数列分块入门 3

思路和上面第2题相差无几了,直接块内排序+二分 

#include <bits/stdc++.h>
using namespace std;
int d[100005], sd[100005], pos[100005], l[333], r[333], add[333], n, m;
void change(int left, int right, int c) {
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            add[i] += c;
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                d[j] += c;
            }
            for (int j = l[i]; j <= r[i]; j++) {
                sd[j] = d[j];
            }
            sort(sd + l[i], sd + r[i] + 1);
        }
    }
}
int ask(int left, int right, int c) {
    int res = INT_MIN;
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            int p = lower_bound(sd + l[i], sd + r[i] + 1, c - add[i]) - sd - 1;
            if (left <= p && p <= right && sd[p] + add[i] < c) {
                res = max(res, sd[p] + add[i]);
            }
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                if (d[j] + add[i] < c) {
                    res = max(res, d[j] + add[i]);
                }
            }
        }
    }
    return res == INT_MIN ? -1 : res;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
        sd[i] = d[i];
    }
    m = sqrt(n);
    for (int i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
        sort(sd + l[i], sd + r[i] + 1);
    }
    while (n--) {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r, c);
        } else {
            cout << ask(l, r, c) << endl;
        }
    }
    return 0;
}

区间加法,区间求和:数列分块入门 4

这个比上面2、3题还简单,只需要在第1题的基础上加一个sum数组,记录每个块内元素的总和,当第i个块完全包含于查询区间时,把sum[i]加到答案里。零散区间就直接暴力

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], add[333], sum[333], n, m;
void change(ll left, ll right, ll c) {
    for (ll i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            add[i] += c;
            sum[i] += c * (r[i] - l[i] + 1);
        } else {
            for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
                d[j] += c;
                sum[i] += c;
            }
        }
    }
}
ll ask(ll left, ll right) {
    ll res = 0;
    for (ll i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            res += sum[i];
        } else {
            for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
                res += d[j] + add[i];
            }
        }
    }
    return res;
}
int main() {
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (ll i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (ll i = 1; i <= m; i++) {
        ll t = 0;
        for (ll j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
            t += d[j];
        }
        sum[i] = t;
    }
    while (n--) {
        ll op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r, c);
        } else {
            cout << (ask(l, r) % (c + 1)) << endl;
        }
    }
    return 0;
}

区间开方,区间求和:数列分块入门 5

首先需要注意到2^{31}开方5次就会变成1,然后继续开方就没意义了 

所以可以考虑设一个flag数组,记录块内元素是不是全部为0或1

对于区间开方:

  • 若是对整块操作,则先看flag[i]是否为true,如果为true,那说明区间内元素都是0或1,对0或1开方等于没开方,所以此时直接跳过就行;如果flag[i]为false,那就暴力对区间所有数进行开方,同时看看是不是开方后能使所有数都为0或1,显然这个暴力过程不会进行太多次,因为一个数最多开方5次就会变成1
  • 若是对零散块操作,直接暴力开方

对于区间求和:

  • 若是对整块操作,把sum[i]加到答案里
  • 若是对零散块操作,暴力
#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[333], r[333], flag[333], sum[333], n, m;
void change(int left, int right) {
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            if (flag[i]) {
                continue;
            } else {
                bool fa = true;
                for (int j = l[i]; j <= r[i]; j++) {
                    int diff = d[j] - (int)sqrt(d[j]);
                    d[j] -= diff;
                    sum[i] -= diff;
                    if (d[j] > 1) fa = false;
                }
                if (fa) flag[i] = 1;
            }
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                int diff = d[j] - (int)sqrt(d[j]);
                d[j] -= diff;
                sum[i] -= diff;
            }
        }
    }
}
int ask(int left, int right) {
    int res = 0;
    for (int i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            res += sum[i];
        } else {
            for (int j = max(l[i], left); j <= min(r[i], right); j++) {
                res += d[j];
            }
        }
    }
    return res;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (int i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        int t = 0;
        for (int j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
            t += d[j];
        }
        sum[i] = t;
    }
    while (n--) {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r);
        } else {
            cout << ask(l, r) << endl;
        }
    }
    return 0;
}

单点插入,单点查询:数列分块入门 6

这题数据是随机的,所以用纯vector实现也是可以过的

纯vector插入O(n),查询O(1)

现在说说vector + 分块的做法,复杂度方面插入O(\sqrt{n}),查询(\sqrt{n})。在这个方法中每个块的元素都用一个vector来存,对于每个插入操作,先找到插入位置所在的块,然后再暴力插入;对于每个查询操作也是一样,先找到在哪个块,再输出那个元素

但这方法有个漏洞,如果某个块有大量的单点插入,那这个方法就会退化成纯vector的方法了,所以这里要引入一个操作——重新分块,当块的大小大过某个值的时候,直接把那个块切成两半,引入重新分块后大概快了400ms吧

#include <bits/stdc++.h>
using namespace std;
int d[100005], l[333], r[333], n, m;
vector<vector<int>> v(333);
void rebuild(int pos) {
    vector<int> t;
    int n = v[pos].size();
    for (int i = 0; i < n / 2; i++) {
        t.push_back(v[pos].back());
        v[pos].pop_back();
    }
    reverse(t.begin(), t.end());
    v.insert(v.begin() + pos + 1, t);
}
void change(int pos, int c) {
    int i = 1;
    while (pos > v[i].size()) {
        pos -= v[i].size();
        i++;
    }
    v[i].insert(v[i].begin() + pos - 1, c);
    if (v[i].size() > 10 * m) {
        rebuild(i);
    }
}
int ask(int pos) {
    int i = 1;
    while (pos > v[i].size()) {
        pos -= v[i].size();
        i++;
    }
    return v[i][pos - 1];
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (int i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            v[i].push_back(d[j]);
        }
    }
    while (n--) {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change(l, r);
        } else {
            cout << ask(r) << endl;
        }
    }
    return 0;
}

区间乘法,区间加法,单点询问:数列分块入门 7

这题需要维护两个标记,一个乘法标记,一个加法标记

需要考虑两个标记的优先级,显然设置乘法比加法优先级高比较好思考

对于第i个元素的值 = d[i] * mul[pos[i]] + add[pos[i]]

区间加法:

  • 对整块:加法标记加个c
  • 对零散块:先把这整块的两个标记都下传,先传乘法标记,再传加法标记,然后将零散区间的每个元素加上c

区间乘法:

  • 对整块:乘法标记和加法标记都乘上c 
  • 对零散块:先把这整块的两个标记都下传,先传乘法标记,再传加法标记,然后将零散区间的每个元素乘上c
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], add[333], mul[333], n, m, mod = 10007;
void change_add(ll left, ll right, ll c) {
    for (ll i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            (add[i] += c) %= mod;
        } else {
            for (ll j = l[i]; j <= r[i]; j++) {
                (d[j] *= mul[i]) %= mod;
                (d[j] += add[i]) %= mod;
            }
            mul[i] = 1;
            add[i] = 0;
            for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
                (d[j] += c) %= mod;
            }
        }
    }
}
void change_mul(ll left, ll right, ll c) {
    for (ll i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            (mul[i] *= c) %= mod;
            (add[i] *= c) %= mod;
        } else {
            for (ll j = l[i]; j <= r[i]; j++) {
                (d[j] *= mul[i]) %= mod;
                (d[j] += add[i]) %= mod;
            }
            mul[i] = 1;
            add[i] = 0;
            for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
                (d[j] *= c) %= mod;
            }
        }
    }
}
int main() {
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (ll i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (ll i = 1; i <= m; i++) {
        for (ll j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
        mul[i] = 1;
    }
    while (n--) {
        ll op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0) {
            change_add(l, r, c);
        } else if (op == 1) {
            change_mul(l, r, c);
        } else {
            cout << (d[r] * mul[pos[r]] + add[pos[r]]) % mod << endl;
        }
    }
    return 0;
}

区间询问等于c的元素个数,并将这个区间的所有元素改为c:数列分块入门 8

这题需要一个flag数组,用flag[i]表示第i个块里的元素全部是flag[i]。规定flag[i] == 1e18,表示这个数组里的元素不是同一个元素

对整块:

  • 块内元素相同,且flag[i] == c,那么块里元素都是查询元素,则把块的长度加到答案里
  • 块里元素不相同,则暴力查询块内元素有多少个是c,然后令flag[i] = c,表示将块内元素全部改为c

对零散块:

  • 注意若是零散块所在块flag[i] != 1e18,那么要先将标记下传并清空,然后再暴力查询并修改

可能会有同学觉得对整块内暴力复杂度过高,但其实暴力一次过后,那个块就会全部变成同一个元素,之后对这个块的操作就基本没有暴力的情况,只有这个块被当成零散块操作,之后才可能再对它进行暴力,但每次区间操作最多产生两个零散块,这样综合来看复杂度应该是没问题的。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll d[100005], pos[100005], l[333], r[333], flag[333], n, m;
ll ask(ll left, ll right, ll c) {
    ll res = 0;
    for (ll i = pos[left]; i <= pos[right]; i++) {
        if (left <= l[i] && r[i] <= right) {
            if (flag[i] != 1e18) {
                if (flag[i] == c) {
                    res += r[i] - l[i] + 1;
                }
            } else {
                for (ll j = l[i]; j <= r[i]; j++) {
                    if (d[j] == c) {
                        res++;
                    }
                }
            }
            flag[i] = c;
        } else {
            if (flag[i] != 1e18) {
                for (ll j = l[i]; j <= r[i]; j++) {
                    d[j] = flag[i];
                }
                flag[i] = 1e18;
            }
            for (ll j = max(l[i], left); j <= min(r[i], right); j++) {
                if (d[j] == c) {
                    res++;
                }
                d[j] = c;
            }
        }
    }
    return res;
}
int main() {
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> d[i];
    }
    m = sqrt(n);
    for (ll i = 1; i <= m; i++) {
        l[i] = m * (i - 1) + 1;
        r[i] = m * i;
    }
    r[m] = n;
    for (ll i = 1; i <= m; i++) {
        for (ll j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
        flag[i] = 1e18;
    }
    while (n--) {
        ll l, r, c;
        cin >> l >> r >> c;
        cout << ask(l, r, c) << endl;
    }
    return 0;
}

区间的最小众数:数列分块入门 9

???心态炸裂 ???

本题数据挺强的,调了很久,终于不WA了,不过还是TLE,WA和TLE加起来有12页了,不想写了,有空再回来改吧

思路如下:

一个区间的最小众数可能是以下两种情况:

  1. 所有完整块的最小众数
  2. 零散块里的某个数

可以使用unordered_map<int, vector<int>> c,存放某个数出现的下标

这样要查询某个数出现的次数,直接在vector里二分就行

还要预处理二维数组f,f[i][j]表示块i到块j的最小众数

如果分块超时可以适当调整块长,调整方式见下方代码

#include <bits/stdc++.h>
using namespace std;
int d[100005], pos[100005], l[2333], r[2333], f[2333][2333], n, m;
unordered_map<int, vector<int>> c;
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
 
template <typename T> void print(T x) {
	if (x < 0) { putchar('-'); print(x); return ; }
    if (x >= 10) print(x / 10);
    putchar((x % 10) + '0');
}
int get(int left, int right, int v) {
    return upper_bound(c[v].begin(), c[v].end(), right) - lower_bound(c[v].begin(), c[v].end(), left);
}
int ask(int left, int right) {
    int le = (left == l[pos[left]] ? pos[left] : pos[left] + 1);
    int ri = (right == r[pos[right]] ? pos[right] : pos[right] - 1);
    int res = f[le][ri], max_cnt = get(left, right, res);
    for (int i = left; i <= r[pos[left]]; i++) {
        int cnt = get(left, right, d[i]);
        if (cnt > max_cnt || (cnt == max_cnt && res > d[i])) {
            res = d[i];
            max_cnt = cnt;
        }
    }
    for (int i = l[pos[right]]; i <= right; i++) {
        int cnt = get(left, right, d[i]);
        if (cnt > max_cnt || (cnt == max_cnt && res > d[i])) {
            res = d[i];
            max_cnt = cnt;
        }
    }
    return res;
}
int main() {
    read(n);
    for (int i = 1; i <= n; i++) {
        read(d[i]);
        c[d[i]].push_back(i);
    }
    int block_len = 80;
    int m = (n + block_len - 1) / block_len;
    for (int i = 1; i <= m; i++) {
        l[i] = block_len * (i - 1) + 1;
        r[i] = block_len * i;
    }
    r[m] = n;
    for (int i = 1; i <= m; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            pos[j] = i;
        }
    }
    for (int i = 1; i <= m; i++) {
        unordered_map<int, int> cn;
        int num, max_cnt = 0;
        for (int j = i; j <= m; j++) {
            for (int k = l[j]; k <= r[j]; k++) {
                cn[d[k]]++;
                if (cn[d[k]] > max_cnt || (cn[d[k]] == max_cnt && num > d[k])) {
                    num = d[k];
                    max_cnt = cn[d[k]];
                }
            }
            f[i][j] = num;
        }
    }
    while (n--) {
        int l, r;
        read(l), read(r);
        print(ask(l, r));
        puts("");
    }
    return 0;
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值