区间分块系列

转自:http://hzwer.com/8053.html
很好的分块知识讲解。

可能涉及的几个词语解释:
区间:数列中连续一段的元素
区间操作:将某个区间[a,b]的所有元素进行某种改动的操作
块:我们将数列划分成若干个不相交的区间,每个区间称为一个块
整块:在一个区间操作时,完整包含于区间的块
不完整的块:在一个区间操作时,只有部分包含于区间的块,即区间左右端点所在的两个块

例题1:
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。

这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。
数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。

以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。

这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。

#include<cstdio>
#include<algorithm>
using namespace std;

int n, blo;
int v[50005], bl[50005], atag[50005];

void add(int a, int b, int c)
{
    for (int i = a; i <= min(bl[a] * blo, b); i++)
        v[i] += c;
    if (bl[a] != bl[b])
        for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
            v[i] += c;
    for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
        atag[i] += c;
}

int main()
{
    scanf("%d", &n); blo = sqrt(n);
    int tmp;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &tmp);
        v[i] = tmp;
    }
    for (int i = 1; i <= n; i++)bl[i] = (i - 1) / blo + 1;
    for (int i = 1; i <= n; i++)
    {
        int opt, a, b, c;
        scanf("%d%d%d%d", &opt, &a, &b, &c);
        if (opt == 0) add(a, b, c);
        if (opt == 1) printf("%d
", v[b] + atag[bl[b]]);
    }

    return 0;
}

例题2:
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法。

不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。

分块的调试检测技巧:可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。

#include<set>
#include<cmath>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n, blo;
const int maxn = 100000 + 5;
int v[maxn], bl[maxn], atag[maxn];
set<int>st[maxn];

void add(int a, int b, int c)
{
    for (int i = a; i <= min(bl[a] * blo, b); i++)
    {
        st[bl[a]].erase(v[i]);
        v[i] += c;
        st[bl[a]].insert(v[i]);
    }
    if (bl[a] != bl[b])
    {
        for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
        {
            st[bl[b]].erase(v[i]);
            v[i] += c;
            st[bl[b]].insert(v[i]);
        }
    }
    for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
        atag[i] += c;
}

int query(int a, int b, int c)
{
    int ans = -1;
    for (int i = a; i <= min(bl[a] * blo, b); i++)
    {
        int val = v[i] + atag[bl[a]];
        if (val<c)ans = max(val, ans);
    }
    if (bl[a] != bl[b])
        for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
        {
            int val = v[i] + atag[bl[b]];
            if (val<c)ans = max(val, ans);
        }
    for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
    {
        int x = c - atag[i];
        set<int>::iterator it = st[i].lower_bound(x);
        if (it == st[i].begin())continue;
        --it;
        ans = max(ans, *it + atag[i]);
    }
    return ans;
}
int main()
{
    scanf("%d", &n);
    blo = 1000;
    for (int i = 1; i <= n; i++) {
        int tmp;
        scanf("%d", &tmp);
        v[i] = tmp;
    }
    for (int i = 1; i <= n; i++)
    {
        bl[i] = (i - 1) / blo + 1;
        st[bl[i]].insert(v[i]);
    }
    for (int i = 1; i <= n; i++)
    {
        int opt, a, b, c;
        scanf("%d%d%d%d", &opt, &a, &b, &c);
        if (opt == 0)add(a, b, c);
        if (opt == 1)printf("%d
", query(a, b, c));
    }
    return 0;
}

例题3:
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。

稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。

看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。

如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。

如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。

这样每个元素至多被开方不超过4次,显然复杂度没有问题

#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;

int n, blo;
const int maxn = 50000 + 5;
int bl[maxn];
int v[maxn], sum[maxn], flag[maxn];

void solve_sqrt(int x) 
{
    if (flag[x])return;
    flag[x] = 1;
    sum[x] = 0;
    for (int i = (x - 1)*blo + 1; i <= x*blo; i++)
    {
        v[i] = sqrt(v[i]), sum[x] += v[i];
        if (v[i]>1)flag[x] = 0;
    }
}

void add(int a, int b, int c)
{
    for (int i = a; i <= min(bl[a] * blo, b); i++)
    {
        sum[bl[a]] -= v[i];
        v[i] = sqrt(v[i]);
        sum[bl[a]] += v[i];
    }
    if (bl[a] != bl[b])
        for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
        {
            sum[bl[b]] -= v[i];
            v[i] = sqrt(v[i]);
            sum[bl[b]] += v[i];
        }
    for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
        solve_sqrt(i);
}

int query(int a, int b)
{
    int ans = 0;
    for (int i = a; i <= min(bl[a] * blo, b); i++)
        ans += v[i];
    if (bl[a] != bl[b])
        for (int i = (bl[b] - 1)*blo + 1; i <= b; i++)
            ans += v[i];
    for (int i = bl[a] + 1; i <= bl[b] - 1; i++)
        ans += sum[i];
    return ans;
}

int main()
{
    scanf("%d", &n);
    blo = sqrt(n);
    int tmp;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &tmp);
        v[i] = tmp;
    }
    for (int i = 1; i <= n; i++)
    {
        bl[i] = (i - 1) / blo + 1;
        sum[bl[i]] += v[i];
    }
    for (int i = 1; i <= n; i++)
    {
        int opt, a, b, c;
        scanf("%d%d%d%d", &opt, &a, &b, &c);
        if (opt == 0) add(a, b, c);
        if (opt == 1) printf("%d
", query(a, b));
    }
    return 0;
}
原文地址:https://www.cnblogs.com/romaLzhih/p/9489845.html