【学习笔记】分块

也许好看一点的观看方式

分块

分块牛逼普拉斯

简介

基本思想

分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度.

分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度.

分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息.

比如不满足可加性的东西

思考

分块是维护序列的数据结构,是一种“准暴力”手段,它比树形数据结构好写,也便于调试

分块的基本思想是通过适当的划分,预处理适当大小的信息并保存下来,用空间换取时间,使得修改以及查询的时间复杂度都能被接受,达到“时空平衡”。

预处理

首先定义块操作的基本要素,便于后续维护和处理:

  • 块的大小:用block表示。
  • 块的数量:用t表示。
  • 块的左右边界:用数组st[],ed[]表示,st[i], ed[i]就是第i块的起点与终点。
    • st[i]=(i-1)*block+1 ed[i]=i∗ block
  • 每个元素所属的块:id[i]表示第i个元素属于哪个块。

区间修改 & 区间查询

分块九题

本文中的所有~x表示分块入门x
具体题目请在OJ上搜索

~1

本题包含分块入门1和4

Prob

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间(单点)加法,区间求和。

分块图示

我们先把它每 \(s\) 个元素分成一块,便可以得到:

[a1 a2 a3...as][as+1...a2s][....]....[as*s-s+1 ... an]
      b1            b2        b3  ...      bn

注意到最后一个可能是不完整的(\(n \% s\)可能不是0)

Think

[Q]怎么实现Query&Update[1]

我们先来实现 query(l,r) 函数

  • 对于完整的块,直接回答 \(b_i\) 即可
  • 对于不完整的块,暴力即可

接下来是 update(l,r,x)

  • 对于完整的块,修改 \(b_i\) 即可
  • 对于不完整的块,暴力即可

具体代码:

void Update(int l, int r, int x)
{
	int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
	if (Lk == Rk) 
	{
		for (int i = l; i <= r; i++)
			A[i] += x, Sum[Lk] += x; // 算自己的贡献&对整个块的贡献
		return;
	}
	for (int i = l; id[i] == Lk; i++) A[i] += x, Sum[Lk] += x;
	for (int i = Lk + 1; i <= Rk - 1; i++) LzTag[i] += x, Sum[i] += x * len;
	for (int i = r; id[i] == Rk; i--) A[i] += x, Sum[Rk] += x;
}

int Query(int l, int r, int mod)
{
	int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
	int Ans = 0;
	if (Lk == Rk)
	{
		for (int i = l; i <= r; i++)
			Ans = (Ans + A[i] + LzTag[Lk]) % mod;
		return Ans;
	}
	for (int i = l; id[i] == Lk; i++) Ans = (Ans + A[i] + LzTag[Lk]) % mod;
	for (int i = Lk + 1; i <= Rk - 1; i++) Ans = (Ans + Sum[i]) % mod;
	for (int i = r; id[i] == Rk; i--) Ans = (Ans + A[i] + LzTag[Rk]) % mod;
	return Ans;
}
[Q]怎么求S使得时间复杂度最低?

那么s怎么求呢?

\[S+\frac{N}{S} \ge 2\sqrt{S\cdot\frac{N}{S}} = 2\sqrt{N} \]

所以\(S=\sqrt{N}\)

[Q]如果不是 \(\sqrt{n}\)会怎么样

如果块数过少,那么就会出现非常多的非完整块

如果块数过多,那么在就会有非常多的整块,从而没有优化的作用了

复杂度分析

所以一次操作是\(O(\sqrt{N})\)\(\Omega(1)\)

(最好 \(O(1)\) 均摊 \(O(\sqrt{n})\) )

Code

#define int long long
static constexpr int mod = 1e9 + 7;
const int maxNOfArray = 5e4 + 10;
#define r(n) n[maxNOfArray]

int r(id), r(A), r(LzTag), r(Sum), len;
void Update(int l, int r, int x)
{
	int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
	if (Lk == Rk) 
	{
		for (int i = l; i <= r; i++)
			A[i] += x, Sum[Lk] += x; // 算自己的贡献&对整个块的贡献
		return;
	}
	for (int i = l; id[i] == Lk; i++) A[i] += x, Sum[Lk] += x;
	for (int i = Lk + 1; i <= Rk - 1; i++) LzTag[i] += x, Sum[i] += x * len;
	for (int i = r; id[i] == Rk; i--) A[i] += x, Sum[Rk] += x;
}

int Query(int l, int r, int mod)
{
	int Lk = id[l], Rk = id[r]; // Lk: L块; Rk: R块
	int Ans = 0;
	if (Lk == Rk)
	{
		for (int i = l; i <= r; i++)
			Ans = (Ans + A[i] + LzTag[Lk]) % mod;
		return Ans;
	}
	for (int i = l; id[i] == Lk; i++) Ans = (Ans + A[i] + LzTag[Lk]) % mod;
	for (int i = Lk + 1; i <= Rk - 1; i++) Ans = (Ans + Sum[i]) % mod;
	for (int i = r; id[i] == Rk; i--) Ans = (Ans + A[i] + LzTag[Rk]) % mod;
	return Ans;
}

