分块


  1. 参考:

    1. 算法训练营第二章

  2.  

    1. 名称:分块。

    2. 本质:优化后的暴力。

    3. 一些abstract:

      设sn = 根号n。

      树状数组和线段树维护的信息必须满足信息合并特性,即对于区间来说,分成两个区间分别操作和单独对一个区间的操作是一样的。不满足这个特性,则不能使用树状数组和线段树。

      分块几乎可以解决所有区间更新和区间查询问题,但是效率差一些。分块算法就是把所有数据分成若干块,维护块内信息,使得块内查询为O(1)时间,总查询为O(sn)。

      设数据是连续的n个,通常我们将数据分成sn块,前面的块大小都是sn,最后一块可能不足这个值,也无所谓。pos[i]表示位置i所属的块号。对每个块都进行信息维护(暴力/懒标记)(懒标记用来存整个区间都进行的操作,只有不是整个区间的操作时才处理懒标记),分块可以解决如下问题:

      1. 单点更新:所属区间懒标记下传,暴力更新,复杂度O(sn)。

      2. 区间更新:对于待修区间横跨的整个块,打上懒标记即可,显然只有两侧的块可能打不了,则下传二者懒标记,再暴力修改覆盖部分的值。因为整个块的数量不超过sn,并且暴力修改的部分的长度小于2sn,下传懒标记也是暴力改,所以下传的操作数小于2sn,下传完再直接修改不完整的部分,操作数也小于2sn,所以总共要进行的操作小于5sn,复杂度O(sn)。

      3. 区间查询:和区间更新类似,也是整个块的用块存储的信息统计答案,两端不完整的块就暴力扫描,时间复杂度同上,为O(sn)。

  3. 操作:

    1. 预处理:将序列分块,每个块都标记左右端点的下标,存到L[i],R[i]中,对最后一个块,要单独赋值它的R[i]。分块结果如下:

       

      R[4] = n = 10

       int = (int)(sqrt(1.0)), num t;
       if (num) num++;  // 一共n个,分成了num块,每一个块大小为t
       for (int 1; <= num; i++) {
         L[i] = (i-1)*1;  // 第i块的起点
         R[i] t;  // 第i块的终点
       }
       R[num] n;  // 最后一块越界了,需要改回来

      这里以维护区间和为例:

       // sum[i]表示第i块的和,a存原始数据,pos[i]表示第i个数据属于pos[i]块
       
       for (int 1; <= num; i++) {
         for (int L[i]; <= R[i]; j++) {
           pos[j] i;
           sum[i] += a[j];
        }
       }
    2. 区间更新:比如将区间[l, r]区间的元素都加d(假设r不小于l)。

      先求出l和r所属的块:

       int pos[l], pos[r];

      分类讨论:如果同属一块,则直接暴力修改:(这里因为只有加,懒标记只有一种,所以不用先下放懒标记,不然先后顺序需要注意一下,比如有一个乘法的懒标记,再进行加操作,就得先下放乘法的懒标记,再执行这次的加法)

       if (== q) {
         for (int l; i<= r; i++) {
           a[i] += d;
        }
         sum[p] += 1ll * (1);
       }

      不属于同一块,则对中间完全覆盖的块打上懒标记,暴力修改不完整的两个小块:

       else {
         add[i] += d;
         for (int 1; <= 1; i++) {  
           add[i] += d;  // 对中间完全覆盖的块打上懒标记
        }
         // 暴力修改不完整的两个小块
         for (int l; <= R[p]; i++) {
           a[i] += d;
        }
         sum[p] += 1ll * (R[p] 1);
         for (int L[q]; <= r; i++) {
           a[i] += d;
        }
         sum[q] += 1ll * (L[q] 1);
       }
    3. 区间查询:查询[l, r]区间的元素和。

      先求出l和r所属的块:

       int pos[l], pos[r];
       long long ans 0;

      分类讨论:如果同属一块,则直接暴力累加,再加上懒标记上的值。

       if (== q) {
         for (int l; i<= r; i++) {
         ans += a[i];
        }
         ans += 1ll add[p] * (1);
       }

      不属于同一块,则对中间完全覆盖的块打上懒标记,暴力修改不完整的两个小块:

       else {
         for (int 1; <= 1; i++) {  
           ans += sum[i] 1ll add[i] * (R[i] L[i] 1);  // 对中间完全覆盖的块打上懒标记
        }
         // 暴力修改不完整的两个小块
         for (int l; <= R[p]; i++) {
           ans += a[i];
        }
        ans += 1ll add[p] * (R[p] 1);
         for (int L[q]; <= r; i++) {
           ans += a[i];
        }
         sum[q] += 1ll add[q] * (L[q] 1);
       }
       
       printf("%lld", ans);
  4. 例题:

    1. (HDU5057)给出一个数组,有两个操作:

      1. 单点修改某个位置的值

      2. 区间查询[l, r],指出这个区间中从低往高数第p位为q的个数是多少

      给n个数,m个操作(都是十万级别)。对于每个查询,给出答案。

      考虑分块,先预处理最开始的每个区间的每个位的不同的值的数量,这个复杂度是O(10nlog(int))级别的。

      对于查询操作,只需要O(sn)级别。

      对于修改操作,暴力修改即可,是O(1)级别。

      轻松通过。

       #define maxn 100010
       #define sn 333
       // block[i][j][k]表示第i块中第j位有多少个为k的数。
       int a[maxn], pos[maxn], L[sn], R[sn], block[sn][11][10], n, m;
       int ten[11] = {0,1, 10, 100, 1000, 10000, 100000, 100000, 1000000, 10000000, 100000000};  // ten[i]就是第i位需要这个数需要除以多少才能取到
       
       void pre(int n) {
         int = (int)(sqrt(1.0)), num t;
         if (num) num++;
         for (int 1; <= num; i++) {
           L[i] = (1)*1;
           R[i] t;
        }
         R[num] n;
         for (int 1; <= num; i++) {
           for (int L[i]; <= R[i]; j++) {
             pos[j] i;
          }
           int tmp a[i];
           for (int 1; <= 10; j++) {
             block[belong[i]][j][tmp%10]++;
             tmp /= 10;
          }
        }
       }
       
       void update(int x, int v) {  // a[x] = v
         for (int 1; <= 10; i++) {  // 少了一个原来的a[x],退回去
           block[pos[x]][i][a[x]%10]--;
           a[x] /= 10;
        }
         a[x] v;  // 把v的补上来
         for (int 1; <= 10; i++) {
           block[pos[x]][i][v%10]++;
           /= 10;
        }
       }
       
       int query(int l, int r, int p, int q) {  // [l, r] 第p位为q的个数
         int ans 0;
         if (pos[l] == pos[r]) {
           for (int l; <= r; i++) {
             if ((a[i] ten[p]) 10 == q) ans++;
          }
           return ans;
        }
         for (int pos[l] 1; pos[r]; i++) {
           ans += block[i][p][q];
        }
         for (int l; <= R[pos[l]]; i++) {
           if ((a[i] ten[p]) 10 == q) ans++;
        }
         for (int L[pos[r]]; <= r; i++) {
           if ((a[i] ten[p]) 10 == q) ans++;
        }
         return ans;
       }

       

posted on 2022-05-09 18:43  小染子  阅读(168)  评论(0)    收藏  举报