君君算法课堂-数列分块


目录
  • 数列分块
    • 区间加法,单点查值
    • 区间加法,区间询问小于 的元素个数
    • 区间加法,查询区间对于 的前驱
    • 区间加法,区间求和
    • 区间乘法,区间加法,单点询问
    • 问题总结

数列分块

当处理数列问题时,有时会遇到区间修改或区间询问操作

使用暴力方法无疑会在 区间 上耗时较多

而对于和区间有关的问题,常常通过维护区间信息等来实现

分块,即将序列分成一小块一小块来进行处理维护

将每一块当作一个整体,记录维护整体的有关信息,来减小时间复杂度

在对序列进行分块时,有 序列总长度 = 每一块序列长度 * 块数

维护序列信息时,当区间长度够长时,维护信息的复杂度与块数有关

但又由于所询问的区间不能够将序列块刚好完整覆盖

则对于 残块 来说,维护信息的复杂度与每一块序列的长度有关

因此,一般我们分块时,将每块序列长度分为 \(\sqrt{n}\),块数 也在 \(\sqrt{n}\) 的级别

这样能够有效降低时间复杂度

下面通过几道例题来理解分块的操作

区间加法,单点查值

题目链接:6277. 数列分块入门 1

\(len = \sqrt{n}\) 记录每一块的序列长度,\(pos[i]\) 记录 \(a[i]\) 所属于的块的 编号

\(L[i]\) 记录 第 \(i\) 个块 的左边界下标,\(R[i]\) 记录 第 \(i\) 个块 的右边界下标(\([L_i, R_i]\)

\(tag[i]\) 记录 编号为 \(i\) 的块 所维护的区间和信息

对于区间加法(区间 \([l,r]\)\(c\))的操作,分情况讨论

若区间仅覆盖一个块,则暴力修改即可

即若 \(pos[l] = pos[r]\):暴力修改每个单点的值

否则执行以下操作:

  • 区间左端点 到 其所对应块的右端点:暴力修改
  • 区间右端点 到 其所对应块的左端点:暴力修改
  • 其它被询问区间完整覆盖的块:维护区间信息即可

对于单点询问,输出 单点的值及该点所在块维护信息 的和即可

时间复杂度:单次区间询问 \(O(\sqrt{n})\),单点查询 \(O(1)\)

#include 
#include 
#include 
using namespace std;
const int N = 1e5 + 5;
int n, len, pos[N], L[N], R[N], a[N], tag[N];
void change(int l, int r, int c) {
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) a[i] += c;
		return;
	}
	for(int i = l; i <= R[pos[l]]; i ++) a[i] += c;
	for(int i = L[pos[r]]; i <= r; i ++) a[i] += c;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) tag[i] += c;
}
int main() {
	scanf("%d", &n); len = sqrt(n);
	for(int i = 1; i <= n; i ++) cin >> a[i], pos[i] = (i-1) / len + 1;
	for(int i = 1; i <= pos[n]; i ++) L[i] = (i-1)*len + 1, R[i] = min(len * i, n);
	for(int i = 1, opt, l, r, c; i <= n; i ++) {
		scanf("%d%d%d%d", &opt, &l, &r, &c);
		if(opt == 0) change(l, r, c);
		if(opt == 1) printf("%d\n", a[r] + tag[pos[r]]);
	}
	return 0;
}

区间加法,区间询问小于 \(x\) 的元素个数

题目链接:6278. 数列分块入门 2

首先进行数列分块,考虑如何对信息进行维护

需要方便求出 某段区间 内小于 \(x\) 的元素个数

考虑如下做法:

对于原数列 \(a[n]\),建立新数组 \(b[i]\),其中在每块内 \(b[i]\) 有序

在询问时便能较为方便的求出 区间内小于 \(x\) 的元素个数

对于区间加法,同之前的方法,不过需要在维护 \(a[i]\) 的同时维护 \(b[i]\)

保证每个块内 \(b[i]\) 有序

在查询时统计每个块内的答案再求和即可

注:\(lower\_bound\) 查找 \(≥x\) 的元素中最小的一个(序列需先排好序),并返回指向该元素的迭代器