当然也可以用前缀和

注意到本题为1和4的结合

~2

Prob

给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值的元素个数。

让我们开始深度睡眠思考

以下的所有查询与修改都指的是区间查询和区间修改

Think

查询
[Q]需要思考哪些问题
  • 如何处理角块呢?
  • 整块如何处理?
  • 需要预处理哪些信息?
[A]-----[A]

我们根据之前的经验,可以大致的想到:

  • 不完整的块暴力处理
  • 完整的块二分即可

注意到有二分,二分的前提是Sorted

所以我们需要在修改时Sort

修改

区间加法

[Q]需要思考哪些问题

还是这三问

  • 如何处理角块呢?
  • 整块如何处理?
  • 需要预处理哪些信息?
[A]-----[A]

对于完整的块还是直接标记即可

而对于不完整的块,修改后区间会乱序

因此需要排序即可。

Code

struct fk
{
    int n, sz;
    vector<long long> a, tag;
    vector<int> pos, L, R;
    vector<long long> v[505];

    fk(int _n) : n(_n), sz(sqrt(_n)), a(_n + 1), tag(_n / sz + 2), pos(_n + 1), L(_n / sz + 2), R(_n / sz + 2)
    {
        for (int i = 1; i <= n; i++)
        {
            pos[i] = (i - 1) / sz + 1;
            if (!L[pos[i]]) L[pos[i]] = i;
            R[pos[i]] = i;
        }
    }

    void reset(int p) 
    {
        v[p].clear();
        for (int i = L[p]; i <= R[p]; i++) v[p].push_back(a[i]);
        sort(v[p].begin(), v[p].end());
    }

    void update(int l, int r, int v_val)
    {
        int p = pos[l], q = pos[r];
        if (p == q) { 
            for (int i = l; i <= r; i++) a[i] += v_val; 
            reset(p); 
            return; 
        }
        for (int i = l; i <= R[p]; i++) a[i] += v_val; reset(p);
        for (int i = L[q]; i <= r; i++) a[i] += v_val; reset(q);
        for (int i = p + 1; i <= q - 1; i++) tag[i] += v_val;
    }

    long long query(int l, int r, long long c)
    {
        int p = pos[l], q = pos[r];
        long long ans = 0;
        if (p == q) { 
            for (int i = l; i <= r; i++) if (a[i] + tag[p] < c) ans++; 
            return ans; 
        }
        for (int i = l; i <= R[p]; i++) if (a[i] + tag[p] < c) ans++;
        for (int i = L[q]; i <= r; i++) if (a[i] + tag[q] < c) ans++;
        for (int i = p + 1; i <= q - 1; i++)
            ans += lower_bound(v[i].begin(), v[i].end(), c - tag[i]) - v[i].begin();
        return ans;
    }
};

复杂度分析

排序 \(O(n\log n)\)

查询在\(\sqrt{n}\)个块中二分,加上暴力\(2\sqrt{n}\) 这些一共\(O(n\sqrt{n}\log\sqrt{n})\)

一共的话:\(O(n\log n + n\sqrt{n}\log\sqrt{n})\)

~3

Prob

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

Think

发现这道题和上一道很像,所以略讲

由于是要找的是 x 的前驱,因此暴力找左右不完整块的比x 小最大值,

再二分找整块比 x 小的最大值,取其中最大即可

时间复杂度的话和上题一样。

~4

Prob

给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。

Think

这道题如果只有区间乘法,那么和第一题没有区别

[Q]但是如何处理两种标记呢?

初步思考:维护两种标记

那么如何修改呢?

先乘后加,先加后乘?

[A]-----[A]

这里先处理乘法,再处理加法。

即两种标记具有优先级关系乘法标记的优先级高于加法。

[Q]如何具体实现?

若当前的一个块乘以\(m1\)后加上\(a1\),这时进行一个乘\(m2\)的操作,则原来的标记变成\(m1*m2\)\(a1*m2\)

若当前的一个块乘以\(m1\)后加上\(a1\),这时进行一个加\(a2\)的操作,则原来的标记变成\(m1\)\(a1+a2\)

[Q]如果不这么做会怎么样?

不同计算顺序,结果不一样。

所以不行。

~5

Prob

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

Think

由于涉及开方,整块无法开方,需要对每一个元素进行操作后,才能知道开方后的总和。

[Q]难以维护,如何解决?

我们手搓几组数据,就会发现:

1e9也只需要5次开方就会变成1,7次变成0!!!

思路

所以我们可以模拟思路:

如果涉及了一些完整的块,这些块经过几次操作以后就会都变成0 / 1

于是我们采取一种分块优化的暴力做法:

