zoukankan      html  css  js  c++  java
  • 数列分块入门

    这个人讲的特别好:「分块」数列分块入门1 – 9 by hzwer

    分块是一种暴力数据结构,可以处理区间操作等问题。

    分块,就是分很多块来进行处理,这样查询时可以直接调用整块信息+暴力查询左右两端信息,将线性的枚举优化。

    要分块,首先要确定块的大小。

    一般来说,块的大小都为√n。这样可以保证整个序列被分成√n块,查询的时间复杂度可以达到平均。

    分块算法中需要记录的信息有:

    int sum[maxn]; //一个块的和
    int l[maxn]; //一个块的左端点(起点)
    int r[maxn]; //一个块的右端点(终点)
    int w[maxn]; //原数组
    int bel[maxn]; //第x个元素属于第bel[i]个块
    int siz; //块的大小,一般siz = sqrt(n)就可以了

    初始分块

    void build(int x){
        for(int i = 1;i <= n;i++)
            bel[i] = (i-1)/siz+1;
        for(int i = 1;i <= bel[x];i++){
            l[i] = (i-1)*siz+1;
            r[i] = min(i*siz,x);
            sum[i] = 0;
        }
    }

    区间修改

      先修改左端的不完整块,如果右边还有不完整块就再修改右边,最后修改整块的

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

      如果是区间乘法,需要每次暴力修改两端时先把lazy标记清除掉,

      修改整块的lazy标记时,默认乘法优先,所以需要每次把加法的lazy标记也乘以要修改的数。

      注意乘法的lazy标记要初始为1而不是0。

    void reset(int x){
        for(int i = l[x];i <= r[x];i++)
            w[i] = (w[i]*mlaz[x] + alaz[x]) %mod;
        alaz[x] = 0;
        mlaz[x] = 1;
    }
    
    void multiply(int a,int b,int c){
        reset(bel[a]);
        for(int i = a;i <= min(r[bel[a]],b);i++)
            w[i] *= c;
        if(bel[a] != bel[b]){
            reset(bel[b]);
            for(int i = l[bel[b]];i <= b;i++)
                w[i] *= c;
        }
        for(int i = bel[a]+1;i <= bel[b]-1;i++){
            mlaz[i] *= c;
            alaz[i] *= c;
        }
    }
      区间修改(乘法)  

     区间查询

      其实一般lazy*siz就行了,但是r-l+1可以处理最后的完整块不足siz之类的情况(不过究竟会不会出现呢...)

    int query(int a,int b){
        int ans = 0;
        for(int i = a;i <= min(r[bel[a]],b);i++)
            ans += w[i] + lazy[bel[a]];
        if(bel[a] != bel[b])
            for(int i = l[bel[b]];i <= b;i++)
                ans += w[i] + lazy[bel[b]];
        for(int i = bel[a]+1;i <= bel[b]-1;i++)
            ans += sum[i] + (r[bel[i]]-l[bel[i]]+1)*lazy[i];
        return ans;

    拓展

    为什么要用分块?因为树状数组和线段树不好写

    当树状数组和线段树不能直接套板子,需要比较大的改动,而分块看起来比较好膜改而且大概可以解决时

    比如——

    1.给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。

    借用hzwer说的 数列简单分块问题实际上有三项东西要我们思考:对于每次区间操作:

    • 不完整的块的O(√n)个元素怎么处理?
    • O(√n)个 整块怎么处理?
    • 要预处理什么信息(复杂度不能超过后面的操作)?

    对于这道题,要求每个块内的数据是有序的,可以考虑用一个vector来维护

    修改函数、lazy标记的用法不变,

    在初始分块和修改两端时需要重置,即把这个块内的数据重新排序。

    vector<int> ve[maxn];
    void reset(int x) {
        ve[x].clear();
        for (int i = l[x]; i <= r[x]; i++) ve[x].push_back(w[i]);
        sort(ve[x].begin(), ve[x].end());
    }
    
    int query(int a, int b, int c) {
        int ans = -1;
        for (int i = a; i <= min(r[bel[a]], b); i++) {
            int now = lazy[bel[a]] + w[i];
            if (now < c)
                ans = max(ans, now);
        }
        if (bel[a] != bel[b])
            for (int i = l[bel[b]]; i <= b; i++) {
                int now = lazy[bel[b]] + w[i];
                if (now < c)
                    ans = max(ans, now);
            }
        for (int i = bel[a] + 1; i <= bel[b] - 1; i++) {
            int d = c - lazy[i];
            vector<int>::iterator it = lower_bound(ve[i].begin(), ve[i].end(), d) - 1;
            if (*it < d && it >= ve[i].begin() && it <= ve[i].end())
                ans = max(ans, *it + lazy[i]);
        }
        return ans;
    }
      数列分块入门3  

    2. 给出一个长为n的数列,以及n个操作,操作涉及单点询问(第x个数),单点插入(第x个数前)

    每次将一个元素插入到某个元素的前面。有了前一道题的经验,很容易想到用vector存块。

    但是这样要膜改的东西就比较多了,因为每个vector的长度都是不同的。

    查询时,无法直接确定它在哪一个块内,需要每次调用当前块的size。

    插入时,首先查询插入的前一个元素在哪一个块内,再把这个元素插入到对应的块里。

    但是当某一个块过大时,需要的暴力操作就会过多,导致时间复杂度爆炸。

    为了避免这种情况,需要引入一个操作:重新分块(重构)

    √n次插入后,重新把数列平均分一下块,重构需要的复杂度为O(n),重构的次数为√n,复杂度没有问题,而且保证了每个块的大小相对均衡。

    当然,也可以当某个块过大时重构,或者只把这个块分成两半。

    struct node {
        int bel, num;
    };
    
    node query(int x) {
        node ans;
        int now = 1;
        while (x > ve[now].size()) x -= ve[now++].size();
        ans.bel = now;
        ans.num = x - 1;
        return ans;
    }
    
    void rebuild() {
        int cnt = 0;
        int q[maxn];
        for (int i = 1; i <= tot; i++) {
            for (int j = 0; j < ve[i].size(); j++) q[++cnt] = ve[i][j];
            ve[i].clear();
        }
        int new_siz = sqrt(cnt);
        for (int i = 1; i <= cnt; i++) ve[(i - 1) / new_siz + 1].push_back(q[i]);
        tot = (cnt - 1) / new_siz + 1;
    }
    
    void insert(int a, int b) {
        node last = query(a);
        ve[last.bel].insert(ve[last.bel].begin() + last.num, b);
        if (ve[last.bel].size() > 2 * siz)
            rebuild();
    }
      数列分块入门6  

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

     ん?(察觉)

    开方不能对区间直接操作,但是暴力算法的时间复杂度显然不够。

    通过查看题解思考可以发现,由于操作只有下开方,几次操作后,一个块内的数字会全部变为0或1,这一块的操作就可以跳过了。

    开一个bool数组记录这一块是否还有不是0或1的数字,否则暴力。其他函数不变。

    void radic(int a,int b){
        if(a > b)swap(a,b);
        for(int i = a;i <= min(r[bel[a]],b);i++){
            if(flag[bel[a]])break;
            sum[bel[a]] -= w[i];
            w[i] = sqrt(w[i]);
            sum[bel[a]] += w[i];
        }
        if(bel[a] != bel[b])
            for(int i = l[bel[b]];i <= b;i++){
                if(flag[bel[b]])break;
                sum[bel[b]] -= w[i];
                w[i] = sqrt(w[i]);
                sum[bel[b]] += w[i];
            }
        for(int i = bel[a]+1;i <= bel[b]-1;i++){
            if(flag[i])continue;
            flag[i] = true;
            sum[i] = 0;
            for(int j = l[i];j <= r[i];j++){
                w[j] = sqrt(w[j]);
                sum[i] += w[j];
                if(w[j] > 1)flag[i] = false;
            }
        }
    }
      Luogu P4145 上帝造题的七分钟2 / 花神游历各国  

    传送:loj hzwer的数列分块入门九题最后一个我还不会写,QAQ

  • 相关阅读:
    【数学】三分法
    【数学】【背包】【NOIP2018】P5020 货币系统
    【数学】【CF27E】 Number With The Given Amount Of Divisors
    【单调队列】【P3957】 跳房子
    【极值问题】【CF33C】 Wonderful Randomized Sum
    【DP】【CF31E】 TV Game
    【神仙题】【CF28D】 Don't fear, DravDe is kind
    【线段树】【CF19D】 Points
    【字符串】KMP字符串匹配
    【二维树状数组】【CF10D】 LCIS
  • 原文地址:https://www.cnblogs.com/mogeko/p/10429391.html
Copyright © 2011-2022 走看看