『笔记』分块与块状数组

\[\textsf{分块,一种优雅的暴力。} \]

定义

对于一系列数据,通过适当的划分,预处理一部分信息并保存下来,用空间换取时间,达到时空平衡。

思想

核心思想还是暴力

把一个整体划分为若干个小块,对整块整体处理,零散块单独处理。



“大段维护,局部朴素”

事实上,分块更接近于“朴素”,效率往往比不上线段树和树状数组,但是他更通用,更容易实现。(对我这样的蒟蒻更加友好)

块状数组

定义

一种基于分块思想处理区间的数据结构。

思想

一个长度为 \(n\) 的数组,将其划分为相同的 \(a\) 块,每块的长度为 \(\frak{n}{a}\) 。显然有时会存在不能完整划分的区块,那么将其单独处理。

即对于一次区间的操作,对区间内部的整块进行整体的操作,而对于区间边缘的零散块单独暴力处理。

正是所谓“优雅的暴力”。

然而,块数的确定又是一个问题。

  • 块数太多,则块的长度就会过短,那么就失去了整体处理的意义。

  • 块数太少,则我们需要处理的零散块就太多,又会花费大量的时间。

为了解决以上问题,一般来说,我们取块数为 \(\sqrt{n}\) ,这样即便是在最坏情况下,我们也只需要对大约 \(\sqrt{n}\) 个整块、 长度为 \(\frac{2 n}{\sqrt{n}}=2 \sqrt{n}\) 的零散块进行处理,总时间复杂度为 \(O(\sqrt{n})\) ,可以接受。

所以,分块可以说是一种根号级算法

优劣势

很显然,分块思想的时间复杂度比不上线段树和树状数组这些可以达到对数级的算法,但是由此换来的,是更高的灵活性和对新手菜鸡蒟蒻(((比如我更良的写代码体验

与线段树不同,块状数组不要求所维护信息必须满足结合律,也不用一遇到大的数据就一层层地传递标记。

但是它们又有相似之处,可以这么理解,线段树是以可高度约为 \(\log_{2}{n}\) 的树,而块状数组则可以看作是一棵高度仅为 \(3\) 的树,而且最顶层的信息不需用维护。

预处理

使用块状数组前,先划分出每个块所占据的范围。

struct Block
{
    int l;
    int r;
} b[_];
int num = sqrt(n);
for (int i = 1; i <= num; i++)
{
    b[i].l = n / num * (i - 1) + 1; // b[i].l 表示 i 号块的第一个元素的下标
    b[i].r = n / num * i;           // b[i].r 表示 i 号块的最后一个元素的下标
}

但是,由于数组的长度并不一定是一个完全平方数,那么我们把遗漏下的那一小块放入最后一个块中,此后对它单独处理。

b[num].r = n;

然后为每个元素确定其所属的块。

for (int i = 1; i <= num; i++)
    for (int j = b[i].l; j <= b[i].r; j++)
        id[j] = i; // 表示 j 号元素归属于第 i 块

如果需要,我们还可以预处理

  • 每个块的大小

    for (int i = 1; i <= num; i++)
        b[i].siz = b[i].r - b[i].l + 1;
    
  • 每个块包含数据的和

    for (int i = 1; i <= lth; i++)
        for (int j = b[i].l; j <= b[i].r; j++)
            b[i].sum += a[j]; // b[i].sum 表示第 i 个块所包含的元素的和,a[i] 表示 i 号元素的值
    
  • \(And\ so\ on\)

其他操作

接下来用一道线段树的题目说明块状数组的其他操作。

P3372 【模板】线段树 1

读入和预处理数据的和

    for (int i = 1; i <= n; i++)
        a[i] = read();
    for (int i = 1; i <= num; i++)
        for (int j = b[i].l; i <= b[i].r; j++)
            b[i].sum += a[i];

区间修改

  • \(x\)\(y\) 在同一块内时,直接暴力修改原数组,然后更新所在块的 \(sum\)

    if (id[x] == id[y])
        for (int i = x; i <= y; i++)
        {
            a[i] += k;
            sum[id[i]] += k;
        }
    
  • 若不在同一个块内

    • 先暴力修改左右两边的零散区间

      for (int i = x; i <= b[id[x]].r; i++)
      {
          a[i] += k;
          b[id[i]].sum += k;
      }
      for (int i = b[id[y]].l; i <= y; i++)
      {
          a[i] += k;
          b[id[i]].sum += k;
      }
      
    • 在对中间的整块打上标记

      for (int i = id[x] + 1; i <= id[y]; i++)
          b[i].lzy += k
      

区间查询

  • 同样地,如果左右两边在同一块,直接暴力计算区间和。
if (id[x] == id[y])
        for (int i = x; i <= y; i++)
            ans += a[i] + b[id[i]].lzy; // 注意要加上标记
  • 否则

    • 先暴力计算零碎块

      for (int i = x; i <= b[id[x]].r; i++)
              ans += a[i] + b[id[i]].lzy;
      for (int i = b[id[y]].l; i <= y; i++)
          ans += a[i] + b[id[i]].lzy;
      
    • 再处理整块:

      for (int i = id[x] + 1; i < id[y]; i++)
          ans += sum[i] + b[i].lzy * b[i].siz; // 注意标记要乘上块长
      

然后这道题差不多就结束了~

最后

分块还是要和莫队结合的吧

完结!

原文地址:https://www.cnblogs.com/Frather/p/14590351.html