分块学习笔记

定义

分块是一种将一些东西分成若干块的一种思想,有分块,数论分块(可能不太算),树分块等等。而分块的这种思想可以优化时间复杂度,一般情况下为 \(O( \sqrt n)\),具体取决于块长的大小。

分块

对数组是分块当中最简单一种。首先我们设块长为 \(s\),那么数组一共就被分成了\(n/s\) 块。而我们需要进行的操作一般就分为两种——区间修改和区间查询。

首先,对于修改操作,我们假设需要修改的区间为 \(l\)\(r\)。我们可以将操作分为三个部分。

  • \(l\)\(l\) 所在块的结尾。我们可以直接暴力地去修改这个区间的每一个数,因为这一段区间的长度最长为 \(s\),修改的时间复杂度为 \(O(s)\)

  • \(l\) 所在块的后一块到 \(r\) 所在块的前一块。对于每一块,我们都可以直接修改一整个块的信息(\(p\) 数组),不需要对每一个数进行修改。而一共最多有 \(n/s\) 个块,时间复杂度 \(O(n/s)\)

  • \(r\) 所在块的起点 到 \(r\) 。同理,这一段区间的长度最长也为 \(s\),修改的时间复杂度为 \(O(s)\)

接下来是查询操作,我们还是假设需要查询的区间为 \(l\)\(r\)。我们也可以将查询操作分为三个部分。

  • \(l\)\(l\) 所在块的结尾。我们可以直接暴力地去计算这个区间的每一个数,因为这一段区间的长度最长为 \(s\),修改的时间复杂度为 \(O(s)\)

  • \(l\) 所在块的后一块到 \(r\) 所在块的前一块。我们会维护一个 \(p\) 数组来记录每一个区间内的信息(修改时也会更新 \(p\) 数组), 这样就不需要对每一个数进行计算 ,只需要把每一块的信息加起来即可 。而一共最多有 \(n/s\) 个块,时间复杂度 \(O(n/s)\)

  • \(r\) 所在块的起点 到 \(r\) 。同理,这一段区间的长度最长也为 \(s\),查询的时间复杂度为 \(O(s)\)

最后讲一下分块常用的一些写法与技巧:

分块有些时候会用到线段树的懒标记的思想,比如区间加 \(k\),可以先将 \(add_{id}\)\(k\),查询 \(k\) 的时候只需要查询 \(k-add_{id}\) 就可以了。分块一般还会用 \(sort\) 对每一块内进行排序,这样查询的时候可以在块内做二分来查找某一个位置。(比如统计 \(l\)\(r\) 中大于 \(k\) 的数的个数)所以有些时候还会带一个 \(\log\) 集别的复杂度,可能被卡常。

这样我们就在 \(O(n/s+s)\) 的时间复杂度下实现了对序列的区间修改和区间查询。这时还剩下最后一个问题,\(s\) 应该取多少才能实现最优?

根据均值不等式可知,当 \(s = n/s\) 的时候,复杂度最优,即 \(s= \sqrt n\)。时间复杂度 \(o(n\sqrt n)\)

分块可以解决的问题也很多,比如区间求和,求区间内一个数出现了多少次,莫队也是根据这个思想来实现的。个人认为这是一个很有用的算法。

但是分块也有缺点,它的时间复杂度稍微劣于线段树和树状数组。优点也很显然,很容易实现,代码比较短。

例题1:P1903

对于一个序列,维护两个操作:

  • \(a_{x}\) 改为 \(p\)

  • \(l\)\(r\) 中有多少个不同的数

这道题本来是带修莫队的板子的,但是我使用分块做的,时间复杂度 \(O(n \sqrt n \log (\sqrt n))\),而数据范围是 \(n \le 133333\)。于是我就TLE了!!!可能是被卡常了。我也懒得换写法了,于是 \(82\) 分遗憾离场。但是这道题可以当做练习分块。

具体思路挺板的...但是这道题其实有个 \(trick\)。就是我们先预处理记录 \(pre_{i}\) 表示上一个 \(a_{i}\) 出现的位置。假设查询的区间为 \(l\)\(r\),如果 \(pre_{now} < l\),那么就说明当前这个颜色在这个区间内还没有出现过,于是 \(ans++\)。修改的时候需要更新 \(now\) 位置以及前后最近的两个的和 \(now\) 相同颜色的位置的 \(pre\) 的值。然后就可以分块做了

例题2:P2801