#include 
#include 
#include 
#include 
using namespace std;
const int N = 5e5 + 5;
int n, len, a[N], b[N], pos[N], L[N], R[N], tag[N];
void change(int l, int r, int c) {
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) a[i] += c;
		for(int i = L[pos[l]]; i <= R[pos[r]]; i ++) b[i] = a[i];
		sort(b + L[pos[l]], b + R[pos[l]] + 1);
		return;
	}
	for(int i = l; i <= R[pos[l]]; i ++) a[i] += c;
	for(int i = L[pos[r]]; i <= r; i ++) a[i] += c;
	for(int i = L[pos[l]]; i <= R[pos[l]]; i ++) b[i] = a[i];
	for(int i = L[pos[r]]; i <= R[pos[r]]; i ++) b[i] = a[i];
	sort(b + L[pos[l]], b + R[pos[l]] + 1);
	sort(b + L[pos[r]], b + R[pos[r]] + 1);
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) tag[i] += c;
}
int query(int l, int r, int c) {
	int res = 0;
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) {
			if(a[i] + tag[pos[i]] < c) res ++;
		}
		return res;
	}
	for(int i = l; i <= R[pos[l]]; i ++) if(a[i] + tag[pos[l]] < c) res ++;
	for(int i = L[pos[r]]; i <= r; i ++) if(a[i] + tag[pos[r]] < c) res ++;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) {
		int p = lower_bound(b + L[i], b + R[i] + 1, c - tag[i]) - b - 1;
		res += max(p - L[i] + 1, 0);
	}
	return res;
}
int main() {
	cin >> n; len = sqrt(n);
	for(int i = 1; i <= n; i ++) cin >> a[i], b[i] = a[i];
	for(int i = 1; i <= n; i ++) pos[i] = (i-1) / len + 1;
	for(int i = 1; i <= pos[n]; i ++)
		L[i] = (i-1) * len + 1, R[i] = min(i * len, n), sort(b + L[i], b + R[i] + 1);
	for(int i = 1, opt, l, r, c; i <= n; i ++) {
		cin >> opt >> l >> r >> c;
		if(opt == 0) change(l, r, c);
		if(opt == 1) cout << query(l, r, c * c) << "\n";
	}
	return 0;
}

区间加法,查询区间对于 \(x\) 的前驱

题目链接:6279. 数列分块入门 3

方法类似第二题,只不过在求解时,返回小于 \(x\) 的数的最大值即可

#include 
#include 
#include 
#include 
using namespace std;
const int N = 5e5;
int n, len, tot, a[N], b[N], pos[N], L[N], R[N], tag[N];
void change(int l, int r, int c) {
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) a[i] += c;
		for(int i = L[pos[l]]; i <= R[pos[r]]; i ++) b[i] = a[i];
		sort(b + L[pos[l]], b + R[pos[r]] + 1);
		return;
	}
	for(int i = l; i <= R[pos[l]]; i ++) a[i] += c;
	for(int i = L[pos[r]]; i <= r; i ++) a[i] += c;
	for(int i = L[pos[l]]; i <= R[pos[l]]; i ++) b[i] = a[i];
	for(int i = L[pos[r]]; i <= R[pos[r]]; i ++) b[i] = a[i];
	sort(b + L[pos[l]], b + R[pos[l]] + 1);
	sort(b + L[pos[r]], b + R[pos[r]] + 1);
	for(int i = pos[l] + 1; i <= pos[r]-1; i ++) tag[i] += c;
}
int query(int l, int r, int c) {
	int ans = -1;
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) {
			if(a[i] + tag[pos[i]] < c) 
				ans = max(ans, a[i]+tag[pos[i]]);
		}
		return ans;
	}
	for(int i = l; i <= R[pos[l]]; i ++) {
		if(a[i] + tag[pos[l]] < c)
			ans = max(ans, a[i] + tag[pos[l]]);
	}
	for(int i = L[pos[r]]; i <= r; i ++) {
		if(a[i] + tag[pos[r]] < c)
			ans = max(ans, a[i] + tag[pos[r]]);
	}
	for(int i = pos[l] + 1; i <= pos[r]-1; i ++) {
		int p = lower_bound(b + L[i], b + R[i] + 1, c - tag[i]) - b - 1;
		if(p < L[i]) continue;
		ans = max(ans, b[p] + tag[i]);
	}
	return ans;
}
int main() {
	cin >> n; len = sqrt(n);
	for(int i = 1; i <= n; i ++) cin >> a[i], pos[i] = (i-1)/len + 1, b[i] = a[i];
	for(int i = 1; (i-1) * len <= n; i ++) 
		L[i] = (i-1)*len+1, R[i] = min(n, i*len), sort(b+L[i], b+R[i]+1);
	tot = pos[n];
	for(int i = 1, opt, l, r, c; i <= n; i ++) {
		cin >> opt >> l >> r >> c;
		if(opt == 0) change(l, r, c);
		if(opt == 1) cout << query(l, r, c) << endl;
	}
	return 0;
}

区间加法,区间求和

题目链接:6280. 数列分块入门 4

\(sum[i]\) 来维护第 \(i\) 个块 的区间和,\(tag[i]\) 维护单点 \(a[x]\)(属于块 \(i\)) 未能加和的值(懒标记)

\(tag[i]\) 能够便于统计残块信息,对于整个块,通过一个标记信息来维护区间信息

这种思想在一些数据结构中很常见,能够极大提高程序运行效率

