分块

分块

前言 | Preface

最近学了分块,所以写了一篇总结

题单:分块

普通分块

引入

分块是一种思想

既然单独的一些元素处理起区间操作来的复杂度很高,那么为什么不将它们看做一个整体呢?

假设一个“整体”的大小是 \(S\),总的元素个数是 \(n\) 个,查询的 \([l,r]\) 区间,这个时候两边不完整的块至多大 \(2 S - 2\),而中间的都是完整的整体

所以易知复杂度可以表示为:\(T(n) = \frac{n}{S} - 2 + 2S - 2 = \mathcal O(\frac{n}{S}+S)\)

由均值不等式易知 \(T(n)\) 最小为 \(S = \sqrt n\) 时,\(T(n) = \mathcal O(\sqrt n)\)

这里涉及到了另外的一个概念————平衡规划,不过很少有人讲,我在后面应该会讲的

实现

考虑将序列分成长度为 \(\lfloor \sqrt n \rfloor\) 的小块,最后一块会不足 \(\lfloor \sqrt{n} \rfloor\),总共会有 \(\lceil \sqrt{n} \rceil\) 个块

对于区间操作 \([l, r]\),考虑两种情况:

  • \(l\) 和 \(r\) 在同一个块内,这个时候直接暴力,时间复杂度 \(\mathcal O(\sqrt{n})\)
  • \(l\) 和 \(r\) 不在同一个块内,不完整的两个端点的块直接暴力,剩余的打上 tag

例题

P13976 数列分块入门 1

就是一道版子

View Code
struct __block{
	ll tag;
	int start, end;
} block[maxn];

ll a[maxn];

int id[maxn];

void solve() {
	
	int n;
	
	cin >> n;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	int block_size = sqrt(n);
	int block_count = (n + block_size - 1) / block_size;
	
	for (int i = 1; i <= n; i++) id[i] = (i - 1) / block_size + 1;
	
	for (int i = 1; i <= block_count; i++) {
		block[i].start = (i - 1) * block_size + 1;
		block[i].end = i * block_size;
	}
	
	block[block_count].end = n;
	
	for (int i = 1; i <= n; i++) {
		int opt, l, r;
		ll c;
		cin >> opt >> l >> r >> c;
		
		if (opt == 0) {
			if (id[l] == id[r]) for (int j = l; j <= r; j++) a[j] += c;
			else {
				for (int j = l; j <= block[id[l]].end; j++) a[j] += c;
				for (int j = id[l] + 1; j <= id[r] - 1; j++) block[j].tag += c;
				for (int j = block[id[r]].start; j <= r; j++) a[j] += c;
			}
		} else
			cout << a[r] + block[id[r]].tag << "\n";
	}
}

[数列分块入门 2-8]

链接见题单

数列分块入门 2

对每一个散块,操作之后进行重建,然后排序,而区间操作的整块则不用,因为加同一个数不会改变相对大小

复杂度 \(\mathcal O(n\sqrt{n}\log \sqrt n)\)

数列分块入门 3

将 2 的操作由求个数改成求最大值即可

数列分块入门 4

维护一个区间和就是了

数列分块入门 5

和这道题 P4145 上帝造题的七分钟 2 / 花神游历各国 有些类似

这道题涉及到了一个叫做势能的概念,它就相当你在银行中存储的钱,会有用完的时候

观察发现,一个数 \(n\) 顶多开方 \(\log\log n\) 次,一个 int 范围内的数就顶多开方 \(5\) 次

基于此,我们可以考虑仅对势能没有用尽、还能够开方的数进行操作

View Code
struct __block{
	ll tag;
	int start, end;
	ll sum;
	bool vis;
} block[maxn];

ll a[maxn];
int id[maxn];