这道题是纯分块板子,只需要维护两个操作:

  • 区间加 \(k\)

  • 求区间内大于等于 \(k\) 的数的个数

用类似线段树中的 \(lazy\) 标记的思想,维护一个 \(add\) 数组,两端的区间暴力做。中间的块直接加在 \(add\) 数组里面就行了。查询的时候用 \(lower\)_\(bound\)\(k - add\) 所在位置即可。其余的用分块随便做即可。

Code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 0x3f3f3f3f
#define ls id << 1
#define rs id << 1 | 1
#define re register
typedef pair <int,int> pii;
const int MAXN = 1e6 + 10;
int n,q,a[MAXN],len,num,lid[MAXN],rid[MAXN],b[MAXN],add[MAXN],pos[MAXN],x,y,z;
char op;
inline bool cmp(int x,int y){return x < y;}
inline void reset(int id)
{
	for(int i = lid[pos[id]];i <= rid[pos[id]];i++) b[i] = a[i];
	sort(b + lid[pos[id]],b + rid[pos[id]] + 1,cmp);
}
inline void Init()
{
	for(int i = 1;i <= num;i++)
	{
		lid[i] = (i - 1) * len + 1,rid[i] = min(len * i,n);
		for(int j = lid[i];j <= rid[i];j++) pos[j] = i,b[j] = a[j];
		sort(b + lid[i],b + rid[i] + 1,cmp);
	}
}
inline void Add(int l,int r,int k)
{
	if(pos[l] == pos[r])
	{
		for(int i = l;i <= r;i++) a[i] += k;
		reset(l);return;
	}
	for(int i = l;i <= rid[pos[l]];i++) a[i] += k; 
	for(int i = lid[pos[r]];i <= r;i++) a[i] += k;
	reset(l);reset(r);
	for(int i = pos[l] + 1;i <= pos[r] - 1;i++) add[i] += k;
}
inline int Query(int l,int r,int k)
{
	int ans = 0;
	if(pos[l] == pos[r])
	{
		for(int i = l;i <= r;i++) if(a[i] + add[pos[l]] >= k) ans++;
		return ans;
	} 
	for(int i = l;i <= rid[pos[l]];i++) if(a[i] + add[pos[i]] >= k) ans++;
	for(int i = lid[pos[r]];i <= r;i++) if(a[i] + add[pos[i]] >= k) ans++;
	for(int i = pos[l] + 1;i <= pos[r] - 1;i++)
	{
		int idx = rid[i] - (lower_bound(b + lid[i],b + rid[i] + 1,k - add[i]) - b);
		ans += idx + 1;
	}
	return ans;
}
signed main()
{
	cin >> n >> q;
	len = sqrt(1.0 * n),num = ceil(1.0 * n / len);
	for(int i = 1;i <= n;i++) cin >> a[i];
	Init();
	while(q--)
	{
		cin	>> op >> x >> y >> z;
		if(op == 'M') Add(x,y,z);
		if(op == 'A') cout << Query(x,y,z) << endl;
	}
	return 0;
} 

LOJ 分块九题做题记录

  • 数列分块入门 1

题意:区间加,单点查询。

思路:对于每个整块维护一个 \(add\) 数组记录加了多少,查询的答案就是 \(a_{i}+add_{i}\),时间复杂度 \(O(n \sqrt n)\)

  • 数列分块入门 2

题意:区间加法,询问区间内小于某个值 \(x\) 元素个数。

思路:首先还是需要维护一个 \(add\) 数组记录加了多少,然后对于每一个块都进行排序,这样求整块中小于某个值 \(x\) 元素个数就可以在排完序过后的数组里二分,二分查找第一个小于等于 \(x-add_{i}\) 的位置,然后就可以推出总个数了。最终的答案就是每个块的个数加起来。注意修改不是整段的区间过后要将这一段重新排一遍序。时间复杂度 \(O(n \sqrt n \log(\sqrt n))\)

  • 数列分块入门 3

题意:区间加法,询问区间内小于某个值 \(x\) 的前驱(比其小的最大元素)。

思路:和第二题差不多,查询的时候二分查找第一个小于等于 \(x-add_{i}\) 的位置,那么前一个位置的数就是当前块的答案。最终的答案就是所有块的答案的最大值。时间复杂度 \(O(n \sqrt n \log(\sqrt n))\)

  • 数列分块入门 4

题意:区间加法,区间求和。

