分块算法
所谓分块算法,就是将一个完整的区间,分成几块不同的区间,然后对这些区间进行处理。
树状数组以及线段树往往用于处理区间相关问题,树状数组是基于二进制与倍增的思想进行的,线段树则是基于分治的思想,然而并不是所有的区间问题都可以使用线段树或者树状数组解决,只有当区间满足区间可加减性时才能使用。
分块算法的思想在于适当的划分,预处理部分信息,它更接近于一种朴素的算法,效率往往低于线段树以及树状数组。
分块的思路如下:将一个长度为 n n n的序列分为 T T T块,每一块的长度为 n / T n / T n/T 。
对于每一个 T T T,我们都称为一个整块。
![](https://i-blog.csdnimg.cn/blog_migrate/ce99eb8c88ad7d2a3c3f8096aaec8348.png)
假定要处理的区间为 [ l , r ] [l, r] [l,r]。存在以下几种情况
1、 [ l , r ] [l, r] [l,r]在某个整块内。
![](https://i-blog.csdnimg.cn/blog_migrate/bbca49941bf73baaeb5125329be326c6.png)
一般对于不足整块的区域可以朴素地进行处理,即循环[l,r]的范围进行处理。
2、 [ l , r ] [l, r] [l,r]范围超过一个整块。
![](https://i-blog.csdnimg.cn/blog_migrate/f92b4e0f66a66042d33821879174bdee.png)
假定 l l l处于 p p p段中, r r r处于 q q q段中,那么整个区间 [ l , r ] [l,r] [l,r],可以被划分为三个段处理:
-
l l l到 p p p段的右边界
-
p + 1 p+1 p+1段到 q − 1 q-1 q−1段
-
q q q段的左边界到 r r r
同理,对于黄色区域的段,我们可以朴素地处理,而对于绿色的区域则是整段进行维护(可以增加标记等,类似于lazy tag)。
思想
分块算法的思想在于:提前预处理好整块的信息,对于整块的进行标记维护,记录区间的信息,对于不足整块的局部信息则是进行朴素的更新。
来看一道例题:
例题
开关 https://www.luogu.com.cn/problem/P3870
题目描述
现有 n 盏灯排成一排,从左到右依次编号为:1,2,……,n。然后依次执行 m 项操作。
操作分为两种:
指定一个区间 [a,b],然后改变编号在这个区间内的灯的状态(把开着的灯关上,关着的灯打开);
指定一个区间 [a,b],要求你输出这个区间内有多少盏灯是打开的。
灯在初始时都是关着的。
输入格式
第一行有两个整数 n 和 m,分别表示灯的数目和操作的数目。
接下来有 m 行,每行有三个整数,依次为:c、a、b。其中 c 表示操作的种类。
当 c 的值为 0 时,表示是第一种操作。
当 c 的值为 1 时,表示是第二种操作。
a 和 b 则分别表示了操作区间的左右边界。
输出格式
每当遇到第二种操作时,输出一行,包含一个整数,表示此时在查询的区间中打开的灯的数目。
输入样例
4 5
0 1 2
0 2 4
1 2 3
0 2 4
1 1 4
输出样例
1
2
思路:此题的方法很多,在此只考虑分块的做法:1表示开灯,0表示关灯。使用异或操作模拟开关灯,对于每一整块维护一个cnt表示灯的数量,不足整块的朴素进行操作。
参考程序
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
template <class T>
inline void read (T &x) {
x = 0; int f = 1; char c = getchar ();
while (!isdigit (c)) {if (c == '-') f = -1; c = getchar ();}
while (isdigit (c)) {x = (x << 3) + (x << 1) + c - 48; c = getchar ();}
x *= f;
}
const int N = 2e5 + 15;
int n;
int a[N];
int sum[N], pos[N], l[N], r[N], tag[N];
void init () {
int t = sqrt (n);
for (int i = 1; i <= t; i ++) {
l[i] = (i - 1) * t + 1;
r[i] = i * t;
}
if (r[t] < n) {
t ++;
l[t] = r[t - 1] + 1;
r[t] = n;
}
for (int i = 1; i <= t; i ++) {
for (int j = l[i]; j <= r[i]; j ++) {
pos[j] = i;
// sum[i] += a[j];
}
}
}
void modify (int x, int y) {
int p = pos[x], q = pos[y];
if (p == q) {
for (int i = x; i <= y; i ++) {
a[i] ^= 1;
if (a[i]) sum[p] ++;
else sum[p] --;
}
} else {
for (int i = p + 1; i <= q - 1; i ++) tag[i] ^= 1;
for (int i = x; i <= r[p]; i ++) {
a[i] ^= 1;
if (a[i]) sum[p] ++;
else sum[p] --;
}
for (int i = l[q]; i <= y; i ++) {
a[i] ^= 1;
if (a[i]) sum[q] ++;
else sum[q] --;
}
}
}
int query (int x, int y) {
int p = pos[x], q = pos[y], ans = 0;
if (p == q) {
for (int i = x; i <= y; i ++) ans += a[i] ^ tag[p];
} else {
for (int i = p + 1; i <= q - 1; i ++) {
if (!tag[i]) ans += sum[i];
else ans += (r[i] - l[i] + 1 - sum[i]);
}
for (int i = x; i <= r[p]; i ++) ans += a[i] ^ tag[p];
for (int i = l[q]; i <= y; i ++) ans += a[i] ^ tag[q];
}
return ans;
}
int main () {
int tt, c, x, y;
read (n), read (tt);
init ();
while (tt --) {
read (c), read (x), read (y);
if (c == 0) {
modify (x, y);
} else {
cout << query (x, y) << endl;
}
}
return 0;
}
再来看一道已经用线段树解决的简单问题
A Simple Problem with Integers (http://poj.org/problem?id=3468)
Description
You have N integers, A1, A2, … , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.
Input
The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, … , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
“C a b c” means adding c to each of Aa, Aa+1, … , Ab. -10000 ≤ c ≤ 10000.
“Q a b” means querying the sum of Aa, Aa+1, … , Ab.
Output
You need to answer all Q commands in order. One answer in a line.
Sample Input
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
Sample Output
4
55
9
15
Hint
The sums may exceed the range of 32-bit integers.
题目大意:区间加法,区间求和。直接分为T块,每块长度sqrt(n),对于不足整块的朴素的处理,涉及到整块的,进行标记(类似于线段树的lazy tag)。
参考程序:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
const int N = 1e5 + 15;
ll sum[N], add[N], a[N];
int l[N], r[N], pos[N]; // 记录每一段的左右端点,以及当前这个值在第几段中
int n;
void init () { // 预处理
int t = sqrt (n);
for (int i = 1; i <= t; i ++) {
l[i] = (i - 1) * t + 1;
r[i] = i * t;
}
if (r[t] < n) { // 最后一段不足sqrt (n)的区域
t ++;
l[t] = r[t - 1] + 1;
r[t] = n;
}
for (int i = 1; i <= t; i ++) {
for (int j = l[i]; j <= r[i]; j ++) {
sum[i] += a[j];
pos[j] = i;
}
}
}
void modify (int x, int y, ll d) {
int p = pos[x], q = pos[y];
if (p == q) { // 在同一段内
for (int i = x; i <= y; i ++)
a[i] += d;
sum[p] += d * (y - x + 1);
} else {
for (int i = p + 1; i <= q - 1; i ++) add[i] += d;
for (int i = x; i <= r[p]; i ++) a[i] += d;
sum[p] += (r[p] - x + 1) * d;
for (int i = l[q]; i <= y; i ++) a[i] += d;
sum[q] += (y - l[q] + 1) * d;
}
}
ll query (int x, int y) {
int p = pos[x], q = pos[y];
ll ans = 0;
if (p == q) {
for (int i = x; i <= y; i ++) ans += a[i];
ans += add[p] * (y - x + 1);
}else {
for (int i = p + 1; i <= q - 1; i ++) ans += sum[i] + add[i] * (r[i] - l[i] + 1);
for (int i = x; i <= r[p]; i ++) ans += a[i];
ans += add[p] * (r[p] - x + 1);
for (int i = l[q]; i <= y; i ++) ans += a[i];
ans += add[q] * (y - l[q] + 1);
}
return ans ;
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0);
int tt;
cin >> n >> tt;
for (int i = 1; i <= n; i ++) cin >> a[i];
char op;
int x, y;
ll d;
init ();
while ( tt -- ) {
cin >> op;
if (op == 'C') {
cin >> x >> y >> d;
modify (x, y, d);
} else {
cin >> x >> y;
cout << query (x, y) << endl;
}
}
return 0;
}
再来看一道较难的题目
[Violet]蒲公英 (https://www.luogu.com.cn/problem/P4168)
题目描述
在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关。
为了简化起见,我们把所有的蒲公英看成一个长度为 n 的序列 a1,a2…an,其中 ai 为一个正整数,表示第 i 棵蒲公英的种类编号。
而每次询问一个区间 [l, r],你需要回答区间里出现次数最多的是哪种蒲公英,如果有若干种蒲公英出现次数相同,则输出种类编号最小的那个。
注意,你的算法必须是在线的。
输入格式
第一行有两个整数,分别表示蒲公英的数量 n 和询问次数 m。
第二行有 n 个整数,第 i 个整数表示第 i 棵蒲公英的种类 ai 。
接下来 m 行,每行两个整数 l0, r0 ,表示一次询问。输入是加密的,解密方法如下:
令上次询问的结果为 x(如果这是第一次询问,则 x = 0),设 l=((l0+x-1) mod n) + 1,r=((r0+x-1) mod n) + 1。如果 l > r,则交换 l, r。
最终的询问区间为计算后的 [l, r]。
输出格式
对于每次询问,输出一行一个整数表示答案。
输入样例
6 3
1 2 3 2 1 2
1 5
3 6
1 5
输出样例
1
2
1
思路:题目的大意是,求区间众数。由于区间众数不满足区间可加性(两个子区间合并之后父区间,其区间众数不能由两个子区间得出),因此无法使用线段树,只好分块进行。
查询 [l, r]区间的众数时。可以分为两种情况探讨
1、[l, r]被包含在某个块中,此时朴素地查询即可
2、[l, r] 跨越不同的块。对于整块的部分,需要提前进行预处理。
由于要求区间的众数,因此我们要记录下每一个数字出现的次数cnt.
另外假定sum[i] [j] 表示从头开始到第i块中,j出现的次数,如果要求第x块到第y块中j出现的次数可以用:sum[y] [j] - sum[x - 1] [j], 类似于前缀和的思想。
m[i] [j]则表示块i到块j中最小的区间众数。
有了上述的几个数组,对于第二种情况,假定l处于x块,r处于y块,答案只可能出现在[x+1, y-1]的区间众数,或者x/y块中的某个数字,只需要把两边不足整块的区域,每一个数字都枚举一下,比较是否能成为答案即可。
注意:题目中的数字可能很大,需要离散化
参考程序:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
const int N = 1e5, M = 400;
int n;
vector <int> alls;
int sum[M][N], m[M][M]; // sum[i][j]表示从头到第i块中,j出现的次数。m[i][j] 表示第i块到第j块的区间众数
int a[N], pos[N], cnt[N]; // pos[i]表示i在第几块中,cnt[i]表示i出现的次数
int find (int x) {
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
void init () {
int t = sqrt (n);
for (int i = 1; i <= n; i ++)
pos[i] = (i - 1) / t + 1;
for (int i = 1; i <= pos[n]; i ++) { // 枚举每一块的情况
for (int j = 1; j <= alls.size (); j ++) sum[i][j] = sum[i - 1][j];
for (int j = (i - 1) * t + 1; j <= n and j <= i * t; j ++) sum[i][a[j]] ++;
}
for (int i = 1; i <= pos[n]; i ++) {
int ans = 0, res = 0; // ans用于表示区间众数、res表示该众数出现的次数
for (int j = 1 + (i - 1) * t; j <= n; j ++) {
cnt[a[j]] ++;
if (cnt[a[j]] > res || cnt[a[j]] == res and alls[a[j] - 1] < alls[ans - 1]) {
res = cnt[a[j]];
ans = a[j];
}
m[i][pos[j]] = ans; // i块到j块的区间众数
}
memset (cnt, 0, sizeof cnt);
}
}
int query (int l, int r) {
int t = sqrt (n);
int x = pos[l], y = pos[r]; // 找到l,r属于哪一块
int ans = m[x + 1][y - 1], res = max (0, sum[y - 1][ans] - sum[x][ans]); // ans表示块x+1到y-1的最小众数,res表示从x+1到y-1块中ans的出现次数
int lim = min (x * t, r), f = 0; // lim表示l点所在块的右边界
if (x != y) f = 1;
for (int i = l; i <= lim; i ++)
cnt[a[i]] = max (0, sum[y - 1][a[i]] - sum[x][a[i]]);
if (f) {
for (int i = (y - 1) * t + 1; i <= r; i ++)
cnt[a[i]] = max (0, sum[y - 1][a[i]] - sum[x][a[i]]);
}
for (int i = l; i <= lim; i ++) { // 枚举l所在的块是否有可能成为答案
cnt[a[i]] ++;
if (cnt[a[i]] > res || cnt[a[i]] == res and alls[a[i] - 1] < alls[ans - 1]) {
res = cnt[a[i]];
ans = a[i];
}
}
if (f) {
for (int i = (y - 1) * t + 1; i <= r; i ++) { // 枚举r所在的块是否有可能成为答案
cnt[a[i]] ++;
if (cnt[a[i]] > res || cnt[a[i]] == res and alls[a[i] - 1] < alls[ans - 1]) {
res = cnt[a[i]];
ans = a[i];
}
}
}
return ans;
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0);
int tt;
cin >> n >> tt;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
alls.push_back (a[i]);
}
sort (alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
for (int i = 1; i <= n; i++)
a[i] = find(a[i]);
init ();
int x = 0, l, r;
while ( tt -- ) {
cin >> l >> r;
l = (l + x - 1) % n + 1, r = (r + x - 1) % n + 1;
if (l > r) swap (l, r);
x = alls[query (l, r) - 1];
cout << x << endl;
}
return 0;
}
小结
分块的核心思想:整段维护,局部朴素。