void solve() {
	int n;
	cin >> n;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	int block_size = sqrt(n);
	int block_count = (n + block_size - 1) / block_size;
	
	for (int i = 1; i <= n; i++) 
		id[i] = (i - 1) / block_size + 1;
	
	for (int i = 1; i <= block_count; i++) {
		block[i].start = (i - 1) * block_size + 1;
		block[i].end = min(i * block_size, n);
		block[i].tag = 0;
		block[i].sum = 0;
		block[i].vis = false;
		for (int j = block[i].start; j <= block[i].end; j++)
			block[i].sum += a[j];
	}
	
	for (int i = 1; i <= n; i++) {
		int opt, l, r;
		cin >> opt >> l >> r;
		
		if (opt == 0) {
			if (id[l] == id[r]) {
				if (!block[id[l]].vis) {
					for (int j = l; j <= r; j++) {
						block[id[l]].sum -= a[j];
						a[j] = sqrt(a[j]);
						block[id[l]].sum += a[j];
					}
				}
			} else {
				if (!block[id[l]].vis) {
					for (int j = l; j <= block[id[l]].end; j++) {
						block[id[l]].sum -= a[j];
						a[j] = sqrt(a[j]);
						block[id[l]].sum += a[j];
					}
				}
				for (int j = id[l] + 1; j <= id[r] - 1; j++) {
					if (block[j].vis) continue;
					bool flag = true;
					for (int k = block[j].start; k <= block[j].end; k++) {
						block[j].sum -= a[k];
						a[k] = sqrt(a[k]);
						block[j].sum += a[k];
						if (a[k] > 1) flag = false;
					}
					if (flag) block[j].vis = true; // 全是 1,势能已经用完了
				}
				if (!block[id[r]].vis) {
					for (int j = block[id[r]].start; j <= r; j++) {
						block[id[r]].sum -= a[j];
						a[j] = sqrt(a[j]);
						block[id[r]].sum += a[j];
					}
				}
			}
		} else {
			ll answer = 0;
			
			if (id[l] == id[r]) {
				for (int j = l; j <= r; j++)
					answer += a[j];
			} else {
				for (int j = l; j <= block[id[l]].end; j++)
					answer += a[j];
				for (int j = id[l] + 1; j <= id[r] - 1; j++) 
					answer += block[j].sum;
				for (int j = block[id[r]].start; j <= r; j++)
					answer += a[j];
			}
			
			cout << answer << "\n";
		}
	}
}

这样子虽然看起来时间复杂度有足足 \(\mathcal O(n^2)\),可是由于 int 范围内的数最多进行 \(5\) 次开方操作,所以均摊下来还是 \(\mathcal O(n\sqrt n)\)

数列分块入门 7

维护两个 tag,一个加 add,一个乘 mul 就可以了

这里需要注意,在区间乘操作的时候,此时的 tag 更新是这样的:\((\text{mul} \times a_i + \text{add}) \times t = (\text{mul}\times t) \times a_i + \text{add} \times t\),也就是两个 tag 都要相乘

注意取模就行了

数列分块入门 8

记录一下区间赋值的操作,然后对于处理的散块,将 tag 下传,然后统计答案,最后附上 tag

对于整块,如果有区间的赋值标记,则看这个标记是否是当前这个数,如果是,统计答案,不是则赋值跳过

如果没有,则遍历统计答案,赋值

P2801 教主的魔法

其实就是数列分块入门 2,只用稍微改一下代码就可以了

CF455D Serega and Fun

我觉得这道题和它的名字一样有意思

发现值域范围不大,可以使用桶来存出现次数

至于右移,STL 里面有一个十分好用的 deque(双端队列)

所以我们考虑每一个块对应一个双端队列,剩下的就是模拟的事了

