序列分块 —— 如何优雅的暴力
最近学了分块,写一篇文章巩固一下。
由于本人水平很菜,如有错误的地方还请多多指教。
建议先去学 线段树。
参考了 oi-wiki 上的文章。
分块入门
引入
给定一个序列,有区间查询和区间修改操作。
观察发现:
| 方法 | 查询 | 修改 |
|---|---|---|
| 前缀和 | ||
| 差分 |
那么,有没有什么既好写时间复杂度又不高的方法呢?
有的兄弟,有的。
分块,顾名思义,就是把整个数组分成若干个块。
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
具体来说,对于整块,我们做的操作就是直接打标记;对散块(不完整的块)我们直接暴力处理。
设块长为
其中
由均值不等式:
当且仅当
因此块长取
下面来讲一下分块的基本实现。
首先,先分
定义以下变量:
int block = sqrt(n);
int t = n / block;//块的数量
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;//起始位置
ed[i] = i * block;//结束位置
}
ed[t] = n;//最后一个散块
然后,我们要把每个数字的位置给记录下来(记录在第几个块)。
定义
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;//向上取整
接着,我们预处理出每一个块的答案。
定义
for(int i = 1; i <= t; ++i){
for(int j = st[i]; j <= ed[i]; ++j) sum[i] += a[j];//第i个块,从st[i]开始,ed[i]结束
}
这就是分块的预处理,时间复杂度总共
接下来就到了重头戏:
区间修改和区间查询。
区间修改
对于区间修改,我们需要引入一个知识 —— 懒标记。
对整块修改时,如果逐个改每个元素,复杂度
懒标记的思想是:不改,只给这个块打上标签。
定义
利用分块实现区间修改的思路如下:
把区间
- 左散块(不完整);
- 中间整块;
- 右散块(不完整)。
对于散块,我们直接暴力处理。
对于整块,我们可以利用懒标记,把当前块的懒标记给加上修改的值。
:::info[修改代码]
void update(int L, int R, int d){
int p = pos[L], q = pos[R];//在哪一个块
if(p == q){
for(int i = L; i <= R; ++i) a[i] += d;//在一个块内且是散块,直接暴力
sum[p] += d * (R - L + 1);//sum[p] 表示第 p 块的和,这里有 (R - L + 1) 个元素被修改,每个加上了 d
}else{
for(int i = p + 1; i <= q - 1; i++) add[i] += d;//先累加整块,p + 1 是第一个,q - 1 是最后一个
for(int i = L; i <= ed[p]; i++) a[i] += d;//处理散块
sum[p] += d * (ed[p] - L + 1);
for(int i = st[q]; i <= R; i++) a[i] += d;//处理散块
sum[q] += d * (R - st[q] + 1);
}
}
:::
区间查询
思路与区间修改很像,都是整块直接算,散块暴力。
:::info[查询代码]
int query(int L, int R){
int p = pos[L], q = pos[R];//在哪一个块
int ans = 0;
if(p == q){
for(int i = L; i <= R; ++i) ans += a[i];//在一个块内且是散块,直接暴力
ans += add[p] * (R - L + 1);//加上懒标记
}else{
for(int i = p + 1; i <= q - 1; ++i){
ans += sum[i] + add[i] * (ed[i] - st[i] + 1);//中间整块,原本的和加懒标记的值乘元素个数
}
for(int i = L; i <= ed[p]; ++i) ans += a[i];//左散块
ans += add[p] * (ed[p] - L + 1);
for(int i = st[q]; i <= R; ++i) ans += a[i];//右散块
ans += add[q] * (R - st[q] + 1);
}
return ans;
}
:::
对比一下其他数据结构
| 分块 | 线段树 | 树状数组 | |
|---|---|---|---|
| 单次操作复杂度 | |||
| 码量 | 中 | 大 | 小 |
| 空间复杂度 | |||
| 维护信息 | 灵活,可排序、二分等 | 较灵活 | 仅限线性运算 |
| 适用场景 | 区间众数、第 |
区间加乘、区间最值等 | 前缀和、逆序对等简单问题 |
| 实现难度 | 低 | 高 | 低 |
| 理解难度 | 低 | 中 | 高 |
| 可读性 | 高 | 中 | 低 |
由上图可知,线段树虽然功能多,但是码量大,而树状数组虽然码量少,但是适用性、可读性都远远小于线段树,而分块就在两者之间。
例题
P13976 数列分块入门 1
板子,区间查询 + 区间修改。
直接按照上面的板子写即可。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 3e5 + 10;
int st[N], ed[N], pos[N], add[N], sum[N], a[N];
void update(int L, int R, int d){
int p = pos[L], q = pos[R];
if(p == q){
for(int i = L; i <= R; ++i) a[i] += d;
sum[p] += d * (R - L + 1);
}else{
for(int i = p + 1; i <= q - 1; i++) add[i] += d;
for(int i = L; i <= ed[p]; i++) a[i] += d;
sum[p] += d * (ed[p] - L + 1);
for(int i = st[q]; i <= R; i++) a[i] += d;
sum[q] += d * (R - st[q] + 1);
}
}
int query(int L, int R){
int p = pos[L], q = pos[R];
int ans = 0;
if(p == q){
for(int i = L; i <= R; ++i) ans += a[i];
ans += add[p] * (R - L + 1);
}else{
for(int i = p + 1; i <= q - 1; ++i){
ans += sum[i] + add[i] * (ed[i] - st[i] + 1);
}
for(int i = L; i <= ed[p]; ++i) ans += a[i];
ans += add[p] * (ed[p] - L + 1);
for(int i = st[q]; i <= R; ++i) ans += a[i];
ans += add[q] * (R - st[q] + 1);
}
return ans;
}
signed main(){
int n;
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
int block = sqrt(n);
int t = n / block;
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[t] = n;
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;
for(int i = 1; i <= t; ++i){
for(int j = st[i]; j <= ed[i]; ++j) sum[i] += a[j];
}
for(int i = 1; i <= n; ++i){
int op, l, r, c;
cin >> op >> l >> r >> c;
if(op == 0) update(l, r, c);
else cout << query(r, r) << endl;
}
return 0;
}
:::
P13977 数列分块入门 2
对于操作
对于操作
加起来
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 3e5 + 10;
int st[N], ed[N], pos[N], add[N], a[N];
vector<int> v[N];
void build(int p){
v[p].clear();
for(int i = st[p]; i <= ed[p]; ++i) v[p].push_back(a[i]);
sort(v[p].begin(), v[p].end());
}
void update(int L, int R, int d){
int p = pos[L], q = pos[R];
if(p == q){
for(int i = L; i <= R; ++i) a[i] += d;
build(p);
}else{
for(int i = p + 1; i <= q - 1; i++) add[i] += d;
for(int i = L; i <= ed[p]; i++) a[i] += d;
build(p);
for(int i = st[q]; i <= R; i++) a[i] += d;
build(q);
}
}
int query(int L, int R, int c){
c = c * c;
int p = pos[L], q = pos[R];
int ans = 0;
if(p == q){
for(int i = L; i <= R; ++i){
if(a[i] + add[p] < c) ans++;
}
}else{
for(int i = L; i <= ed[p]; ++i){
if(a[i] + add[p] < c) ans++;
}
for(int i = st[q]; i <= R; ++i){
if(a[i] + add[q] < c) ans++;
}
for(int i = p + 1; i <= q - 1; ++i) ans += lower_bound(v[i].begin(), v[i].end(), c - add[i]) - v[i].begin();
}
return ans;
}
signed main(){
int n;
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
int block = sqrt(n);
int t = n / block;
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[t] = n;
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;
for(int i = 1; i <= t; ++i) build(i);
for(int i = 1; i <= n; ++i){
int op, l, r, c;
cin >> op >> l >> r >> c;
if(op == 0) update(l, r, c);
else cout << query(l, r, c) << endl;
}
return 0;
}
:::
数列分块入门 5
看起来好像很难,主要是开方操作。
注意到
定义
然后我们维护
每个数最多开方
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 3e5 + 10;
int st[N], ed[N], pos[N], add[N], sum[N], a[N], mx[N];
void build(int p){
sum[p] = mx[p] = 0;
for(int i = st[p]; i <= ed[p]; ++i){
sum[p] += a[i];
mx[p] = max(mx[p], a[i]);
}
}
void update(int L, int R){
int p = pos[L], q = pos[R];
if(p == q){
for(int i = L; i <= R; ++i) a[i] = sqrt(a[i]);
build(p);
}else{
for(int i = L; i <= ed[p]; ++i) a[i] = sqrt(a[i]);
build(p);
for(int i = st[q]; i <= R; ++i) a[i] = sqrt(a[i]);
build(q);
for(int i = p + 1; i <= q - 1; ++i){
if(mx[i] <= 1) continue;
for(int j = st[i]; j <= ed[i]; ++j) a[j] = sqrt(a[j]);
build(i);
}
}
}
int query(int L, int R){
int p = pos[L], q = pos[R];
int ans = 0;
if(p == q){
for(int i = L; i <= R; ++i) ans += a[i];
ans += add[p] * (R - L + 1);
}else{
for(int i = p + 1; i <= q - 1; ++i){
ans += sum[i] + add[i] * (ed[i] - st[i] + 1);
}
for(int i = L; i <= ed[p]; ++i) ans += a[i];
ans += add[p] * (ed[p] - L + 1);
for(int i = st[q]; i <= R; ++i) ans += a[i];
ans += add[q] * (R - st[q] + 1);
}
return ans;
}
signed main(){
int n;
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
int block = sqrt(n);
int t = n / block;
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[t] = n;
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;
for(int i = 1; i <= t; ++i) build(i);
for(int i = 1; i <= n; ++i){
int op, l, r;
cin >> op >> l >> r ;
if(op == 0) update(l, r);
else cout << query(l, r) << endl;
}
return 0;
}
:::
P13981 数列分块入门 6
这个题需要的知识:重构块。
首先,对于插入操作,我们可以暴力操作。
先算这个数在哪个位置,然后使用 vector 暴力插入即可。
分析一下复杂度:
如果多个数频繁插入同一个块,该块的长度会不断增大,甚至可能超过
若不加控制,单次插入和查询的复杂度会退化为
其中
因此需要定期重构:当某个块的大小超过阈值(如
这样均摊下来,单次操作的时间复杂度仍为
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 3e5 + 10;
int a[N];
int n, block;
vector<int> v[N];
void build(){
vector<int> t;
for(int i = 1; v[i].size(); ++i){
for(int x : v[i]) t.push_back(x);
v[i].clear();
}
block = sqrt(t.size());
for(int i = 0; i < t.size(); ++i) v[i / block + 1].push_back(t[i]);
}
void update(int l, int r){
for(int i = 1;;++i){
if(l <= v[i].size()){
v[i].insert(v[i].begin() + l - 1, r);
if(v[i].size() > 5 * block) build();
return;
}
l -= v[i].size();
}
}
int query(int c){
for(int i = 1;;++i){
if(c <= v[i].size()) return v[i][c - 1];
c -= v[i].size();
}
}
signed main(){
int n;
cin >> n;
block = sqrt(n);
for(int i = 1; i <= n; ++i) cin >> a[i], v[(i - 1) / block + 1].push_back(a[i]);
for(int i = 1; i <= n; ++i){
int op, l, r, c;
cin >> op;
if(!op){
cin >> l >> r;
update(l, r);
}else{
cin >> c; cout << query(c) << endl;
}
}
return 0;
}
:::
P13982 数列分块入门 7
考虑维护两个懒标记。
我们定义:
则答案为
对于加和乘的懒标记,我们可以直接暴力维护。
显然时间复杂度为
有一个需要注意的点是:因为我们是先乘后加,所以我们在做乘法操作的时候需要把加法的懒标记也乘上修改的数字。
然后给出代码,不难。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5 + 10, MOD = 1e4 + 7;
int n, block, a[N], st[N], ed[N], pos[N], add[N], mul[N];
void build(int p){
for(int i = st[p]; i <= ed[p]; ++i) a[i] = (a[i] * mul[p] + add[p]) % MOD;
mul[p] = 1;
add[p] = 0;
}
void update_add(int l, int r, int c){
int p = pos[l], q = pos[r];
if(p == q){
build(p);
for(int i = l; i <= r; ++i) a[i] = (a[i] + c) % MOD;
}else{
build(p);
for(int i = l; i <= ed[p]; ++i) a[i] = (a[i] + c) % MOD;
build(q);
for(int i = st[q]; i <= r; ++i) a[i] = (a[i] + c) % MOD;
for(int i = p + 1; i <= q - 1; ++i) add[i] = (add[i] + c) % MOD;
}
}
void update_mul(int l, int r, int c){
int p = pos[l], q = pos[r];
if(p == q){
build(p);
for(int i = l; i <= r; ++i) a[i] = (a[i] * c) % MOD;
}else{
build(p);
for(int i = l; i <= ed[p]; ++i) a[i] = (a[i] * c) % MOD;
build(q);
for(int i = st[q]; i <= r; ++i) a[i] = (a[i] * c) % MOD;
for(int i = p + 1; i <= q - 1; ++i){
mul[i] = (mul[i] * c) % MOD;
add[i] = (add[i] * c) % MOD;
}
}
}
signed main(){
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
block = sqrt(n);
int t = n / block;
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
mul[i] = 1;
add[i] = 0;
}
ed[t] = n;
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;
for(int i = 1; i <= n; ++i){
int op, l, r, c;
cin >> op >> l >> r >> c;
if(op == 0) update_add(l, r, c);
else if(op == 1) update_mul(l, r, c);
else{
int p = pos[r];
cout << (a[r] * mul[p] + add[p]) % MOD << '\n';
}
}
return 0;
}
:::
P13983 数列分块入门 8
一道突破了定式了的题目。
对于查询,我们可以参考线段树的懒标记。
我们定义
那这有什么作用呢?
分
- 如果
\text{tag}[i] = c ,整块全是c ,答案加块长; - 如果
\text{tag}[i] = -1 ,暴力扫整块统计; - 如果
\text{tag}[i] 是其他值,跳过。
对于散块来说,这就是一个线段树的懒标记。
具体做法:先 push_down 下放整块标记,然后暴力判断每个元素。
对于修改:
散块:先 push_down,再暴力改每个元素为
整块:直接
时间复杂度:
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5 + 10;
int n, block, a[N], pos[N], st[N], ed[N], tag[N];
void push_down(int p){
if(tag[p] != -1){
for(int i = st[p]; i <= ed[p]; ++i) a[i] = tag[p];
tag[p] = -1;
}
}
void update(int l, int r, int c){
int p = pos[l], q = pos[r];
if(p == q){
push_down(p);
for(int i = l; i <= r; ++i) a[i] = c;
return;
}else{
push_down(p);
for(int i = l; i <= ed[p]; ++i) a[i] = c;
push_down(q);
for(int i = st[q]; i <= r; ++i) a[i] = c;
for(int i = p + 1; i <= q - 1; ++i) tag[i] = c;
}
}
int query(int l, int r, int c){
int p = pos[l], q = pos[r], ans = 0;
if(p == q){
push_down(p);
for(int i = l; i <= r; ++i) ans += (a[i] == c);
return ans;
}else{
push_down(p);
for(int i = l; i <= ed[p]; ++i) ans += (a[i] == c);
push_down(q);
for(int i = st[q]; i <= r; ++i) ans += (a[i] == c);
for(int i = p + 1; i <= q - 1; ++i){
if(tag[i] == c) ans += ed[i] - st[i] + 1;
else if(tag[i] == -1){
for(int j = st[i]; j <= ed[i]; ++j) ans += (a[j] == c);
}
}
}
return ans;
}
signed main(){
memset(tag, -1, sizeof(tag));
int n;
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
int block = sqrt(n);
int t = n / block;
if(n % block) t++;
for(int i = 1; i <= t; ++i){
st[i] = (i - 1) * block + 1;
ed[i] = i * block;
}
ed[t] = n;
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / block + 1;
while(n--){
int l, r, c;
cin >> l >> r >> c;
cout << query(l, r, c) << endl;
update(l, r, c);
}
return 0;
}
:::
最终挑战:P13984 数列分块入门 9
这个题目的思想还是很简单。
对于每一个整块,我们预处理一下。
对于整块部分,我们定义
暴力枚举所有块对,统计块间众数,复杂度
根据瞪眼法我们发现,答案除了整块的答案,其他的可能性就只剩下散块的答案了。
所以对于散块,我们直接跑暴力即可。
然而,我们如果直接跑散块的暴力,还是满足不了要求,因为跑散块暴力的时间复杂度为 vector 存一下每一个数的结果,然后二分查询。这样子时间复杂度降低成
总共
注意要离散化!
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6 + 10;
int st[N], ed[N], f[1010][1010], block, t, n, m, a[N], pos[N], cnt[N], lis[N];
vector<int> v[N];
void init(int x){
memset(cnt, 0, sizeof(cnt));
int maxx = 0, pos1 = N + 1;
for(int i = (x - 1) * block + 1; i <= n; ++i){
cnt[a[i]]++;
if(cnt[a[i]] > maxx || (cnt[a[i]] == maxx && a[i] < pos1)) maxx = cnt[a[i]], pos1 = a[i];
f[x][pos[i]] = pos1;
}
}
int get(int x, int y, int k){
return upper_bound(v[k].begin(), v[k].end(), y) - lower_bound(v[k].begin(), v[k].end(), x);
}
int query(int l, int r){
if(pos[l] == pos[r]){
int maxx = 0, pos1 = N + 1;
for(int i = l; i <= r; ++i){
int tmp = get(l, r, a[i]);
if(tmp > maxx || (tmp == maxx && a[i] < pos1)) maxx = tmp, pos1 = a[i];
}
return pos1;
}else{
int maxx, pos1;
if(pos[l] + 1 <= pos[r] - 1){
maxx = get(l, r, f[pos[l] + 1][pos[r] - 1]);
pos1 = f[pos[l] + 1][pos[r] - 1];
}else{
maxx = 0;
pos1 = N + 1;
}
for(int i = l ;i <= ed[pos[l]]; ++i){
int tmp = get(l, r, a[i]);
if(tmp > maxx || tmp == maxx && a[i] < pos1) maxx = tmp, pos1 = a[i];
}
for(int i = st[pos[r]]; i <= r; ++i){
int tmp = get(l, r, a[i]);
if(tmp > maxx || tmp == maxx && a[i] < pos1) maxx = tmp, pos1 = a[i];
}
return pos1;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
block = sqrt(n);
t = n / block;
if(n % block) t++;
for(int i = 1; i <= n; ++i){
cin >> a[i];
lis[i] = a[i];
pos[i] = (i - 1) / block + 1;
}
sort(lis + 1, lis + n + 1);
int tot = unique(lis + 1, lis + n + 1) - lis - 1;
for(int i = 1; i <= n; ++i){
a[i] = lower_bound(lis + 1, lis + 1 + tot, a[i]) - lis;
v[a[i]].push_back(i);
}
for(int i = 1; i <= tot; ++i) v[i].push_back(n + 1);
for(int i = 1; i <= t; ++i) st[i] = (i - 1) * block + 1, ed[i] = i * block;
ed[t] = n;
for(int i = 1; i <= t; ++i) init(i);
int x = 0;
for(int i = 1; i <= n; ++i){
int l, r;
cin >> l >> r;
int ans = query(l, r);
cout << lis[ans] << '\n';
}
return 0;
}
:::
练习题目:P13978,P13979,P4168,P2801,P3870,P3203。
AI 使用说明:本文使用了 deepseek 修缮格式。