分块思想
引用一下 oi-wiki 的话:
分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
数列/序列分块
引入
#6280. 数列分块入门 4
给定一长度为$ n$ 的数列,有 \(n\) 次操作。
操作分为两种:区间加,查询区间和。
\(1\le n\le5×10^4\)。
划分
首先将序列按每 \(T\) 个元素一块进行分块,并记录每块的区间和。
\(T\) 的大小需要根据实际要求选择,下文复杂度分析部分会详细解析。
划分过程的常用模板:
len = T; //根据实际要求选择一个合适的大小。
cnt = (n - 1) / len + 1;
for(int i = 1; i <= cnt; ++ i) { //分配块左右边界。
L[i] = R[i - 1] + 1;
R[i] = min(n, L[i] + len - 1);
}
//分配元素所属的块编号。
for(int i = 1; i <= n; ++i) pos[i] = (i - 1) / len + 1;
查询
设查询区间 \([l,r]\) 的区间和。
-
若 l,r 在同一块内,直接暴力求和即可。块长为 \(T\),单次查询复杂度上界 \(O(T)\)。
-
否则答案由三部分构成:
- 以 \(l\) 开头的不完整块的和。
- 以 \(r\) 结尾的不完整块的和。
- 中间的完整块的和。
对于 \(1、2\) 两部分,可暴力求和,复杂度上界 \(O(T)\)
。
对于 \(3\) ,直接查询预处理的完整块和即可,复杂度上界 \(O(nT)\)(查询所有的完整块)。
单次查询总复杂度上界 \(O(\dfrac{n}{T}+T)\)。
修改
修改操作的过程与查询操作相同,设修改区间为 \([l,r]\)。
-
若 \(l,r\) 在同一块内,直接暴力修改,并更新区间和。块长为 \(T\),单次修改复杂度上界 \(O(T)\)。
-
否则将修改区间划分成三部分:
- 以 \(l\) 开头的不完整块。
- 以 \(r\) 结尾的不完整块。
- 中间的完整块。
\(1、2\) 直接暴力修改序列,\(3\) 给完整的块打上区间加的标记。
单次修改复杂度上界 \(O(\dfrac{n}{T}+T)\)。
复杂度分析
总复杂度为 \(O(n\times(nT+T))\)。
由均值不等式,\(\dfrac{n}{T}+T\le 2\sqrt{\dfrac{n}{T}\times T}\),当且仅当 \(\dfrac{n}{T}=T\) 时等号成立,复杂度最低。
此时块大小为 \(\sqrt{n}\),总复杂度 \(O(n\sqrt{n})\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del[N], sum[N];
void add(int l, int r, int c){
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) a[i] += c, sum[pos[i]] += c;
return ;
}
for(int i = l; i <= R[pos[l]]; ++i) a[i] += c, sum[pos[i]] += c;
for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c;
for(int i = L[pos[r]]; i <= r; ++i) a[i] += c, sum[pos[i]] += c;
}
int ask(int l, int r, int c){
int res = 0;
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) (res += a[i] + del[pos[i]]) %= c;
return res;
}
for(int i = l; i <= R[pos[l]]; ++i) (res += a[i] + del[pos[i]]) %= c;
for(int i = pos[l] + 1; i < pos[r]; ++i) (res += sum[i] + del[i] * (R[i] - L[i] + 1)) %= c;
for(int i = L[pos[r]]; i <= r; ++i) (res += a[i] + del[pos[i]]) %= c;
return res;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1, sum[pos[i]] += a[i];
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) add(l, r, c);
else printf("%lld\n", ask(l, r, c + 1));
}
return 0;
}
练习
#6277. 数列分块入门 1
区间加,单点查询
直接暴力分块
完整块 修改永久懒标记
两端不完整块暴力修改元素值
单点查询值 = 元素值 + 懒标记
完整块数量不超过 \(\sqrt{n}\)
, 两不完整块总长度不超过 \(2\sqrt{n}\)
总复杂度 \(O(n\sqrt{n})\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del[N];
void add(int l, int r, int c){
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) a[i] += c;
return ;
}
for(int i = l; i <= R[pos[l]]; ++i) a[i] += c;
for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c;
for(int i = L[pos[r]]; i <= r; ++i) a[i] += c;
}
int ask(int l){
return a[l] + del[pos[l]];
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) add(l, r, c);
else printf("%lld\n", ask(r));
}
return 0;
}
#6278. 数列分块入门 2
区间加,区间查询小于给定值 元素数
同 \(1\), 先进行分块, 再分别考虑完整块与不完整块 :
- 先不考虑 修改操作 :
不完整块 可直接暴力查询
对于完整块, 查询小于给定值元素数, 易联想到 lower_bound 操作
由于分块后单个块较小, 则可直接暴力排序, 再通过 lower_bound 求得元素数 - 再考虑 修改操作
同样, 不完整块 可直接进行暴力修改
对于完整块, 区间加后, 不影响它们之间的排名, 排序后 顺序不变
进行区间加 \(x\) 后 再进行区间查询 \(y\) , 与 在区间加 \(x\) 之前, 区间查询 \(y - x\) , 得到的结果相同
则可使用类似 \(1\) 的懒惰标记法 进行区间修改, 并根据懒标记调整查询的值
由上, 则需记录 未排序的数列 与 排序后的数列(需要进行 lower_bound)
可使用 vector 类, 来储存每一排序后的完整块
-
修改时 :
不完整块 暴力修改未排序数列, 将 原排序后的数列 重新赋值并排序 (为了便于 查询)
其总长度不超过 \(2\sqrt{n}\), 单次排序复杂度 \(O(\sqrt{n}\log\sqrt{ n})\)
完整块直接更新 懒标记即可, 其个数 不超过 \(\sqrt{n}\)
则单次修改总复杂度为 \(O(\sqrt{n}+\sqrt{n}\log\sqrt{ n})\) -
查询时 :
不完整块 暴力查询未排序数列, 其总长度不超过 \(2\sqrt{n}\)
完整块 对排序后数列进行 lower_bound, 其个数 不超过 \(\sqrt{n}\), 单次 lower_bound 复杂度为 \(O(\sqrt{n}\log\sqrt{n})\)
则单次修改总复杂度也为 \(O(\sqrt{n}+\sqrt{n}\log\sqrt{ n})\)
在预处理分块时 需要对整个数列进行排序预处理, 复杂度 \(O(n\log n)\)
则上述算法 总复杂度为 \(O(n\log n+\sqrt{n}\log\sqrt{ n})\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del[N];
vector<int> E[505];
void reset(int x){
E[x].clear();
for(int i = L[x]; i <= R[x]; ++i) E[x].push_back(a[i]);
sort(E[x].begin(), E[x].end());
}
void change(int l, int r, int c){
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) a[i] += c;
reset(pos[l]);
return ;
}
for(int i = l; i <= R[pos[l]]; ++i) a[i] += c; reset(pos[l]);
for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c;
for(int i = L[pos[r]]; i <= r; ++i) a[i] += c; reset(pos[r]);
}
int ask(int l, int r, int c){
int res = 0;
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) res += (a[i] + del[pos[i]] < c);
return res;
}
for(int i = l; i <= R[pos[l]]; ++i) res += (a[i] + del[pos[i]] < c);
for(int i = pos[l] + 1; i < pos[r]; ++i) res += lower_bound(E[i].begin(), E[i].end(), c - del[i]) - E[i].begin();
for(int i = L[pos[r]]; i <= r; ++i) res += (a[i] + del[pos[i]] < c);
return res;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= n; ++i) E[pos[i]].push_back(a[i]);
for(int i = 1; i <= cnt; ++i) sort(E[i].begin(), E[i].end());
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) change(l, r, c);
else printf("%lld\n", ask(l, r, c * c));
}
return 0;
}
#6279. 数列分块入门 3
区间加,区间查询小于给定值 最大元素
-
算法1 :
套用 数列分块2 的做法, 在进行lower_bound时 求得每个块中 小于给定值 最大元素,
再将多个块的答案合并 取最大值.
总复杂度 \(O(n\log n + n\sqrt{n}\log\sqrt{n})\) -
算法2 :
查询前驱 ? 来发Splay
可以对每个块 都维护一个 set , 来代替算法 1 中的排序数组.
重赋值和插入操作 都会变得更方便 .
#include<bits/stdc++.h>
#define int long long
#define iter multiset<int>::iterator
using namespace std;
const int N = 1e5 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del[N];
multiset<int> E[1010];
void change(int l, int r, int c){
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) E[pos[l]].erase(a[i]), a[i] += c, E[pos[l]].insert(a[i]);
return ;
}
for(int i = l; i <= R[pos[l]]; ++i) E[pos[l]].erase(a[i]), a[i] += c, E[pos[l]].insert(a[i]);
for(int i = pos[l] + 1; i < pos[r]; ++i) del[i] += c;
for(int i = L[pos[r]]; i <= r; ++i) E[pos[r]].erase(a[i]), a[i] += c, E[pos[r]].insert(a[i]);
}
int ask(int l, int r, int c){
int res = -1;
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i)
if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]);
return res;
}
for(int i = l; i <= R[pos[l]]; ++i)
if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]);
for(int i = pos[l] + 1; i < pos[r]; ++i){
iter it = E[i].lower_bound(c - del[i]);
if(it == E[i].begin()) continue;
res = max(res, *(--it) + del[i]);
}
for(int i = L[pos[r]]; i <= r; ++i)
if(a[i] + del[pos[i]] < c) res = max(res, a[i] + del[pos[i]]);
return res;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= n; ++i) E[pos[i]].insert(a[i]);
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) change(l, r, c);
else printf("%lld\n", ask(l, r, c));
}
return 0;
}
#6280. 数列分块入门 4
区间加, 区间求和
上面有了。
#6281. 数列分块入门 5
区间开方,区间求和
对于求和操作 :
同 T4 , 维护完整块元素和.
不完整块暴力查询, 完整块查询 整块元素和.
显然, 单次求和复杂度为 \(O(\sqrt{n})\)
- 对于修改操作 :
不完整块 直接暴力开方 .
对于完整块 :
由于开方运算不满足 一系列运算律, 无法使用标记法.
怎么办? 考虑直接暴力.
众所周知,
\(\lfloor{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{2^{31}}}}}}=1\rfloor}\) .
对于一个数, 最多 5 次修改后 必然等于 1 或 0 .
对于一个块, 最多 5 次修改后 必然全部由 1 或 0 组成 .
而 \(\sqrt{1}=1\), \(\sqrt{0}=0\) , 修改操作对其不会有影响 .
则可以标记一个完整块是否全为 1/0.
若全为 1/0, 则无论其修改次数, 元素和都不会改变, 不进行修改操作.
否则 直接暴力修改, 并在暴力修改时 完成对标记的更新.
由于每个完整块的修改次数 \(≤5\) , 而不完整块的大小 \(≤\sqrt{n}\).
则所有修改操作 总复杂度最大为 \(O(5n+\sqrt{n})\).
#include<bits/stdc++.h>
#define int long long
#define iter multiset<int>::iterator
using namespace std;
const int N = 1e5 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del[N], sum[N];
bool flag[N];
void solve(int x){
if(flag[x]) return ;
sum[x] = 0, flag[x] = 1;
for(int i = L[x]; i <= R[x]; ++i){
a[i] = sqrt(a[i]), sum[x] += a[i];
if(a[i] > 1) flag[x] = 0;
}
}
void change(int l, int r, int c){
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i];
return ;
}
for(int i = l; i <= R[pos[l]]; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i];
for(int i = pos[l] + 1; i < pos[r]; ++i) solve(i);
for(int i = L[pos[r]]; i <= r; ++i) sum[pos[i]] -= a[i], a[i] = sqrt(a[i]), sum[pos[i]] += a[i];
}
int ask(int l, int r){
int res = 0;
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) res += a[i];
return res;
}
for(int i = l; i <= R[pos[l]]; ++i) res += a[i];
for(int i = pos[l] + 1; i < pos[r]; ++i) res += sum[i];
for(int i = L[pos[r]]; i <= r; ++i) res += a[i];
return res;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1, sum[pos[i]] += a[i];
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) change(l, r, c);
else printf("%lld\n", ask(l, r));
}
return 0;
}
#6282. 数列分块入门 6
单点插入,单点查询
众所周知 , vector 的 insert 操作, 复杂度与 vector 内元素个数 呈正比.
则可使用分块 来构建多个vector , 以降低插入复杂度 :
插入时 枚举所有块, 将元素插入指定位置所在块中, 单次复杂度近似 \(O(\sqrt{n})\).
查询时 枚举所有块, 在指定位置所在块中查询 , 单次复杂度近似 \(O(\sqrt{n})\).
总复杂度近似 \(O(n\sqrt{n})\).
上述复杂度分析 都是基于 随机数据 的前提下的.
如果出题人恶意构造类似下方的数据 :
100000
1 1 1 1
1 1 1 1
1 1 1 1
...
上述做法会出现 不断插入同一块的情况 ,
单次插入会被卡到 \(O(n)\) , 总复杂度被卡到 \(O(n^2)\).
考虑 如何才能使各块的大小相对平均, 避免产生上述问题.
直接再分一遍块不就行了 !
为块的大小设定上限 , 在插入同时判断块的大小是否达到上限.
若达到上限, 枚举所有元素, 重新进行分块, 以平均块的大小.
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], pos[N];
vector<int> E[N], tmp;
void rebuild(){
tmp.clear();
for(int i = 1; i <= cnt; ++i){
int ll = E[i].size();
for(int j = 0; j < ll; ++j) tmp.push_back(E[i][j]);
E[i].clear();
}
len = sqrt(tmp.size()), cnt = (tmp.size() - 1) / len + 1;
for(int i = 0; i < tmp.size(); ++i) E[i / len + 1].push_back(tmp[i]);
return ;
}
void insert(int x, int val){
int pos1 = 1, pos2 = x;
while(pos2 > E[pos1].size()) pos2 -= E[pos1].size(), ++pos1;
E[pos1].insert(E[pos1].begin() + pos2 - 1, val);
if(E[pos1].size() > 10 * len) rebuild();
return ;
}
int ask(int x){
int pos1 = 1, pos2 = x;
while(pos2 > E[pos1].size()) pos2 -= E[pos1].size(), ++pos1;
return E[pos1][pos2 - 1];
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= n; ++i) E[pos[i]].push_back(a[i]);
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) insert(l, r);
else printf("%lld\n", ask(r));
}
return 0;
}
#6283. 数列分块入门 7
区间加,区间乘,单点查询
如果只有区间乘操作, 只需要将 T1 的 + 改为 * 即可.
但是 区间加区间乘 同时存在, 需记录 两懒标记, 就需要注意懒标记与 数列值之间的关系
(以下, 使用 \(k\) 代表乘法懒标记, \(b\) 代表加法标记, \(x\) 代表数列值).
对于完整块的两修改操作 :
-
当出现 先乘后加 时, 有 : \((kx+b)+y=kx+b+y\).
若只修改 数列值 x , 则会出现: \([k(x+y)+b]=kx+ky+b≠kx+b+y\). -
当出现 先加后乘 时, 有 : \((kx+b)×y=kxy+by\).
若只修改 数列值, 则会出现: $kxy+b≠kxy+by $.
若只修改 乘法懒标记, 则会出现: \(kyx+b≠kxy+by\) .
进行修改操作时 要进行多方面的考虑, 需要进行分类讨论
吗, 当然是不需要的.
不完整块较小 , 可以直接暴力处理 .
先通过修改前的懒标记 求得整个块各元素值 , 并清空懒标记,
再暴力修改数列值 , 即可避免上述情况发生.
对于完整块的两修改操作 :
- 区间加时, 直接修改加法懒标记即可.
- 区间乘时, 根据 乘法结合律有 :\((kx+b)×y=kxy+by\).
则将 加法标记 与 乘法标记同乘即可.
单点查询, 即输出 \(kx+b\), 复杂度 \(O(1)\).
完整块数量不超过 \(\sqrt{n}\), 两不完整块总长度不超过 \(2\sqrt{n}\)
综上, 总复杂度为 \(O(n\sqrt{n})\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5, mod = 1e4 + 7;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], del1[N], del2[N];
void reset(int x){
for(int i = L[x]; i <= R[x]; ++i) a[i] = (a[i] * del2[x] + del1[x]) % mod;
del2[x] = 1, del1[x] = 0;
}
void add1(int l, int r, int c){
if(pos[l] == pos[r]){
reset(pos[l]); for(int i = l; i <= r; ++i) a[i] = (a[i] + c) % mod;
return ;
}
reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) a[i] = (a[i] + c) % mod;
for(int i = pos[l] + 1; i < pos[r]; ++i) del1[i] = (del1[i] + c) % mod;
reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) a[i] = (a[i] + c) % mod;
}
void add2(int l, int r, int c){
if(pos[l] == pos[r]){
reset(pos[l]); for(int i = l; i <= r; ++i) a[i] = a[i] * c % mod;
return ;
}
reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) a[i] = a[i] * c % mod;
for(int i = pos[l] + 1; i < pos[r]; ++i) del1[i] = del1[i] * c % mod, del2[i] = del2[i] * c % mod;
reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) a[i] = a[i] * c % mod;
}
int ask(int x){
return (a[x] * del2[pos[x]] % mod + del1[pos[x]]) % mod;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1), del2[i] = 1;
while(n--){
int opt = read(), l = read(), r = read(), c = read();
if(!opt) add1(l, r, c);
else if(opt == 1) add2(l, r, c);
else if(opt == 2) printf("%lld\n", ask(r));
}
return 0;
}
#6283. 数列分块入门 8
区间赋值,查询区间等于给定值元素数
同T6 , 还是先想暴力.
对于修改操作, 不完整块 直接暴力修改 , 完整块打懒标记.
单次修改操作 , 复杂度显然为 \(O(\sqrt{n})\).
对于查询操作, 不完整块在暴力修改时 顺便暴力查询, 完整块分下列两种情况讨论 :
1. 有懒标记 , 则块内元素相同 , 直接判断标记与查询值 是否相同.
2. 无懒标记 , 没有什么好的处理方法 , 直接暴力查询.
若使一次查询, 其完整块查询操作 复杂度变为 \(O(n)\) , 需要使区间内完整块全部无懒标记.
而一次修改操作 , 只能使修改区间两端的 2 不完整块 懒标记清空,
若使一区间完整块懒标记全部清空, 需要先经过 \(\sqrt{n}\) 次修改操作
单次查询操作, 处理不完整块复杂度为 \(O(\sqrt{n})\) ,
而对于完整块的处理, 在无懒标记的情况下 可能会被卡到 \(O(n)\).
最多只会有 \(\sqrt{n}\) 次的修改操作
故上述算法 总复杂度 近似 \(O(n \sqrt{n})\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt;
int a[N], L[N], R[N], pos[N], same[N];
void reset(int x){
if(same[x] == -1) return ;
for(int i = L[x]; i <= R[x]; ++i) a[i] = same[x];
same[x] = -1;
}
int solve(int l, int r, int c){
int res = 0;
if(pos[l] == pos[r]){
reset(pos[l]); for(int i = l; i <= r; ++i) res += (a[i] == c), a[i] = c;
return res;
}
reset(pos[l]); for(int i = l; i <= R[pos[l]]; ++i) res += (a[i] == c), a[i] = c;
for(int i = pos[l] + 1; i < pos[r]; ++i){
if(same[i] != -1) res += (R[i] - L[i] + 1) * (same[i] == c);
else for(int j = L[i]; j <= R[i]; ++j) res += (a[j] == c);
same[i] = c;
}
reset(pos[r]); for(int i = L[pos[r]]; i <= r; ++i) res += (a[i] == c), a[i] = c;
return res;
}
signed main(){
n = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1), same[i] = -1;
while(n--){
int l = read(), r = read(), c = read();
printf("%lld\n", solve(l, r, c));
}
return 0;
}
#6283. 数列分块入门 9
查询区间最小众数
预处理出两个数组:
\(p[i][j]\):表示第 \(i\) 个块 到第 \(j\) 个块的(最小的)众数。
\(s[i][j]\):类似于前缀和,在前 \(i\) 个(包括 \(i\) )个块中 \(j\) (离散化之后的值)出现了几次。
如何预处理 \(p,s\)
对于 \(s\) ,直接每个块扫一遍,复杂度 \(O(n \sqrt n)\)
对于 \(p\) ,双重循环枚举 \(i,j\),开一个数组暴力统计每个数出现了多少次。复杂度 \(O(\sqrt n \sqrt n \sqrt n)=O(n \sqrt n)\)
如果 \(pos[r]-pos[l]\le2\) 直接暴力即可
如果 \(pos[r]-pos[l]>2\) ,
对于不完整块,直接暴力计算
对于完整块,就是之前预处理的 \(p[pos[l]+1][pos[r]-1]\)
众数数量就是 \(max(\)在不完整块内出现数字中的在\([l,r]\)中出现次数最大值,完整块内的出现次数最多的数字在\([l,r]\)中的出现次数\()\),取最小的那个数就好了,由于这道题之前就打过了,码风有点不大一样,应该也是看得懂的。
#include<bits/stdc++.h>
#define N 100005
using namespace std;
void read(int &o){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
o=x*f;
}
int n,m,len,cnt;
int a[N],b[N];
int l[N],r[N];
int sum[N];
int id[405][405],p[405][405],s[405][N]; //p[i][j] 表示块i 到 块j 的最小众数个数 s[i][j] 前i块 j 出现次数
int Get(int x){
int k=x/len;
k+=(x%len?1:0);
return k;
}
int main(){
read(n);
len=sqrt(n);
cnt=(n-1)/len+1;
for(int i=1;i<=n;i++) read(a[i]),b[i]=a[i];
sort(b+1,b+1+n);
int tot=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=cnt;i++) l[i]=r[i-1]+1,r[i]=(i==cnt?n:l[i]+len-1);
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
for(int i=1;i<=cnt;i++){
int B[N]={0},ID=0,P=0;
for(int j=i;j<=cnt;j++){
for(int k=l[j];k<=r[j];k++){
B[a[k]]++;
if(B[a[k]]>P){
P=B[a[k]];
ID=a[k];
}else if(B[a[k]]==P) ID=min(a[k],ID);
}
id[i][j]=ID,p[i][j]=P;
}
}
for(int i=1;i<=cnt;i++){
for(int j=1;j<=n;j++) s[i][a[j]]=s[i-1][a[j]];
for(int j=l[i];j<=r[i];j++) s[i][a[j]]++;
}
int x=0;
while(n--){
int ll,rr,ans=0;
read(ll),read(rr);
memset(sum,0,sizeof(sum));
int ql=Get(ll),qr=Get(rr);
if(qr-ql<=2){
for(int i=ll;i<=rr;i++){
sum[a[i]]++;
if(sum[ans]<sum[a[i]]) ans=a[i];
else if(sum[a[i]]==sum[ans]) ans=min(ans,a[i]);
}
printf("%d\n",x=b[ans]);
}else{
ans=id[ql+1][qr-1];
for(int i=ll;i<=r[ql];i++) sum[a[i]]++;
for(int i=l[qr];i<=rr;i++) sum[a[i]]++;
int maxnum=0,maxn=0;
for(int i=ll;i<=r[ql];i++){
int val=s[qr-1][a[i]]-s[ql][a[i]]+sum[a[i]];
if(val>maxn){
maxn=val;
maxnum=a[i];
}else if(val==maxn) maxnum=min(maxnum,a[i]);
}
for(int i=l[qr];i<=rr;i++){
int val=s[qr-1][a[i]]-s[ql][a[i]]+sum[a[i]];
if(val>maxn){
maxn=val;
maxnum=a[i];
}else if(maxn==val) maxnum=min(maxnum,a[i]);
}
int ss=sum[ans]+p[ql+1][qr-1];
if(maxn>ss) ans=maxnum;
else if(maxn==ss) ans=min(ans,maxnum);
printf("%d\n",x=b[ans]);
}
}
return 0;
}
P4135 作诗
询问区间出现次数为正偶数的数的个数。
其实与区间众数那道题类似,实现预处理出块与块之间的答案,再根据散块内的值分类讨论进行修改。
记得位运算时要加括号来保证优先级,调了好久。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 51;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, len, cnt, q, ans, c, top;
int a[N], L[N], R[N], pos[N], num[351][N], f[351][351];
//num[i][j] 表示第 i 块到 n 中,j 的出现次数 f[i][j] 表示第 i 块到第 j 块的答案
int stk[N], cc[N];
signed main(){
n = read(), c = read(), q = read(); len = sqrt(n), cnt = (n - 1) / len + 1;
for(int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for(int i = 1; i <= cnt; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
for(int i = 1; i <= cnt; ++i){
int t = 0;
for(int j = L[i]; j <= n; ++j){
++num[i][a[j]];
if(num[i][a[j]] > 1 && (num[i][a[j]] & 1)) --t;
else if((num[i][a[j]] & 1) == 0) ++t;
f[i][pos[j]] = t;
}
}
while(q--){
int l = (read() + ans) % n + 1, r = (read() + ans) % n + 1;
if(l > r) swap(l, r); ans = 0;
if(pos[l] == pos[r]){
for(int i = l; i <= r; ++i) ++cc[a[i]], stk[++top] = a[i];
while(top){
if(cc[stk[top]]) ans += (cc[stk[top]] & 1) ^ 1;
cc[stk[top--]] = 0;
}
printf("%d\n", ans); continue;
}
if(pos[l] + 1 <= pos[r] - 1) ans = f[pos[l] + 1][pos[r] - 1];
for(int i = l; i <= R[pos[l]]; ++i) ++cc[a[i]], stk[++top] = a[i];
for(int i = L[pos[r]]; i <= r; ++i) ++cc[a[i]], stk[++top] = a[i];
while(top){
int t = stk[top];
if(cc[t] != 0){
if(num[pos[l] + 1][t] - num[pos[r]][t] > 0 && ((num[pos[l] + 1][t] - num[pos[r]][t]) & 1) == 0 && (cc[t] & 1)) ans--;
else if(num[pos[l] + 1][t] - num[pos[r]][t] > 0 && ((num[pos[l] + 1][t] - num[pos[r]][t]) & 1) && (cc[t] & 1)) ans++;
else if(num[pos[l] + 1][t] - num[pos[r]][t] == 0 && ((cc[t] & 1) == 0)) ans++;
}
cc[stk[top--]] = 0;
}
printf("%d\n", ans);
}
return 0;
}
值域分块
顾名思义,就是将值域分块,统计 值域为\([l,r]\)的答案,一般与莫队一起使用。
P4396 [AHOI2013]作业
求区间 \([l,r]\) 值在值域 \([a,b]\) 的数的个数和数的种类。
区间询问显然可以用莫队来实现,对于值域 \([a,b]\) 的查询可以用值域分块来实现。(不知道为什么好像分块的时候根据序列分块要比根据值域分块要快)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 51;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-')
f = -f;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int n, m, len, cnt;
int a[N], pos[N], L[N], R[N], ans[N], res[N];
// ans[i] 是不同数的种类
// res[i] 是数的数量
int sumr[N], sum[N], suma[N];
// sum[i] 表示 i 这个值的数量
// sumr[i] 表示 pos[i] 这个块的 res
// suma[i] 表示 pos[i] 这个块的 ans
struct Query {
int id, l, r, a, b;
bool operator<(const Query &A) const { return pos[l] ^ pos[A.l] ? pos[l] < pos[A.l] : r < A.r; }
} q[N];
inline void add(int x) {
++sum[a[x]], ++sumr[pos[a[x]]];
if (sum[a[x]] <= 1)
++suma[pos[a[x]]];
}
inline void del(int x) {
--sum[a[x]], --sumr[pos[a[x]]];
if (sum[a[x]] <= 0)
--suma[pos[a[x]]];
}
inline int get_ans(int l, int r) {
int ss = 0;
if (l > r)
return 0;
if (pos[l] == pos[r]) {
for (int i = l; i <= r; ++i) ss += (sum[i] >= 1);
return ss;
}
for (int i = l; i <= R[pos[l]]; ++i) ss += (sum[i] >= 1);
for (int i = pos[l] + 1; i <= pos[r] - 1; ++i) ss += suma[i];
for (int i = L[pos[r]]; i <= r; ++i) ss += (sum[i] >= 1);
return ss;
}
inline int get_res(int l, int r) {
int ss = 0;
if (l > r)
return 0;
if (pos[l] == pos[r]) {
for (int i = l; i <= r; ++i) ss += sum[i];
return ss;
}
for (int i = l; i <= R[pos[l]]; ++i) ss += sum[i];
for (int i = pos[l] + 1; i <= pos[r] - 1; ++i) ss += sumr[i];
for (int i = L[pos[r]]; i <= r; ++i) ss += sum[i];
return ss;
}
int main() {
n = read(), m = read();
len = sqrt(n);
for (int i = 1; i <= n; ++i) a[i] = read(), pos[i] = (i - 1) / len + 1;
for (int i = 1; i <= m; ++i) q[i] = (Query){ i, read(), read(), read(), read() };
sort(q + 1, q + 1 + m);
for (int i = 1; i <= n; ++i) L[i] = R[i - 1] + 1, R[i] = min(n, L[i] + len - 1);
int l = 1, r = 0;
for (int i = 1; i <= m; ++i) {
while (r < q[i].r) add(++r);
while (l > q[i].l) add(--l);
while (l < q[i].l) del(l++);
while (r > q[i].r) del(r--);
res[q[i].id] = get_res(q[i].a, q[i].b);
ans[q[i].id] = get_ans(q[i].a, q[i].b);
}
for (int i = 1; i <= m; ++i) printf("%d %d\n", res[i], ans[i]);
return 0;
}
参考资料: 「笔记」分块
标签:ch,分块,int,复杂度,sqrt,学习,完整,笔记 From: https://www.cnblogs.com/jiangchen4122/p/17417627.html