分块学习笔记
分块
史上最优雅的暴力。
趁今天好好讲一下分块。
思想
所谓分块,就是把要维护的数据分成一个一个区间,我们称之为块。
然后可以去预处理块内维护的信息。
可以支持区间查询操作与区间修改与单点查询等操作。
相信大家都学过线段树吧。
我们不妨思考一下为什么线段树的时间复杂度非常优秀。
这是因为线段树的二分操作每次将区间分成许多个小区间。
对于区间查询时,可以将查询区间分成很多个已经处理好区间信息的小区间,然后将各个小区间的信息进行汇总,之后在查询出查询区间的答案。
分块也是这样。
只是不同的是,分块是直接在原数列上进行拆分,而线段树是在原数列基础上进行向下形成更小的区间,直到遍历的数列的每个单点,再向上处理区间信息。
基本操作及实现
那么,我们大概知道了分块的思想,现在我们来讲解如何实现。
首先,分块分块嘛,应该先将原序列分成很多个小区间。
这是,我们需要自己来定义块的长度。这很重要,因为这将直接影响到代码的时间复杂度。
之后将讲到分块的时间复杂度证明,这里先按下不表。
然后,我们可以记录每个块的左右端点,这样可以较为简单地找到这个块所维护的区间。
同时,我们要维护每个点属于哪个块,这样有利于在修改和查找时定位到要进行操作的块。
然后按照题意维护所维护的信息。
这样,我们就完成了预处理。
len = sqrt(n); // 块长
top = ceil(n * 1.0 / len); // 块的个数
for(int i = 1;i <= n;i ++)
{
cin >> a[i]; // 输入
b[i] = a[i]; // 同时维护一个原数组(因为本题有块内排序操作,所以需要维护原数组)
id[i] = (i - 1) / len + 1; // 这个点属于哪个块
}
for(int i = 1;i <= top;i ++)
{
st[i] = (i - 1) * len + 1; // 这个块的左端点
ed[i] = i * len; // 这个块的右端点
}
ed[top] = n; // 最后一个块的右端点(因为最后一个块可能长度不满块长)
for(int i = 1;i <= top;i ++) sort(a + st[i],a + ed[i] + 1); // 进行块内排序(按照题意维护所维护的信息)
然后,我们就到了重头戏:查询和修改操作。
在开始之前,让我们先理解两种概念:散块和整块。
散块,顾名思义,就是不全的块,但这里的不全不是指在原序列上的不全,而是指在查询或修改操作中,位于查询或修改区间左右端点处的没有被该区间完全包含的块。
而整块就是在查询或修改区间被完全包含的块。
然后,我们可以开始进行修改操作了。
考虑到要修改区间 \([l,r]\)。
首先考虑散块,这很简单,直接暴力修改即可。(但注意,在本题中,要对散块的原序列(排序之前的)进行修改)
在修改完后,重新维护该散块信息即可。(本题中为排序)
tips:要判断一下修改区间的左右端点是否在同一个块内,防止对一个块多次修改。
对于整块,我们可以类比线段树去维护一个 \(laz\),来记录该块修改信息。
这样,我们就完成了修改操作:
inline void update(int l,int r,int x)
{
for(int i = l;i <= min(ed[id[l]],r);i ++) b[i] += x; // 对散块的原序列(排序之前的)进行修改
for(int i = st[id[l]];i <= ed[id[l]];i ++) a[i] = b[i]; // 排序,将整个块替换后排序
sort(a + st[id[l]],a + len + ed[id[l] - 1] + 1);
if(id[l] != id[r]) // 判断一下修改区间的左右端点是否在同一个块内
{
for(int i = st[id[r]];i <= r;i ++) b[i] += x; // 对散块的原序列(排序之前的)进行修改
for(int i = st[id[r]];i <= ed[id[r]];i ++) a[i] = b[i];// 同理,排序,将整个块替换后排序
sort(a + st[id[r]],a + ed[id[r]] + 1);
}
if(id[l]!=id[r]) for(int i = id[l] + 1;i <= id[r] - 1;i ++) laz[i] += x; // 对整块进行标记
}
然后我们考虑查询操作:
类比线段树,我们同样可以把要查询的区间分成一些维护好区间信息的小区间挨个查询,之后在汇总即可。
对于散块,直接暴力遍历查询。
对于整块,可以在排好序的块内二分,来找到第一个小于等于查询值的块内排名,之后再汇总即可。
直接上代码:
inline int Shiroko(int id,int val)//比 val 小 或 相等 的数量 (块内二分)
{
int l = st[id];
int r = ed[id];
if(a[l] + laz[id] > val) return 0; // 判断合不合法
if(a[r] + laz[id] <= val) return ed[id] - l + 1; // 判断合不合法
int mid;
while(l < r)
{
mid = (l + r) / 2 + 1;
if(a[mid] + laz[id] <= val) l = mid;
else r = mid - 1;
}
if(a[l] + laz[id] <= val) return l - ed[id - 1]; // 返回块内排名
return 0;
}
inline int query(int l,int r,int val)//比 val 小 或 相等 的数量 (进行查询)
{
int res = 0;
for(int i = l;i <= min(ed[id[l]],r);i ++)
{
res += (b[i] + laz[id[l]] <= val) ? 1 : 0; // 散块暴力查询
}
if(id[l] != id[r])
{
for(int i = st[id[r]];i <= r;i ++) res += (b[i] + laz[id[r]] <= val); // 散块暴力查询
}
for(int i = id[l] + 1;i <= id[r] - 1;i ++) res += Shiroko(i,val); // 整块二分查询
return res;
}
这样,我们就学完了分块的基本操作,给自己鼓个掌吧。
接下来,我们开始惊简心单动刺魄激的复杂度证明
我们设块长为 \(len\) ,块的个数为 \(top\)。
-
对于每个修改操作,最多修改 \(top\) 个整块,修改每个散块最多需要修改 \(len\) 个元素。
-
对于每个查询操作,最多查询 \(top\) 个整块,查询每个散块最多需要查询 \(len\) 个元素。
所以显然可得,分块的时间复杂度是与 \(top\) 与 \(len\) 有关的。
显然 \(top = \frac n{len}\)。
那么分块的时间复杂度为 \(O(q(len + top))\),也就是 \(O(q(len + \frac n{len}))\) ,我们可以通过基本不等式进行化简。
来自同机房知名同学的鲜花:证的好麻烦的说,一般而言令两项相等就可以调出一个最优的复杂度,多项就考虑省略较小项,不能省略就两两相等或者强行让同级(数据范围差不多的)换成同一个未知量,再不行就让三项尽可能相近,硬算罢。
- 基本不等式:$a + b \ge 2 \sqrt{ab} $,当且仅当 \(x = y\) 时取等。
也就是让 \(len = \frac n{len}\) ,即 \(len = \sqrt{n}\)。
将其代入原式可得最终时间复杂度: \(O(n\sqrt{n})\)。
这也是最优的时间复杂度。
tips:有些分块修改和查询操作也许不是 \(O(len + top)\),那么实际时间复杂度不是 \(O(n\sqrt{n})\),但也可以通过基本不等式求最优时间复杂度,并算出最优块长。
Q:为什么明明线段树 \(O(n \log{n})\) 的时间复杂度更优,我们还要写分块呢?
A:虽然分块的时间复杂度不优,但是常数极小,一般比线段树跑的更快,有时甚至比树状数组跑的都快。
另外,分块更灵活,可以维护更加复杂的信息,比如说本题,如果用线段树做非常困难或是根本不可做。
线段树只能在有外力帮助下完成这一操作,即线段树套平衡树。
下面放几道分块例题,如果做完分块的这几道入门题的话,可以试着做做。
P5356 [Ynoi Easy Round 2017] 由乃打扑克
U597856 茁壮地长大吧! [蔚蓝档案] (宣传一下团队里的一道题,可以用分块写,感觉是道分块好题)
\(\text {talk is cheap,show me your code.}\)
#include<bits/stdc++.h>
#define int long long
#define Blue_Archive return 0
#define Girls_Band_Cry return
#define My_Go break
#define op DZC
#define O721 Ayachi Nene
using namespace std;
const int N = 1e6 + 9;
const int INF = 1e18;
int n;
int len;//块长
int top;
int anss;
int minn;
int maxx;
int a[N];
int b[N];
int id[N];//编号
int st[N];//左编号
int ed[N];//右端点
int add[N];
inline void init()
{
for(int i = 1;i <= top;i ++)
{
st[i] = (i - 1) * len + 1;
ed[i] = i * len;
}
ed[top] = n;
for(int i = 1;i <= top;i ++) sort(a + st[i],a + ed[i] + 1);
}
inline void update(int l,int r,int x)
{
for(int i = l;i <= min(ed[id[l]],r);i ++) b[i] += x;
for(int i = st[id[l]];i <= ed[id[l]];i ++) a[i] = b[i];
sort(a + st[id[l]],a + len + ed[id[l] - 1] + 1);
if(id[l] != id[r])
{
for(int i = st[id[r]];i <= r;i ++) b[i] += x;
for(int i = st[id[r]];i <= ed[id[r]];i ++) a[i] = b[i];
sort(a + st[id[r]],a + ed[id[r]] + 1);
}
if(id[l]!=id[r])for(int i = id[l] + 1;i <= id[r] - 1;i ++) add[i] += x;
}
inline int Shiroko(int id,int val)//比 val 小 或 相等 的数量
{
int l = st[id];
int r = ed[id];
if(a[l] + add[id] > val) return 0;
if(a[r] + add[id] <= val) return ed[id] - l + 1;
int mid;
while(l < r)
{
mid = (l + r) / 2 + 1;
if(a[mid] + add[id] <= val) l = mid;
else r = mid - 1;
}
if(a[l] + add[id] <= val) return l - ed[id - 1];
return 0;
}
inline int query(int l,int r,int val)//比 val 小 或 相等 的数量
{
int res = 0;
for(int i = l;i <= min(ed[id[l]],r);i ++)
{
res += (b[i] + add[id[l]] <= val) ? 1 : 0;
}
if(id[l] != id[r])
{
for(int i = st[id[r]];i <= r;i ++) res += (b[i] + add[id[r]] <= val);
}
for(int i = id[l] + 1;i <= id[r] - 1;i ++) res += Shiroko(i,val);
return res;
}
signed main()
{
// freopen("data.in","r",stdin);freopen("data.out","w",stdout);
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
minn = INF;
cin >> n;
len = sqrt(n);
top = ceil(n * 1.0 / len);
for(int i = 1;i <= n;i ++)
{
cin >> a[i];
b[i] = a[i];
id[i] = (i - 1) / len + 1;
}
init();
for(int i = 1,op,l,r,k;i <= n;i ++)
{
cin >> op >> l >> r >> k;
if(op == 1)//query
{
k = k * k;
cout << query(l,r,k) << '\n';
}
if(op == 0)//add
{
update(l,r,k);
}
}
Blue_Archive;
}

浙公网安备 33010602011771号