只要每个整块暴力开方后,开数组记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1的块即可

Code

const int MAXN = 50005;
ll a[MAXN];
ll sum[300];
ll max_val[300];
int L[300], R[300], pos[MAXN];
int n, Ksize, Kcount;

void build(int b)
{
	sum[b] = 0;
	max_val[b] = 0;
	for (int i = L[b]; i <= R[b]; ++i)
	{
		sum[b] += a[i];
		max_val[b] = max(max_val[b], a[i]);
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	
	Ksize = sqrt(n);
	Kcount = (n + Ksize - 1) / Ksize;
	
	for (int b = 1; b <= Kcount; b++)
	{
		L[b] = (b - 1) * Ksize + 1;
		R[b] = min(b * Ksize, n);
		for (int i = L[b]; i <= R[b]; i++)
		{
			pos[i] = b;
		}
		build(b);
	}
	
	for (int i = 0; i < n; i++)
	{
		int opt, l, r;
		ll c;
		cin >> opt >> l >> r >> c;
		
		int bl = pos[l];
		int br = pos[r];
		
		if (opt == 0)
		{
			if (bl == br)
			{
				for (int j = l; j <= r; j++)
					a[j] = floor(sqrt(a[j]));
				build(bl);
			}
			else
			{
				for (int j = l; j <= R[bl]; j++)
					a[j] = floor(sqrt(a[j]));
				build(bl);
				
				for (int b = bl + 1; b < br; b++)
				{
					if (max_val[b] > 1)
					{
						for (int j = L[b]; j <= R[b]; j++)
							a[j] = floor(sqrt(a[j]));
						build(b);
					}
				}
				
				for (int j = L[br]; j <= r; j++)
					a[j] = floor(sqrt(a[j]));
				build(br);
			}
		}
		else
		{
			ll ans = 0;
			if (bl == br)
				for (int j = l; j <= r; j++)
					ans += a[j];
			else
			{
				for (int j = l; j <= R[bl]; j++)
					ans += a[j];
				for (int b = bl + 1; b < br; b++)
					ans += sum[b];
				for (int j = L[br]; j <= r; j++)
					ans += a[j];
			}
			cout << ans << "\n";
		}
	}
	
	return 0;
}

~6

Prob

给出一个长为 n的数列,以及 n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。

Think

我们发现,根本无法维护!!

[Q]如何维护?

刚开始是有不同的权值,但随着询问次数增多,可能就几种权值区间了

我们借鉴一下~5的思路:维护每一个块是否已被完全覆盖

对于同权值的一个块能在 \(O(1)\) 即可统计答案,若不是只有一种权值,那么就暴力统计答案,然后修改标记,而对于不完整块,同样使用暴力维护

[Q]这么一来,岂不是最坏 \(O(n)\)?

No,假设初始序列都是同一个值,那么查询是 \(O(n)\),如果此时进行一个区间操作,其最多破坏首尾两个块的标记,因此只能使后面的询问至多多两个块的暴力时间, 所以均摊后每次操作的复杂度仍为\(O(\sqrt{n})\)

简单来说,要想让一个操作花费 \(O(n)\) 的时间,就要先花费 \(\sqrt{n}\)个操作修改。

~7

Prob

给出一个长为 n的数列,以及 n个操作,操作涉及单点插入,单点询问,数据随机生成。

观察到数据随机生成

所以可以直接暴力即可

Ex

如果数据不是随机的

我们只需要重新分块即可

或者是说用Pb-dsRope

~8

Prob

给出一个长为 n的数列,以及 n个操作,操作涉及询问区间的最小众数。

众数不满足可加性,所以本题分块最合适

Think

  • 如何处理角块呢?
  • 整块如何处理?
  • 需要预处理哪些信息?

众数可能是 完整的所有块的众数,和不完整块中出现的数

预处理出\(f[i][j]\)表示第 i 块到第 j 块的众数,开个桶从遍历一遍

那么只要能快速得出一个数在某个区间内出现次数即可,每次只要比较至多\(2\sqrt{n} + 1\) 个元素的出现次数,这题就解决了

由于没有修改,只要离散化以后,给每个数 x 开个vector,按顺序存下x 出现的位置,每次询问x 时把区间的左右端点放进对应 vector 二分一下即可。

我们来用均值不等式算一下,就可以得到大小大概是

\[\sqrt{n\dfrac{n}{\log n}} \]

所以我们就解决了问题!

Ex

如何把复杂度下降呢?

用前缀和

\(cnt[i][j]\)表示:从1到第 i 块结束, 数字 j 出现的次数

对于每个候选值,用前缀和快速计算它在中间整块的出现次数,加上两个角块的出现次数


大致总结

分块比线段树更强!

牛逼普拉斯

__EOF_


  1. 具体看这里 ↩︎

posted @ 2026-04-14 17:34  Axor  阅读(55)  评论(0)    收藏  举报