古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

分块思想

简介:

分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
我们来看一下这道题
题目链接:https://www.luogu.com.cn/problem/P2801

P2801 教主的魔法

题目描述

教主最近学会了一种神奇的魔法,能够使人长高。于是他准备演示给 XMYZ 信息组每个英雄看。于是 \(N\) 个英雄们又一次聚集在了一起,这次他们排成了一列,被编号为 \(1, 2, \ldots, N\)

每个人的身高一开始都是不超过 \(1000\) 的正整数。教主的魔法每次可以把闭区间 \([L, R]\)\(1≤L≤R≤N\))内的英雄的身高全部加上一个整数 \(W\)。(虽然 \(L=R\) 时并不符合区间的书写规范,但我们可以认为是单独增加第 \(L(R)\) 个英雄的身高)

CYZ、光哥和 ZJQ 等人不信教主的邪,于是他们有时候会问 WD 闭区间 \([L, R]\) 内有多少英雄身高大于等于 \(C\),以验证教主的魔法是否真的有效。

WD 巨懒,于是他把这个回答的任务交给了你。

输入格式

\(1\) 行为两个整数 \(N, Q\)\(Q\) 为问题数与教主的施法数总和。

\(2\) 行有 \(N\) 个正整数,第 \(i\) 个数代表第 \(i\) 个英雄的身高。

\(3\) 到第 \(Q+2\) 行每行有一个操作:

  1. 若第一个字母为 M,则紧接着有三个数字 \(L, R, W\)。表示对闭区间 \([L, R]\) 内所有英雄的身高加上 \(W\)

  2. 若第一个字母为 A,则紧接着有三个数字 \(L, R, C\)。询问闭区间 \([L, R]\) 内有多少英雄的身高大于等于 \(C\)

输出格式

对每个 A 询问输出一行,仅含一个整数,表示闭区间 \([L, R]\) 内身高大于等于 \(C\) 的英雄数。

输入输出样例 #1

输入 #1

5 3
1 2 3 4 5
A 1 5 4
M 3 5 1
A 1 5 4

输出 #1

2
3

说明/提示

【输入输出样例说明】

原先 \(5\) 个英雄身高为 \(1, 2, 3, 4, 5\),此时 \([1, 5]\) 间有 \(2\) 个英雄的身高大于等于 \(4\)。教主施法后变为 \(1, 2, 4, 5, 6\),此时 \([1, 5]\) 间有 \(3\) 个英雄的身高大于等于 \(4\)

【数据范围】

对于 \(30\%\) 的数据,\(N≤1000\)\(Q≤1000\)

对于 \(100\%\) 的数据,\(N≤10^6\)\(Q≤3000\)\(1≤W≤1000\)\(1≤C≤10^9\)


\(\text{upd 2022.8.18}\):新增加一组 Hack 数据。
\(\text{upd 2023.8.16}\):新增加一组 Hack 数据

思考

题目要我们干什么呢?
进行很多个操作,增加操作(实现对区间内的所有数进行增加),查询操作(查询区间内有多少个数大于x(输入的))
我们看到区间修改与区间查询,我们首先会想到线段树,但是经过我的思考,要储存的信息太多了,我无法想到用线段树来维护,于是我们可以采用分块的思想。

分块,是一种优雅的暴力,它通过对数列分段,完成对数列一些区间操作和区间查询的操作,是一种根号算法。

我们引入以下变量
本文中,以下变量有特定的含义:

\(block\):块的大小
\(n\):被分块的数列的大小(长度)
\(L x\):第 x 号块的左边界
\(R x ​\):第 x 号块的右边界
\(tot\):块的数量
\(belong\) \(x\):第 x 号元素所属的块

建块

首先我们来建块
通常把一段序列分为\(\sqrt n\) n个块
所以我们来建块,块的大小为\(\sqrt n\)

确定块的数量

在确定了块的大小后,块的数目就很容易确定了。
但是 n 不一定是一个完全平方数,我们需要把最后几个无法凑足 block 个元素的再单独分一个块。
然后我们标记标记每个块的左右边界,并且处理每个节点属于哪一个块,再对每个块的元素进行初始化,就完成了基本的分块操作。

void build() {
	block = ( i64 ) sqrt(n);
	tot = ( n + block - 1 ) / block;//记录块的总数
	for (int i = 1; i <= n; i++) {
		belong [ i ] = ( i - 1 ) / block + 1;//第i个字符属于哪一个块
	}
	for (int i = 1; i <= tot; i++) {
		l [ i ] = ( i - 1 )*block + 1;//找到每个块的左右边界
		r [ i ] = min(i*block , n);
		sort(d + l [ i ] , d + r [ i ] + 1);//对每个块进行排序
	}

}//分块

修改和查询

接下来我们来实现修改和查询操作:
对于修改操作,如果采用暴力的方法,时间复杂度过高,所以我们采用线段树中懒惰标记的思想
考虑应用在分块中。在修改操作中,如果是整块,就不维护每个的具体信息,而是在这个块的 lazy 标记上加上 k。对于没有整块修改的部分(即块 \(belong x\)\(belong y​\) 的修改部分),暴力修改。这样的话,第 i 个数据$ a i$​的真正数据值为 \(ai+lazy\)