View Code
int last = 0;
	
	for (int i = 1; i <= q; i++) {
		int opt;
		cin >> opt;
		
		if (opt == 1) {
			int l, r;
			cin >> l >> r;
			l = (l + last - 1) % n + 1;
			r = (r + last - 1) % n + 1;
			if (l > r) swap(l, r);
			
			if (id[l] == id[r]) {
				int value = dq[id[l]][r - block[id[l]].start];
				
				dq[id[l]].erase(dq[id[l]].begin() + (r - block[id[l]].start));
				dq[id[l]].insert(dq[id[l]].begin() + (l - block[id[l]].start), value);
			} else {
				dq[id[l]].insert(dq[id[l]].begin() + (l - block[id[l]].start), dq[id[r]][r - block[id[r]].start]);
				
				cnt[id[l]][dq[id[r]][r - block[id[r]].start]]++;
				cnt[id[r]][dq[id[r]][r - block[id[r]].start]]--;
				
				dq[id[r]].erase(dq[id[r]].begin() + (r - block[id[r]].start));
				
				for (int j = id[l] + 1; j <= id[r]; j++) {
					int temp = dq[j - 1].back();
					
					dq[j].push_front(temp);
					++cnt[j][temp];
					
					dq[j - 1].pop_back();
					--cnt[j - 1][temp];
				}
			}
		} else {
			int l, r, k;
			cin >> l >> r >> k;
			l = (l + last - 1) % n + 1;
			r = (r + last - 1) % n + 1;
			k = (k + last - 1) % n + 1;
			if (l > r) swap(l, r);
			int answer = 0;
			if (id[l] == id[r]) {
				for (int j = l; j <= r; j++)
					if (dq[id[l]][j - block[id[l]].start] == k)
						answer++;
			} else {
				for (int j = l; j <= block[id[l]].end; j++) if (dq[id[l]][j - block[id[l]].start] == k) answer++;
				for (int j = block[id[r]].start; j <= r; j++) if (dq[id[r]][j - block[id[r]].start] == k) answer++;
				for (int j = id[l] + 1; j < id[r]; j++) answer += cnt[j][k];
			}
			cout << answer << "\n";
			last = answer;
		}
	}

哦对了,deque 支持随机访问(下标访问)

块状链表

如果我们要支持 insert 和 erase 操作,该怎么办?

很简单,我们将分出来的块看做一个数组就行了,然后像链表那般连起来(实际上不会连的)

但是如果一个块内插入了很多元素,近乎退化成了普通数组怎么办?

当然是拼夕夕砍一刀

其实砍一刀的思路是对的,当一个块的大小大于 \(2B\) (\(B\) 是给定的阈值)时,将它裂成两段,这样子每一段都维持在了 \(\B \sim 2B\) 的范围内了

这就是分裂操作了

但是如果删除操作太多了,退化成链表了怎么办?

当然是拼起来

当然,这个合并操作很少有人写,毕竟删除多了,自然 \(n\) 也就小了

模版就如下了

vector<vector<ll>> block;

int B;

void update(int position, ll value) {
	int current = 0;
	int k = position - 1;
	
	while (current < block.size() - 1 and k >= block[current].size()) {
		k -= block[current].size();
		current++;
	}
	
	block[current].insert(block[current].begin() + k, value);
	
	if (block[current].size() > 2 * B) {
		vector<ll> new_block(block[current].begin() + B, block[current].end());
		block[current].erase(block[current].begin() + B, block[current].end());
		block.insert(block.begin() + current + 1, new_block);
	}
}

ll query(int position) {
	int current = 0;
	int k = position - 1;
	
	while (current < block.size() - 1 and k >= block[current].size()) {
		k -= block[current].size();
		current++;
	}
	
	return block[current][k];
}

例题

P13981 数列分块入门 6

版子,使用上面的模板就可以通过

平衡规划

由于没有什么好的文章,所以问了一下 AI,以下是我自己的理解,可能有误

之前香槟说要讲一道平衡规划的题,可是一直没有讲,现在看到分块才想起来

引入

首先,我们需要明白,平衡规划的定义是什么

定义 1

平衡规划指的是在给定的资源约束(输入规模 \(N\))下,构造一个包含可调参数 \(B\) 的复杂度函数 \(T(N, B)\),通过求解 \(\min\limits_B T(N, B)\) ,找到使得系统整体效率达到帕累托最优 Pareto Optimality的参数配置

帕累托最优 Pareto Optimality
在不损害其他目标的前提下,无法再改进任何一个目标的一个解

这个定义就是找到一个在给定的规模下的最优解,根据选取的 \(B\) 的含义的不同,可能会有不同,而根据具体的规模定义,也会有不同