#include 
#include 
#include 
#define int long long
using namespace std;
const int N = 5e5 + 5;
int n, len, a[N], pos[N], L[N], R[N], tag[N], sum[N];
void change(int l, int r, int c) {
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) a[i] += c;
		sum[pos[l]] += (r - l + 1) * c;
		return;
	}
	for(int i = l; i<= R[pos[l]]; i ++) a[i] += c;
	sum[pos[l]] += (R[pos[l]] - l + 1) * c;
	for(int i = L[pos[r]]; i <= r; i ++) a[i] += c;
	sum[pos[r]] += (r - L[pos[r]] + 1) * c;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) tag[i] += c, sum[i] += len * c;
}
int query(int l, int r, int c) {
	int res = 0;
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) {
			(res += a[i] + tag[pos[i]]) %= c;
		}
		return res;
	}
	for(int i = l; i <= R[pos[l]]; i ++) (res += a[i] + tag[pos[l]]) %= c;
	for(int i = L[pos[r]]; i <= r; i ++) (res += a[i] + tag[pos[r]]) %= c;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) (res += sum[i]) %= c;
	return res;
}
signed main() {
	cin >> n; len = sqrt(n);
	for(int i = 1; i <= n; i ++) cin >> a[i];
	for(int i = 1; i <= n; i ++) pos[i] = (i-1) / len + 1, sum[pos[i]] += a[i];
	for(int i = 1; i <= pos[n]; i ++) 
		L[i] = (i-1) * len + 1, R[i] = min(i * len, n);
	for(int i = 1, opt, l, r, c; i <= n; i ++) {
		cin >> opt >> l >> r >> c;
		if(opt == 0) change(l, r, c);
		if(opt == 1) cout << query(l, r, c + 1) << "\n";
	}
	return 0;
}

区间乘法,区间加法,单点询问

题目链接:6283. 数列分块入门 7

对于每个块,维护一个乘法标记 \(mul[i]\) 和一个加法标记 \(add[i]\)

对于每次修改操作,将操作区间两端的标记影响进行消除

(消除标记时,先消除乘法标记,再消除加法标记)

考虑对于中间的块

进行加法操作时,之前的乘法标记不会影响到目前的操作

而进行乘法操作时,之前的加法标记却会影响到当前的操作

需要将加法标记也进行变化,来正确维护标记

#include 
#include 
#include 
using namespace std;
const int N = 1e5 + 5, mod = 1e4 + 7;
int n, len, a[N], pos[N], L[N], R[N], add[N], mul[N];
void reset(int x) {
	for(int i = L[x]; i <= R[x]; i ++) {
		(a[i] = a[i] * mul[x] + add[x]) %= mod;
	}
	add[x] = 0; mul[x] = 1;
}
void change_add(int l, int r, int c) {
	reset(pos[l]);
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) (a[i] += c) %= mod;
		return;
	}
	reset(pos[r]);
	for(int i = l; i <= R[pos[l]]; i ++) (a[i] += c) %= mod;
	for(int i = L[pos[r]]; i <= r; i ++) (a[i] += c) %= mod;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++) (add[i] += c) %= mod;	
}
void change_mul(int l, int r, int c) {
	reset(pos[l]);
	if(pos[l] == pos[r]) {
		for(int i = l; i <= r; i ++) (a[i] *= c) %= mod;
		return;
	}
	reset(pos[r]);
	for(int i = l; i <= R[pos[l]]; i ++) (a[i] *= c) %= mod;
	for(int i = L[pos[r]]; i <= r; i ++) (a[i] *= c) %= mod;
	for(int i = pos[l] + 1; i <= pos[r] - 1; i ++)
		(mul[i] *= c) %= mod, (add[i] *= c) %= mod;
}
int main() {
	cin >> n; len = sqrt(n);
	for(int i = 1; i <= n; i ++) cin >> a[i], pos[i] = (i-1) / len + 1;
	for(int i = 1; i <= pos[n]; i ++) L[i] = (i-1) * len + 1, R[i] = min(len * i, n);
	for(int i = 1; i <= pos[n]; i ++) mul[i] = 1;
	for(int i = 1, opt, l, r, c; i <= n; i ++) {
		cin >> opt >> l >> r >> c;
		if(opt == 0) change_add(l, r, c);
		if(opt == 1) change_mul(l, r, c);
		if(opt == 2) cout << ((a[r] * mul[pos[r]] + add[pos[r]]) % mod) << "\n";
	}
	return 0;
}

问题总结

对于区间上的操作,能够通过将区间拆分,统计区间所维护的信息

即使方法可能会有些暴力,但由于 每个块的大小 并不大

因此总的复杂度并不会太高

能够帮助我们解决一些较为复杂的问题

分块的特点就在于方法虽然暴力,但是容易实现

可以避免使用其他复杂的数据结构(难以调试)

便可以成功求解问题

对于分块解决问题,主要考虑对于区间信息的维护

即如何通过一种效率合适的方法将问题的答案进行统计