引言
在处理大规模数组的区间查询与修改问题时,线段树与树状数组是常见的高效数据结构。然而,当操作类型复杂、难以用简单的懒标记维护时,分块算法(Sqrt Decomposition)以其优雅的平方根分解思想,提供了一种代码简洁、逻辑清晰的替代方案。本文将用 Java 实现分块算法,讲解其区间求和、区间修改的核心机制,并分析其时间复杂度。
核心思想
分块算法的核心是将长度为 n 的数组划分为约 √n 个块,每块包含约 √n 个元素。对于任意区间操作:
- 完整块:直接对整块进行批量处理,时间复杂度
O(√n) - 不完整块:暴力逐个处理边缘元素,时间复杂度
O(√n)
单次操作总时间复杂度为 O(√n),既避免了纯暴力的 O(n),又无需线段树复杂的懒标记下传逻辑。
完整 Java 实现
import java.util.Arrays;
/**
* 分块算法实现:支持区间加法修改与区间求和查询
*/
public class SqrtDecomposition {
private final int[] arr; // 原数组
private final int[] block; // 每块的和
private final int[] add; // 每块的懒标记(待下传增量)
private final int blockSize; // 块大小
private final int blockCount; // 块数量
public SqrtDecomposition(int[] nums) {
int n = nums.length;
this.blockSize = (int) Math.sqrt(n) + 1;
this.blockCount = (n + blockSize - 1) / blockSize;
this.arr = Arrays.copyOf(nums, n);
this.block = new int[blockCount];
this.add = new int[blockCount];
// 初始化每块的和
for (int i = 0; i < n; i++) {
block[i / blockSize] += arr[i];
}
}
/**
* 区间加法:将 [l, r] 内每个元素加上 val
*/
public void rangeAdd(int l, int r, int val) {
int leftBlock = l / blockSize;
int rightBlock = r / blockSize;
if (leftBlock == rightBlock) {
// 在同一块内,直接暴力修改
for (int i = l; i <= r; i++) {
arr[i] += val;
}
block[leftBlock] += val * (r - l + 1);
} else {
// 左端不完整块
int leftEnd = (leftBlock + 1) * blockSize - 1;
for (int i = l; i <= Math.min(leftEnd, arr.length - 1); i++) {
arr[i] += val;
}
block[leftBlock] += val * (Math.min(leftEnd, arr.length - 1) - l + 1);
// 中间完整块,使用懒标记
for (int b = leftBlock + 1; b < rightBlock; b++) {
add[b] += val;
}
// 右端不完整块
int rightStart = rightBlock * blockSize;
for (int i = rightStart; i <= r; i++) {
arr[i] += val;
}
block[rightBlock] += val * (r - rightStart + 1);
}
}
/**
* 区间求和:计算 [l, r] 的元素和
*/
public long rangeSum(int l, int r) {
long sum = 0;
int leftBlock = l / blockSize;
int rightBlock = r / blockSize;
if (leftBlock == rightBlock) {
for (int i = l; i <= r; i++) {
sum += arr[i] + add[leftBlock];
}
} else {
// 左端不完整块
int leftEnd = (leftBlock + 1) * blockSize - 1;
for (int i = l; i <= Math.min(leftEnd, arr.length - 1); i++) {
sum += arr[i] + add[leftBlock];
}
// 中间完整块
for (int b = leftBlock + 1; b < rightBlock; b++) {
sum += (long) block[b] + (long) add[b] * blockSize;
}
// 右端不完整块
int rightStart = rightBlock * blockSize;
for (int i = rightStart; i <= r; i++) {
sum += arr[i] + add[rightBlock];
}
}
return sum;
}
/**
* 单点查询:获取位置 idx 的当前值(下传懒标记)
*/
public int pointQuery(int idx) {
return arr[idx] + add[idx / blockSize];
}
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SqrtDecomposition sd = new SqrtDecomposition(nums);
System.out.println("初始区间 [0, 9] 的和: " + sd.rangeSum(0, 9)); // 55
sd.rangeAdd(2, 7, 5);
System.out.println("[2,7] 加 5 后 [0, 9] 的和: " + sd.rangeSum(0, 9)); // 85
System.out.println("位置 5 的值: " + sd.pointQuery(5)); // 11
System.out.println("位置 1 的值: " + sd.pointQuery(1)); // 2
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 初始化 | O(n) | O(√n) |
| 区间修改 | O(√n) | O(1) |
| 区间查询 | O(√n) | O(1) |
| 单点查询 | O(1) | O(1) |
分块算法单次操作的时间复杂度为 O(√n),当 n = 10^5 时约为 316 次运算,在大多数场景下均可接受。其核心优势在于代码简洁、易于扩展,可轻松支持区间最值、区间赋值、区间开方等复杂操作。
应用场景
- 不支持线段树懒标记的复杂操作(如区间模运算、区间开方)
- 需要
O(1)单点查询的场景 - 作为莫队算法的预处理结构
- 离线查询中配合其他算法使用
总结
分块算法以平方根为界,在完整块与零散块之间取得了完美的平衡。它虽不如线段树在对数时间上的极致效率,却以极简的实现成本和极强的扩展性,成为算法竞赛与工程实践中的利器。掌握分块思想,将为你打开区间问题处理的另一扇大门。