分块

分块思想

一、性质与证明

分块,故名思义,是将一个区间分成几个块,然后对于每个询问,整合一个或多个(甚至全部区间)的信息,但这种分块和整合是有技巧性的,否则很难有效地降低时间复杂度。

先来看一道例题:

老方有一个长度为 n 的序列,被她的学生拜托完成以下三个操作:

  1. 修改某位置的元素值
  2. 将一段区间的元素加上或减去一个值
  3. 求一个区间内的最大最小值

共有 m 个这样的操作,保证 n, m <= 50000

考虑分块。

我们考虑将整个序列划分为 sqrt(n) 块,因为这样可以使得总查询时间最短。

证明如下:

​设这个序列的完整分块数是 C 块,每一个完整个分块都包含 S 个元素,那么显然可能存在一种情况即在区间 [l, r] 的两端包含有不完整的分块。

如图,蓝色部分即为不完整的分块。

可以发现该序列内有 C 个完整分块和两个不完整分块,且这两个不完整分块内元素数量之和小于 2S ,则对于一个序列而言,我们要进行的查询总数的极限为 C + 2S ,则时间复杂度为 O(C + 2S) ,也就相当于 O(max{C, S}) .

因此,由于 C * S + 2 * S >= r - l + 1 ,则可以近似的看作当且仅当 S == C 时,时间复杂度可以取得最小上界, 此时 C == S == sqrt(n) .

二、具体实现

初始化

考虑用 belong[] 来表示每个点与分块之间的关系。

为了便于阅读,除最终整合实现的代码外,其余代码块一律省略头文件

且在 vscode 中,如出现 "end"不明确 这一类的报错,可以在相应变量名前加 ```::`` 即区域标识符。

#define MAXN 50000 + 5

int n, a[MAXN];
int S, C = 0, st[MAXN], end[MAXN]; // st[]:每个块的左边界,end[]:每个块的右边界
int belong[MAXN]; // 记录每个元素与所分块的关系

void init() {
    S = int(sqrt(double(n)));

    for(int i = 1; i <= n; i += S) {
        st[++C] = i;
        end[C] = min(n, i + S - 1); 
    }
    /*
    也可以写成:C = n / S; if(n % S) C++;
	*/
    
    for(int i = 1; i <= C; i++) {
        for(int j = st[i]; j <= end[i]; j++) {
            belong[j] = i;
            /*
            我们还可以在这里进行其他一系列区间操作:
            sum[i] += a[j];
            maxx[i] = max(maxx[i], a[j]);
            */
        }
    }
}

接下来是区间修改和查询操作。似乎每个数据结构都绕不过这几个

修改操作

考虑对于原序列的单点 x 修改除了会影响到 a[x] ,还会影响到所在分块的和、最大值等系列区间信息。实际上很好实现。

inline void updata(int x, int y) { 
    int home = belong[x];
    a[x] += y;
    sum[home] += y;
    return;
}

查询操作

考虑分治,分两种情况考虑:a. [x, y] 在一个块内;b.[x, y]不在一个块内。

a. [x, y] 在一个块内

如图所示,要查询区间[x, y]的信息,非常简单,直接暴力即可。

    if(belong[x] == belong[y]) {
        int ans = 0;
        for(int i = x; i <= y; i++) {
            ans += a[i];
        }
        return ans;
    } 
b.[x, y]不在一个块内

如图所示,我们发现,区间[x, y]的和即就是 [x, 绿色块1尾] + 蓝色块 + [绿色块2头,y]

所以我们可以将绿色部分暴力,蓝色部分直接查询。

 	if(belong[x] != belong[y]) {
        for(int i = x; i <= ::end[belong[x]]; i++) {
            ans += a[i];
        }
        for(int i = belong[x] + 1; i <= belong[y] - 1; i++) {
            ans += sum[i];
        }
        for(int i = st[belong[y]]; i <= y; i++) {
            ans += a[i];
        }
        return ans;
    }

这样便可以完美的解决查询问题。

三、相应习题

[luogu P3374] 树状数组1

[luogu P2357] 守墓人

实际上,这两道题都是板子题,只要大家看懂了上面的知识,这两道题乱杀。

四、后话

老方很开心,因为你们帮她解决了一个大难题.......(逃)

其实分块的时间复杂度为 O(sqrt(n) * m) ,慢于树状数组和线段树,同学们可以在掌握分块思想后去学习树状数组和线段树。

注意,分块是种思想!

posted @ 2021-07-27 11:29  许江一墨  阅读(121)  评论(0编辑  收藏  举报