void modify_part(int bid, int st, int ed) {
	for(int i = st; i <= ed; i++) {
		a[i] += k;
	}
	int len = r[bid] - l[bid] + 1;
	memcpy(d + l[bid], a + l[bid], len * sizeof(i64));
	sort(d + l[bid], d + r[bid] + 1);//修改完后重新给块内元素排序;
}

void modify() {
	if(belong[x] == belong[y]) {
		modify_part(belong[x], x, y);//x,y在同一个块,直接修改
		return;
	}

	modify_part(belong[x], x, r[belong[x]]);//部分暴力修改
	modify_part(belong[y], l[belong[y]], y);

	for(int i = belong[x] + 1; i < belong[y]; i++) {//将中间的块加上懒惰标记
		laz[i] += k;
	}
}

查询操作

查询操作类似,如果包含这一块,对这一块内的元素,进行二分查找,如果不包含,使用暴力查找

int query_part(int bid, int st, int ed) {
	int ret = 0;
	for(int i = st; i <= ed; i++) {//暴力查找
		if(a[i] + laz[bid] >= k) {
			++ret;
		}
	}
	return ret;
}
int query() {
	if(belong[x] == belong[y]) {
		return query_part(belong[x], x, y);
	}

	int ansL = query_part(belong[x], x, r[belong[x]]);//部分查找
	int ansR = query_part(belong[y], l[belong[y]], y);//部分查找
	int ansM = 0;

	for(int i = belong[x] + 1; i < belong[y]; i++) {
		int pos = lower_bound(d + l[i], d + r[i] + 1, k - laz[i]) - d;//二分查找块内临界值
		ansM += r[i] - pos + 1;
	}

	return ansL + ansR + ansM;//返回答案
}

这样就基本实现了一个分块思想

AC代码
#include <iostream>
#include <queue>
#include <set>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
using i64 = long long;
const i64 M = 1000005;
i64 n ,x,y ,k,q , m , a [ 1000005 ] , d [ 1000005 ];
i64 l [ 500005 ] , r [ 500005 ] , laz [ 500005 ];
i64 belong [ M ];
i64 tot;
i64 block;
void build() {
	block = ( i64 ) sqrt(n);
	tot = ( n + block - 1 ) / block;//记录块的总数
	for (int i = 1; i <= n; i++) {
		belong [ i ] = ( i - 1 ) / block + 1;//第i个字符属于哪一个块
	}
	for (int i = 1; i <= tot; i++) {
		l [ i ] = ( i - 1 )*block + 1;//找到每个块的左右边界
		r [ i ] = min(i*block , n);
		sort(d + l [ i ] , d + r [ i ] + 1);
	}

}//分块
void modify_part(int bid, int st, int ed) {
	for(int i = st; i <= ed; i++) {
		a[i] += k;
	}
	int len = r[bid] - l[bid] + 1;
	memcpy(d + l[bid], a + l[bid], len * sizeof(i64));
	sort(d + l[bid], d + r[bid] + 1);
}

void modify() {
	if(belong[x] == belong[y]) {
		modify_part(belong[x], x, y);
		return;
	}

	modify_part(belong[x], x, r[belong[x]]);
	modify_part(belong[y], l[belong[y]], y);

	for(int i = belong[x] + 1; i < belong[y]; i++) {
		laz[i] += k;
	}
}
int query_part(int bid, int st, int ed) {
	int ret = 0;
	for(int i = st; i <= ed; i++) {
		if(a[i] + laz[bid] >= k) {
			++ret;
		}
	}
	return ret;
}
int query() {
	if(belong[x] == belong[y]) {
		return query_part(belong[x], x, y);
	}

	int ansL = query_part(belong[x], x, r[belong[x]]);
	int ansR = query_part(belong[y], l[belong[y]], y);
	int ansM = 0;

	for(int i = belong[x] + 1; i < belong[y]; i++) {
		int pos = lower_bound(d + l[i], d + r[i] + 1, k - laz[i]) - d;
		ansM += r[i] - pos + 1;
	}

	return ansL + ansR + ansM;
}
int main() {
	ios::sync_with_stdio();
	cin.tie(0) , cout.tie(0);
	cin >> n>>q;
	for (int i = 1; i <= n; i++) {
		cin >> a [ i ];
		d [ i ] = a [ i ];
	}
	build();
	while (q--) {
		char c;
		cin >> c;
		if (c == 'M') {

			cin >> x >> y >> k;
			modify();
		}
		if (c == 'A') {

			cin >> x >> y >> k;
			cout << query() << endl;
		}
	}


}
posted @ 2025-08-20 20:36  Morphis‘  阅读(10)  评论(0)    收藏  举报

特别策划 · CINEMATIC NOTES

风沙与孤骑

风起塞外,
胜负在刀剑之前。

“把复杂拆成秩序,把未知写成答案。”
Morphis · 山河一卷 愿你从这里出发,仍能听见山风。 影像:farfarSébastien Goldberg / Unsplash