zoukankan      html  css  js  c++  java
  • 区间分块系列

    转自: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;
    }
  • 相关阅读:
    ASP.NET MVC 异常捕获
    Jquery 扩展方法
    Spring.NET笔记1
    ASP.NET MVC Ninject 实现依赖注入
    ASP.NET MVC Unity实现依赖注入
    windows service
    反射用法
    抽象工厂核心反射
    (C#)中的DataSet、string、DataTable等对象转换成Json
    .NET批量删除代码前的行号
  • 原文地址:https://www.cnblogs.com/romaLzhih/p/9489845.html
Copyright © 2011-2022 走看看