分块

创建时间:2025-04-08


分块的思想

分块,运用了隔离的思想,将较大的 \(n\) 划分成若干小块,使每次操作仅需要对较少的整块进行整体操作和较少的散块进行暴力操作。这样的思想使复杂的问题变得简单无脑,且时间复杂度一般也只有 \(O(m \sqrt{n})\),与线段树等其他数据结构的 \(O(m \log n)\) 相差不大,但思路更简单清晰。同时,分块的空间复杂度只有 \(O(n)\),这使得分块在考场上很适合骗分(当然,也适合写正解),很受欢迎。

分块的具体实现

洛谷P3372为例,这道题使线段树+懒标记的模板题,要求维护长度为 \(n\) 的序列 \(a\),进行 \(m\) 此操作:将区间 \([x,y]\) 内的所有数字加上 \(k\),或求出区间 \([x,y]\) 内所有数的和。

如果用线段树求解,需要维护基础和 \(sum[i]\) 和懒标记 \(tag[i]\),分别表示在没有标记的情况下节点 \(i\) 表示的区间内所有数的和以及节点 \(i\) 表示的区间内的所有数都共同加上的一个数,并使用pushup函数和pushdown函数更新它们,这里不再赘述。

考虑使用分块求解:

将长度为 \(n\) 的序列分成长度为 \(size\) 的块,如果最后一个块并未覆盖到 \(n\),则再加一个块。这样,便得到了 \(cnt\) 个块(\(cnt \le \frac{n}{size}+1\)),为这 \(cnt\) 个块维护 \(L\) 数组、\(R\) 数组、\(sum\) 数组和 \(tag\) 数组,\(L[i]\)\(R[i]\) 表示第 \(i\) 个块的左端点和右端点,\(sum\)\(tag\) 意义同线段树中的 \(sum\)\(tag\),但此处的 \(tag\) 不像线段树中的 \(tag\) 一样需要pushdown下传、更新。

\(pos[i]\) 表示 \(i\) 所属的块的编号。

考虑如何将区间 \([lt,rt]\) 内的所有数字加上 \(k\):设 \(lp=pos[lt]\)\(rp=pos[rt]\),如果 \(lp=rp\),即整个区间在一个块中,我们可以直接以 \(O(size)\) 的复杂度遍历 \([lt,rt]\) 中的每一个元素,暴力地更新 \(a\)\(sum\);如果 \(lp \neq rp\),则将 \([lt,rt]\) 分为 \([lt,R[lp]]\)\([L[lp+1],R[rp-1]]\)\([L[rp],rt]\) 三个部分,对于 \([lt,R[lp]]\)\([L[rp],rt]\) 这两个部分,可以参考 \(lp=rp\) 的情况暴力处理,对于 \([L[lp+1],R[rp-1]]\) 的部分,可以发现这是由若干个整块构成的!故可以直接修改第 \(lp+1 \sim rp-1\) 块的 \(tag\)

类似的,可以得出查询操作的解法:对于 \(lp=rp\) 的情况,直接将 \(lp\sim rp\) 中的 \(a[i]+tag[lp]\) 累加进 \(ans\);否则在 \([lt,R[lp]]\)\([L[rp],rt]\) 中暴力累加,在 \([L[lp+1],R[rp-1]]\) 中,即第 \(lp+1 \sim rp-1\) 个块,使 \(ans\) 加上 \(sum[i]+tag[i] \times (R[i]-L[i]+1)\)

那么 \(size\) 应该取多少呢?可以发现,上述算法的时间复杂度是 \(O(m \cdot (size+cnt))\) 的,即 \(O(m \cdot (size+\frac{n}{size}))\) 的。显然,当 \(size=\sqrt{n}\) 时复杂度最小,为 \(O(m\sqrt{n})\)

以上便是分块的实现,Code is here:

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int MAX_N = 1e5 + 50;

int n, m, L[MAX_N], R[MAX_N], pos[MAX_N];
ll a[MAX_N], sum[MAX_N], tag[MAX_N];

void init() {
	int cnt = sqrt(n);
	const int size = sqrt(n);
	for (int i = 1; i <= cnt; i++) {
		L[i] = (i - 1) * size + 1;
		R[i] = i * size;
	}
	if (R[cnt] < n) {
		L[cnt + 1] = R[cnt] + 1;
		R[++cnt] = n;
	}
	for (int i = 1; i <= cnt; i++)
		for (int j = L[i]; j <= R[i]; j++) {
			sum[i] += a[j];
			pos[j] = i;
		}
}

void modify(int lt, int rt, ll val) {
	int lp = pos[lt], rp = pos[rt];
	if (lp == rp) {
		for (int i = lt; i <= rt; i++)
			a[i] += val, sum[lp] += val;
	} else {
		for (int i = lt; i <= R[lp]; i++)
			a[i] += val, sum[lp] += val;
		for (int i = lp + 1; i < rp; i++)
			tag[i] += val;
		for (int i = L[rp]; i <= rt; i++)
			a[i] += val, sum[rp] += val;
	}
}

ll query(int lt, int rt) {
	int lp = pos[lt], rp = pos[rt];
	ll ans = 0;
	if (lp == rp) {
		for (int i = lt; i <= rt; i++)
			ans += a[i] + tag[lp];
	} else {
		for (int i = lt; i <= R[lp]; i++)
			ans += a[i] + tag[lp];
		for (int i = lp + 1; i < rp; i++)
			ans += sum[i] + tag[i] *  (R[i] - L[i] + 1);
		for (int i = L[rp]; i <= rt; i++)
			ans += a[i] + tag[rp];
	}
	return ans;
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	init();
	for (int opt, x, y; m--; ) {
		cin >> opt >> x >> y;
		if (opt == 1) {
			ll k;
			cin >> k;
			modify(x, y, k);
		} else
			cout << query(x, y) << '\n';
	}
	return 0;
} 

分块的应用

分块有着次于线段树的复杂度,但有的线段树解决不了的问题却可以用分块解决,且分块拥有其独特的优点:隔离!运用隔离,可以解决很多难以直接解决的问题,例如减少排序的复杂度、将建模隐蔽的问题隔离并简化,以下问题为例:

洛谷P3870(模板变形)
洛谷P4145(经典诈骗题)
洛谷P2801(隔离性排序)
洛谷P4109(合适块长的选择)
洛谷P3202(较为隐蔽的隔离性)
洛谷P5356(隐蔽性排序+二分答案)
洛谷P1975(隔离性排序+逆序对)

posted @ 2026-05-19 12:26  xubaichuan  阅读(14)  评论(0)    收藏  举报