为什么一定要满足帕累托最优

假设算法 A 是我们平衡规划出来的,而 B 是满足在这个规模下的帕累托最优的算法

这样则有:

\(T_A(n) \le T_B(n)\),\(S_A(n) \le S_B(n)\)(\(S\) 是空间复杂度)

并且这之中一定有一个是 \(\lt\),那么算法 A 就不是这个规模下的最优解

解耦

通常来讲,耦合在一起的步骤十分的难以操作,经常是 \(\mathcal O(f(n) \cdot g(n))\) 的时间复杂度

通过解耦操作,我们可以降成 \(\mathcal O(w(n)(f'(n) + g'(n)))\) 的形式,其中 \(w(n)\) 是复杂度低于 \(f(n)\)、\(g(n)\) 的函数

解耦后的复杂度通常与解耦的操作有关

以分块为例

其实我们发现,分块的主要时间复杂度优化在于懒标记,所以我们可以考虑从懒标记出发

最早学到懒标记,是在线段树中的,对应的是一整块的区间

所以我们会想到将序列分成很多份,但这还不是解耦,因为在修改之后,还是必须立马操作

这个时候,懒标记的作用很明显了,它对时间进行了解耦,将原本应该立即执行的操作延后到询问,正是这种延后,使得不用每一个元素都访问

所以时间复杂度有 \(\mathcal O(n ^ 2)\) 变成了 \(\mathcal O(\frac{n}{B} + B)\)

很神奇

总结

所以所平衡规划的主要概念就是 平衡 和 解耦,平衡让我们找到最优解,解耦则是方法,由于规模的定义可以不同,随意根据我们想要的,可以自定义,然后找出合适的解

找个合适的时间写一篇博客吧

根号分治

与其说是算法,不如说是一种思想

引入

其实是对于平衡规划的应用

对应的概念叫做根号平衡
这里有一篇总结的十分全的文章:对祂使用根号平衡吧(十二重分块法)

思路

对于一道数据规模 \(n\),查询 \(q\) 的题目,朴素算法的复杂度一般是 \(\mathcal O(nq)\)

但是 \(n\) 和 \(q\) 是耦合在一起的,两者非正交,所以在不引入外界条件的情况下无法分成 \(\mathcal O(f(n) + g(q))\)

所以我们现在引入一个阈值 \(B\),小于 \(B\) 的直接暴力修改,\(\mathcal O(B)\)

大于 \(B\) 的,分成很多个块加上预处理修改,\(\mathcal O(\frac{n}{B})\)

所以总复杂度降为 \(T(n,q) = \mathcal O(nB + \frac{nq}{B}))\)

由均值不等式得到,当 \(B = \sqrt n\) 的时候 \(T(n,q)\) 最小,最小为 \(T(n,q) = \mathcal O((n + q)\sqrt n)\)

例题

P3396 哈希冲突

用 ans[mod][rest] 表示模数为 mod,余数为 rest 的所有数的和

