分块
思想
分块作为一种在(oi)中十分(暴)妙(力)的算法和思想,常常被使用于各种数据结构和暴力算法中,所以它又被叫做优雅的暴力.世界上最好的算法就是暴力,因为它能处理最多的问题,但是更好的算法就是分块.分块主要基于分治,便是把各种数据分别装到不同的块中,然后在处理的时候分类讨论该怎么处理的算法.
用处
总的来说这是所有算法中为数不多的 (sqrt n)级别的算法,因此相比于(log_n)级别的算法来说虽说是有点慢,但是比起暴力的话,还是快了不少的,且它处理的问题肯定也是要比(log_n)级别的算法要多。并且跟线段树比较,线段树要满足的条件可以信息合并,而分块却并不需要,因此分块也是用处很大,在(oi)比赛中常常可以用来写正解或骗上不少的分.
实现
我们一般都会用分块跟线段树一样来处理区间问题,分块,分块,第一步首先就要就先要分块,而每块应该分几个元素,也是我们都要注意的,经过前人千万次的尝试,发现(sqrt n)往往是最快的(在莫队算法中往往是(n^frac{2} {3})),,总的来说分块就是把询问和修改的复杂度均分。而如果你拿不准的话,也可以枚举试一试,最后取得一个比较好的答案。
我们设(K)为每块要分的元素,这样也可以知道一共有(K)或(K+1)块。我们用(Belong_i)来表示(i)所述的块的编号当然我们也要和线段树一样运用到标记这个操作,这样就可以实现基本操作了。
比如说我们就可以拿线段树一来练一波分块。因为分块的时间复杂度是(nsqrt n)的,而线段树的时间复杂度是(nlogn)
的,两者差距不大因此就用分块实现线段树的题。
数组变量及其含义
在讲如何实现分块的操作时,需要先深刻理解每个数组的含义,不然在后文的代码中可能就会被混淆。
int block, num, l[], r[], belong[],lazy[];//分别表示总共每一块的大小,总共有几块,每一块的左断点, 每一块的右端点,每一个元素所属的块,以及懒标记
我们可以用一个例题来介绍分块的简单操作。
(Example)
[教主的魔法] (http://www.luogu.org/problemnew/show/P2801)
分块题要先初始化
inline void init()
{
block = sqrt(n);
num = n / block;//初始化
if (n % block)//如果还有剩余的元素,就多加一块
num++;
for (int i = 1; i <=n; i++)
belong[i] = (i - 1) / block + 1, d[i] = a[i]; //初始化每个元素所属的块
for (int i = 1; i <= num; i++) l[i] = (i - 1) * block + 1, r[i] = i*block;//初始化每一块的左右断点
r[num] = n;//不要忘了最后的右端点
for(int i = 1; i <= num; i++)
sort(d + l[i], d + r[i] + 1);//维护每一块的有序性 且左闭右开
}
再讲这个题的操作,主要是区间修改和查询。
- 区间修改
区间修改是跟线段树有几点相似的地方,主要是要分类讨论,首先我们用(left,right)表示要修改的区间,那么可以分两种情况,:
- (left)和(right)在一个块中,那么我们就可以直接把left和right之间所有的值跑一边就可以了,因为在一个块中所以时间复杂度不会超过(sqrt n),可以接受
if(belong[left] == belong[right])
{
int now = belong[left];
for(int i = left; i <= right; i++)//将所要修改的区间修改
a[i] += add;
for(int i = l[now]; i <= r[now]; i++)//把要修改的区间所在的块里的所有元素更新
d[i] = a[i];
sort(d + l[now], d + r[now] + 1);//加完之后还是要维护每一块的有序性,来方便后面的二分查询操作
}
- (left)和(right)不在一个块中,那就要分开操作,将每一块都要向在一个块中的操作,然后就可以
(huge{ctrl+c},)(huge{ctrl + v}) 了。
//维护左半块
for (int i = left; i <= r[belong[left]]; i++)//将要修改的区间的左端点与其所在的块的右端点之间的所有元素修改(可能会有点绕)
a[i] += add;
for (int i = l[belong[left]]; i <= r[belong[left]]; i++)//把左端点所在块里的所有元素更新
d[i] = a[i];
sort(d + l[belong[left]], d + r[belong[left]] + 1);//加完之后还是要维护每一块的有序性,来方便后面的二分查询操作
//维护右半块 (与维护左半块同理)
for (int i = l[belong[right]]; i <= right; i++)
a[i] += add;
for (int i = l[belong[right]]; i <= r[belong[right]]; i++)
d[i] = a[i];
sort(d + l[belong[right]],d + r[belong[right]] + 1);
for (int i = belong[left] + 1; i <= belong[right] - 1; i++)
lazy[i] += add;
- 区间查询
区间查询其实套路和区间修改差不多,也是要分类讨论。
- (left)和(right)如果在一个块中的话,也是枚举一遍,时间复杂度不会超过(sqrt n)
if (belong[left] == belong[right])
{
for (int i = left; i <= right; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
printf("%d
", ans);
}
- 而当(left)和(right)不在一块时,就可以分成三块(left)和(right)中间完整的块,以及两边不完整的块。
for (int i = left; i <= r[belong[left]]; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
for (int i = l[belong[right]]; i <= right; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
for (int i = belong[left] + 1; i < belong[right]; i++)//每一块都二分一遍
{
int ll = l[i], rr = r[i], result = 0, mid;
while (ll <= rr)//二分,之前的排序就是为了此操作的有序性
{
mid = (ll + rr) >> 1;
if(d[mid] + lazy[i] >= maxn)
rr = mid - 1, result = r[i] - mid + 1;
else
ll = mid + 1;
}
ans += result;
}
printf("%d
", ans);
代码
#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
#define M 1000100
using namespace std;
int n, m;
int a[M], num, block, belong[M], l[M], r[M], lazy[M], d[M];
inline void init()
{
block = sqrt(n);
num = n / block;//初始化
if (n % block)//如果还有剩余的元素,就多加一块
num++;
for (int i = 1; i <=n; i++)
belong[i] = (i - 1) / block + 1, d[i] = a[i]; //初始化每个元素所属的块
for (int i = 1; i <= num; i++) l[i] = (i - 1) * block + 1, r[i] = i*block;//初始化每一块的左右断点
r[num] = n;//不要忘了最后的右端点
for(int i = 1; i <= num; i++)
sort(d + l[i], d + r[i] + 1);//维护每一块的有序性,方便后面的二分查询操作
}
inline void update(int left, int right, int add)
{
if(belong[left] == belong[right])
{
int now = belong[left];
for(int i = left; i <= right; i++)//将所要修改的区间修改
a[i] += add;
for(int i = l[now]; i <= r[now]; i++)//把要修改的区间所在的块里的所有元素更新
d[i] = a[i];
sort(d + l[now], d + r[now] + 1);//加完之后还是要维护每一块的有序性,且左闭右开, 来方便后面的二分查询操作
}
else
{//维护左半块
for (int i = left; i <= r[belong[left]]; i++)//将要修改的区间的左端点与其所在的块的右端点之间的所有元素修改(可能会有点绕)
a[i] += add;
for (int i = l[belong[left]]; i <= r[belong[left]]; i++)//把左端点所在块里的所有元素更新
d[i] = a[i];
sort(d + l[belong[left]], d + r[belong[left]] + 1);//加完之后还是要维护每一块的有序性,来方便后面的二分查询操作
//维护右半块 (与维护左半块同理)
for (int i = l[belong[right]]; i <= right; i++)
a[i] += add;
for (int i = l[belong[right]]; i <= r[belong[right]]; i++)
d[i] = a[i];
sort(d + l[belong[right]],d + r[belong[right]] + 1);
for (int i = belong[left] + 1; i <= belong[right] - 1; i++)
lazy[i] += add;
}
}
int query(int left, int right, int maxn)//查询left到right中>=maxn的元素的个数
{
int ans = 0;
if (belong[left] == belong[right])
{
for (int i = left; i <= right; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
printf("%d
", ans);
}
else
{
for (int i = left; i <= r[belong[left]]; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
for (int i = l[belong[right]]; i <= right; i++)
if (a[i] + lazy[belong[i]] >= maxn)
ans++;
for (int i = belong[left] + 1; i < belong[right]; i++)
{
int ll = l[i], rr = r[i], result = 0, mid;
while (ll <= rr)//二分,之前的排序就是为了此操作的有序性
{
mid = (ll + rr) >> 1;
if(d[mid] + lazy[i] >= maxn)
rr = mid - 1, result = r[i] - mid + 1;
else
ll = mid + 1;
}
ans += result;
}
printf("%d
", ans);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
init();
for (int i = 1; i <= m; i++)
{
char c;
int x, y, k;
cin >> c;
scanf("%d%d%d", &x, &y, &k);
if(c == 'A')
query(x, y, k);
else
update(x, y, k);
}
return 0;
}