分块学习笔记

定义

分块是一种将一些东西分成若干块的一种思想,有分块,数论分块(可能不太算),树分块等等。而分块的这种思想可以优化时间复杂度,一般情况下为 \(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  阅读(25)  评论(0编辑  收藏  举报
  1. 1 イエスタデイ(翻自 Official髭男dism) 茶泡饭,春茶,kobasolo
  2. 2 世间美好与你环环相扣 柏松
  3. 3 True love tired
  4. 4 一笑江湖 (DJ弹鼓版) 闻人听書_
  5. 5 最好的安排 曲婉婷
  6. 6 星星在唱歌 司南
  7. 7 山川 李荣浩
  8. 8 On My Way Alan Walker
  9. 9 百战成诗 王者荣耀·100英雄官方群像献礼歌
  10. 10 雪 Distance Capper / 罗言
  11. 11 Edamame bbno$ / Rich Brian
  12. 12 半生雪 七叔-叶泽浩
  13. 13 Catch My Breath Kelly Clarkson
  14. 14 Love Is Gone SLANDER / Dylan Matthew
  15. 15 Endless Summer Alan Walker / Zak Abel
  16. 16 悬溺 葛东琪
  17. 17 风吹丹顶鹤 葛东琪
  18. 18 Normal No More TYSM
  19. 19 哪里都是你 队长
  20. 20 Stronger Kelly Clarkson
  21. 21 廖俊涛
  22. 22 消愁 毛不易
  23. 23 The Runner Yubik
  24. 24 踏山河 七叔-叶泽浩
  25. 25 Waiting For Love Avicii
  26. 26 在你的身边 盛哲
  27. 27 Dream It Possible Delacey
  28. 28 凄美地 郭顶
  29. 29 满天星辰不及你 ycc
  30. 30 侧脸 于果
  31. 31 阿拉斯加海湾 蓝心羽
  32. 32 虞兮叹 闻人听書_
  33. 33 离别开出花 就是南方凯
  34. 34 盗墓笔记·十年人间 李常超 (Lao乾妈)
イエスタデイ(翻自 Official髭男dism) - 茶泡饭,春茶,kobasolo
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.