有两种思路:

  1. 纯暴力(计算 ans 数组
  2. 以模数为步长,一步一步地跳

我们会发现而更适合模数大的情况,而一则适合模数小的情况

考虑设置一个阈值 \(B\),则低于阈值的使用方案2,大于则使用方案1

这样子复杂度变为 \(\mathcal O(nB + \frac{nq}{B})\)

取 \(B = \sqrt n\),复杂度为 \(T(n) = \mathcal O(q\sqrt n)\)

View Code
using ll = long long;

const int maxn = 1.5e5 + 5;
const int max_block = 405;

int answer[max_block][max_block];

int n, m, B;

int arr[maxn];

void solve() {
	cin >> n >> m;
	B = sqrt(n);
	
	for (int i = 1; i <= n; i++) {
		cin >> arr[i];
		for (int mod = 1; mod <= B; mod++) answer[mod][i % mod] += arr[i];
	}
	
	while (m--) {
		char opt;
		int x, y;
		cin >> opt >> x >> y;
		
		if (opt == 'C') {
			int delta = y - arr[x];
			arr[x] = y;
			
			for (int mod = 1; mod <= B; mod++) answer[mod][x % mod] += delta;
		} else if (opt == 'A'){
			
			if (x <= B) cout << answer[x][y] << "\n";
			else {
				int sum = 0;
				for (int i = (y == 0 ? x : y); i <= n; i += x) sum += arr[i];
				cout << sum << "\n";
			}
		}
	}
}

其余

[P5309 [Ynoi] 初始化]

不对,怎么会有区间查询呢?

首先要维护一个区间和 sum

然后发现这个修改有一点阴

对于大的块,直接按照步长 \(x (x \gt B)\) 跳跃,暴力修改 a 和 sum

对于小的块,如果直接按照步长 \(x (x \le B)\) 跳的话,时间复杂度可能退化为 \(\mathcal O(nq)\),所以考虑存起来

用 prefix[x][r] 表示 步长为 \(x\) 时,所有修改 \(y \le r\) 的贡献
用 suffix[x][r] 表示 步长为 \(x\) 时,所有修改 \(y \ge r\) 的贡献

这样子询问的时候就可以用原数组加上修改的累计贡献快速求出了

时间复杂度 \(\mathcal O((n + q)\sqrt n)\)

View Code
const int maxn = 2e5 + 5;

const ll mod = 1e9 + 7;

struct __block{
	ll sum;
	int start, end;
} block[maxn];

ll a[maxn];

ll prefix[maxn][500], suffix[maxn][500];

int id[maxn];

int n, m;

int block_size;

ll Summation_of_Block(int l, int r) {
	int xb = id[l], yb = id[r];
	ll result = 0;
	if (xb == yb)
		for (int i = l; i <= r; i++) result = (result + a[i]) % mod;
	else {
		for (int i = l; i <= block[xb].end; i++) result = (result + a[i]) % mod;
		for (int i = xb + 1; i < yb; i++) result = (result + block[i].sum) % mod;
		for (int i = block[yb].start; i <= r; i++) result = (result + a[i]) % mod;
	}
	return result;
}

void update(int x, int y, int delta) {
	if (x >= block_size) {
		for (int i = y; i <= n; i += x) {
			a[i] = (a[i] + delta) % mod;
			block[id[i]].sum = (block[id[i]].sum + delta);
		}
	} else {
		for (int i = y; i <= x; i++) prefix[x][i] = (prefix[x][i] + delta) % mod;
		for (int i = 1; i <= y; i++) suffix[x][i] = (suffix[x][i] + delta) % mod;
	}
}

ll query(int x, int y) {
	ll result = Summation_of_Block(x, y);
	for (int i = 1; i <= block_size; i++) {
		ll X = (x - 1) / i + 1, Y = (y - 1) / i + 1;
		ll length = Y - X - 1;
		if (X == Y) result = (result - prefix[i][(x - 1) % i] + prefix[i][(y - 1) % i + 1]) % mod;
		else result = (result + 1ll * length * prefix[i][i] + prefix[i][(y - 1) % i + 1] + suffix[i][(x - 1) % i + 1]) % mod;
	}
	return (result + mod) % mod;
}

void solve() {
	
	cin >> n >> m;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	block_size = sqrt(n);
	int block_count = (n + block_size - 1) / block_size;
	
	for (int i = 1; i <= n; i++) id[i] = (i - 1) / block_size + 1;
	
	for (int i = 1; i <= block_count; i++) {
		block[i].start = (i - 1) * block_size + 1;
		block[i].end = min(i * block_size, n);
	}
		
	for (int i = 1; i <= n; i++)
		block[id[i]].sum = (block[id[i]].sum + a[i]) % mod;
	
	while (m--) {
		int opt;
		cin >> opt;
		if (opt == 1) {
			int x, y, z;
			cin >> x >> y >> z;
			update(x, y, z);
		} else {
			int x, y;
			cin >> x >> y;
			cout << query(x, y) << "\n";
		}
	}
}
posted @ 2026-07-20 21:29  Yangyihao  阅读(13)  评论(0)    收藏  举报