思路:和第一题差不多,对于每一个块还需要多维护一个 \(sum\) 数组表示这一块中所有数的和,这样每一块的和就是 \(sum_{i}+add_{i}\times cnt\),这里的 \(cnt\) 表示当前块的数的个数。答案为每个块的和加起来,时间复杂度 \(O(n \sqrt n)\)

  • 数列分块入门 5

题意:区间开方,区间求和。

思路:首先我们知道 \(1\) 无论怎么开方最终的结果肯定还是 \(1\),所以一个数被开方的次数是很小的,准确来说是 \(O(n \log \log n)\) 级别的,所以我们可以对于每一个块,如果当前块内的所有数都为 \(1\) 的话,那么就直接跳过当前块,否则直接暴力开方,开方后再记录 \(flag_{i}\) 表示当前块的所有数是否都为 \(1\),以便下次的判断。求和方法和第四题一样,,时间复杂度 \(O(n \sqrt n\log \log n)\)

  • 数列分块入门 6

题意:单点插入,单点询问。

思路:先考虑暴力的做法,可以直接开一个 \(vector\) 来维护,众所周知 \(vector\) 自带插入和查询的功能。但是 \(vector\) 的插入操作是近似于 \(O(n)\) 的,这样肯定会 \(TLE\),所以还需要优化。利用分块的思想,我们可以维护 \(\sqrt n\)\(vector\),每次插入先找到应该在哪个 \(vector\) 中插入,然后用自带函数将 \(x\) 插入即可。查询的时候也用同样的方法找到对应的 \(vector\) 直接进行查询就可以了。注意有可能会多次插入到同一个 \(vector\) 中,这样时间复杂度就会假,于是如果当前 \(vector\) 的大小大于 \(2\sqrt n\) 了,就应该全局重新分配一下。总时间复杂度 \(O(n\sqrt n)\)

  • 数列分块入门 7

题意:区间乘法,区间加法,单点询问。

思路:相比于第四题,还需要多维护一个 \(mul_{i}\) 表示 第 \(i\) 块乘了多少。还有就是每次进行区间乘法的时候也要将 \(add_{i}\) 乘上 \(c\),因为必须满足乘法的分配率。因为支持两种运算,所以在修改非整块的时候还要将当前块的 \(add\)\(mul\) 的值 \(pushdown\) 一下。查询的时候答案为 \(a_{i}\times mul_{pos_{i}}+add_{pos{i}}\),如果是区间查询的话,那么第 \(i\) 块的答案为 \(sum_{i}\times mul_{i}+add_{i}\times cnt\)\(cnt\) 表示当前块的数的个数。时间复杂度 \(O(n\sqrt n)\)

  • 数列分块入门 8

题意:区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)

思路:记录一个 \(tag\) 数组表示第 \(i\) 块中的所有数都为 \(tag_{i}\)\(tag_{i}=0\) 的话说明块内元素不相等。询问的时候如果 \(tag_{i}=0\) 的话暴力枚举,否则直接通过 \(tag_{i}\) 的值是否为 \(x\) 来计算。修改的时候对于非整块直接暴力修改,对于整段只需要直接修改 \(tag\) 的值就可以了。当修改和查询非整块的时候还需要将当前块的 \(tag\)\(pushdown\) 一下。这样虽然每次会 \(O(\sqrt n)\) 暴力查每一块,但是查完之后就一定会被赋上 \(tag\) 值,之后就可以直接 \(O(1)\) 查询了,所以暴力查的块数是 \(O(n)\),即每次的非整块,总时间复杂度 \(O(n\sqrt n)\)

  • 数列分块入门 9

题意:询问区间的最小众数。

思路:众数不满足区间可加性,所以不能用线段树了,只能考虑分块。先将题目弱化一下:询问区间的最小众数出现次数。那就可以直接用离线莫队解决了。但是我们会发现中途不能维护区间众数是多少,所以要换方法。还是考虑用最朴素的分块,维护两个数组 \(s_{i,j}\) 表示前 \(i\) 块中 \(j\) 的出现次数和 \(p_{i,j}\) 表示第 \(i\) 块到第 \(j\) 块中的区间众数,这两个数组都可以在 \(O(n \sqrt n)\) 的时间复杂度下预处理出来。对于一个区间可以先直接得出中间所有整块的众数,再暴力枚举所有非整块,看一下其中有没有新的数出现次数比当前众数还大,如果有的话就拿新数更新答案即可。时间复杂度 \(O(n \sqrt n)\)

posted @ 2023-12-19 11:47  Creeper_l  阅读(2)  评论(0编辑  收藏  举报