同步我的博客:https://me.mycbxzd.top/archives/32
ChatGPT 也可能会犯错。请核查重要信息。
一、普通莫队算法
1. 概述
普通莫队算法是处理一维区间查询问题的基础版本。主要目标是通过排序和分块,将暴力计算中每次区间操作的重复部分重用,从而优化查询效率。
2. 适用场景
普通莫队适用于区间不变、仅需查询统计信息的问题,比如:
- 区间和
- 区间最大值/最小值
- 区间中不同数字的个数
- 判断区间是否满足某些条件
3. 算法步骤
- 分块:将数组按块分为大小接近
sqrt(n)
的区间。 - 排序查询:按查询的左端点所在块排序,若块号相同按右端点升序排序。
- 逐个处理查询:
- 维护当前区间
[L, R]
。 - 通过增减左右端点来更新当前区间的统计值。
- 保存查询结果。
- 维护当前区间
4. 代码实现
以下是计算区间 [L, R]
元素和的例子:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000; // 数组最大长度
const int BLOCK=317; // 分块大小,约为sqrt(MAXN)
int arr[MAXN]; // 输入数组
int result[MAXN]; // 保存每个查询的结果
int currentSum=0; // 当前区间的和
struct Query {
int L, R, idx;
} queries[MAXN];
bool compare(Query a, Query b) {
if(a.L/BLOCK != b.L/BLOCK) return a.L/BLOCK < b.L/BLOCK;
return a.R < b.R;
}
void add(int idx) {
currentSum += arr[idx];
}
void remove(int idx) {
currentSum -= arr[idx];
}
int main() {
int n, q;
cin >> n >> q;
for(int i=0; i<n; i++) cin >> arr[i]; // 输入数组
for(int i=0; i<q; i++) {
cin >> queries[i].L >> queries[i].R;
queries[i].L--, queries[i].R--; // 转为0基索引
queries[i].idx = i; // 记录查询编号
}
sort(queries, queries+q, compare); // 按莫队规则排序
int L=0, R=-1; // 当前区间初始化为空
for(int i=0; i<q; i++) {
while(R < queries[i].R) add(++R);
while(R > queries[i].R) remove(R--);
while(L < queries[i].L) remove(L++);
while(L > queries[i].L) add(--L);
result[queries[i].idx] = currentSum; // 保存结果
}
for(int i=0; i<q; i++) cout << result[i] << endl; // 按查询顺序输出结果
return 0;
}
5. 代码解析
- 分块和排序:
- 按块号排序,块内按右端点升序排序,确保查询顺序最优。
- 维护区间统计:
add(idx)
:将元素arr[idx]
加入当前区间。remove(idx)
:将元素arr[idx]
移出当前区间。
- 调整区间:
- 当前区间
[L, R]
不断向查询目标[L', R']
移动,通过while
循环动态维护统计信息。
- 当前区间
6. 时间复杂度
- 单次查询调整左右端点至多涉及
O(sqrt(n))
次操作。 - 查询总时间复杂度为
O((n+q) * sqrt(n))
。
7. 例题
- 求区间和
- 求区间中不同数字的个数(通过统计频率数组实现)
- 判断区间是否为某种性质
二、带修改莫队算法
1. 概述
普通莫队算法不支持对数组的动态修改。带修改的莫队算法引入修改操作,解决同时存在查询和更新的情况。
2. 适用场景
需要同时支持区间查询和单点更新的问题,比如:
- 区间和,允许修改数组某个元素的值。
- 区间统计,允许改变数组某些数据。
3. 算法思想
- 扩展查询操作:
- 查询分为两部分:
- 区间
[L, R]
的操作(类似普通莫队)。 - 处理修改带来的影响。
- 区间
- 查询分为两部分:
- 离线处理:
- 将查询和更新操作统一存储,通过操作序号区分。
- 按照莫队规则排序查询时,动态应用之前的更新。
4. 代码实现
以下是同时支持区间和查询及单点更新的实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
const int BLOCK=317;
int arr[MAXN]; // 原始数组
int temp[MAXN]; // 当前数组状态
int result[MAXN]; // 查询结果
int currentSum=0; // 当前区间的和
struct Query {
int type, L, R, idx, x; // type=1为查询,type=2为修改
} queries[MAXN];
bool compare(Query a, Query b) {
if(a.L/BLOCK != b.L/BLOCK) return a.L/BLOCK < b.L/BLOCK;
return a.R < b.R;
}
void add(int idx) {
currentSum += temp[idx];
}
void remove(int idx) {
currentSum -= temp[idx];
}
int main() {
int n, q;
cin >> n >> q;
for(int i=0; i<n; i++) {
cin >> arr[i];
temp[i] = arr[i];
}
int queryCount=0;
for(int i=0; i<q; i++) {
int type;
cin >> type;
if(type == 1) { // 查询
int l, r;
cin >> l >> r;
queries[queryCount++] = {1, l-1, r-1, i, -1};
} else { // 修改
int idx, x;
cin >> idx >> x;
queries[queryCount++] = {2, idx-1, -1, i, x};
}
}
sort(queries, queries+queryCount, compare);
int L=0, R=-1;
for(int i=0; i<queryCount; i++) {
if(queries[i].type == 1) { // 查询
while(R < queries[i].R) add(++R);
while(R > queries[i].R) remove(R--);
while(L < queries[i].L) remove(L++);
while(L > queries[i].L) add(--L);
result[queries[i].idx] = currentSum;
} else { // 修改
int idx = queries[i].L;
int x = queries[i].x;
remove(idx);
temp[idx] = x;
add(idx);
}
}
for(int i=0; i<q; i++) {
if(queries[i].type == 1)
cout << result[queries[i].idx] << endl;
}
return 0;
}
5. 代码解析
type=1
表示查询,type=2
表示修改。- 在处理每个查询之前,动态应用之前的修改操作。
6. 复杂度分析
时间复杂度同普通莫队,额外复杂度取决于更新操作的数量。
三、树上莫队
1. 概述
树上莫队是普通莫队在树结构上的扩展,用于解决树上路径查询相关的问题。例如,求一棵树中从节点 u
到节点 v
路径上的某种统计值。
树上莫队通过离线处理,将树的查询问题转换为类似一维数组的问题。它的关键是将树线性化,使得莫队算法可以直接应用。
2. 适用场景
- 查询树中两节点路径上的元素和。
- 查询路径上的不同元素个数。
- 查询路径上是否存在某种特定条件的元素。
3. 算法思想
-
树的线性化(DFS序):
- 对树进行深度优先搜索,记录每个节点的起始时间和结束时间,即将树的节点展开为一个线性序列。
- 在路径查询时,树上两节点间的路径可以转换为这个序列上的区间。
-
莫队应用:
- 使用 DFS 序列,将路径查询转化为区间查询,直接套用莫队算法进行处理。
-
特殊处理:
- 对于路径上的两段区间(当路径经过树根时),需要考虑两次统计信息。
4. 代码实现
以下是求树上两节点路径元素和的实现:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
struct Edge {
int to, next;
} edge[MAXN*2];
int head[MAXN], tot=0;
int arr[MAXN]; // 树节点的值
int euler[2*MAXN]; // 记录DFS序列
int in[MAXN], out[MAXN], idx=0;
int result[MAXN]; // 查询结果
int currentSum=0; // 当前路径和
int vis[MAXN]; // 访问标记,1表示路径上,0表示不在路径上
struct Query {
int L, R, idx, lca;
} queries[MAXN];
// 添加树的边
void addEdge(int u, int v) {
edge[++tot] = {v, head[u]};
head[u] = tot;
}
// DFS 生成 Euler 序列
void dfs(int u, int parent) {
in[u] = ++idx;
euler[idx] = u;
for(int i=head[u]; i; i=edge[i].next) {
int v = edge[i].to;
if(v != parent) dfs(v, u);
}
out[u] = ++idx;
euler[idx] = u;
}
// 莫队排序规则
bool compare(Query a, Query b) {
if(a.L/316 != b.L/316) return a.L/316 < b.L/316;
return a.R < b.R;
}
// 切换访问状态
void toggle(int x) {
if(vis[x]) currentSum -= arr[x];
else currentSum += arr[x];
vis[x] ^= 1;
}
int main() {
int n, q;
cin >> n >> q;
for(int i=1; i<=n; i++) cin >> arr[i]; // 节点的值
for(int i=1; i<n; i++) {
int u, v;
cin >> u >> v;
addEdge(u, v);
addEdge(v, u);
}
dfs(1, -1); // 从节点1开始DFS
int queryCount=0;
for(int i=0; i<q; i++) {
int u, v;
cin >> u >> v;
if(in[u] > in[v]) swap(u, v); // 保证u在DFS序中靠前
if(out[u] >= in[v]) { // u是v的祖先
queries[queryCount++] = {in[u], in[v], i, -1};
} else { // u和v不在同一子树
queries[queryCount++] = {out[u], in[v], i, u};
}
}
sort(queries, queries+queryCount, compare);
int L=1, R=0;
for(int i=0; i<queryCount; i++) {
while(R < queries[i].R) toggle(euler[++R]);
while(R > queries[i].R) toggle(euler[R--]);
while(L < queries[i].L) toggle(euler[L++]);
while(L > queries[i].L) toggle(euler[--L]);
if(queries[i].lca != -1) toggle(queries[i].lca); // 特殊处理lca
result[queries[i].idx] = currentSum;
if(queries[i].lca != -1) toggle(queries[i].lca); // 恢复状态
}
for(int i=0; i<q; i++) cout << result[i] << endl;
return 0;
}
5. 代码解析
- 树的线性化:
- 使用 DFS 遍历树,记录每个节点进入和离开时的时间(
in[u]
和out[u]
)。 - DFS 序列
euler
是展开后的线性数组。
- 使用 DFS 遍历树,记录每个节点进入和离开时的时间(
- 路径查询的处理:
- 查询分为两种情况:
- 一个区间:若
u
是v
的祖先,路径可以直接表示为[in[u], in[v]]
。 - 两个区间:路径需要拼接为
[out[u], in[v]]
和lca
节点。
- 一个区间:若
- 查询分为两种情况:
- 莫队算法:
- 按照 DFS 序列,将路径查询转化为普通区间查询。
- 特殊处理
lca
节点,以避免重复统计。
6. 时间复杂度
- DFS:
O(n)
- 莫队:
O((n+q) * sqrt(n))
- 总时间复杂度为
O((n+q) * sqrt(n))
。
四、回滚莫队
1. 概述
回滚莫队是一种将莫队算法和回滚技术(Undo操作)相结合的扩展。通过保存操作的状态,可以在动态调整查询区间时快速回退至之前的状态,避免重新计算,从而支持更多场景。
2. 适用场景
- 查询区间和需要动态修改的操作同时存在,例如:
- 需要对数组某些位置频繁更新,但查询和更新操作彼此交替。
- 处理区间内满足某种性质的元素个数,并允许实时撤销。
- 查询区间上涉及复杂计算,且需要频繁修改的情况。
3. 算法思想
-
时间维度扩展:
- 在普通莫队中,处理区间是通过调整左右端点
[L, R]
来动态维护的,而在回滚莫队中,除了维护左右端点,还需要维护“时间”维度。 - “时间”维度指查询时的操作序号(支持修改)。
- 在普通莫队中,处理区间是通过调整左右端点
-
保存状态:
- 通过一个栈保存每次操作的影响,以便撤销(Undo)。
-
查询和修改处理:
- 查询:使用莫队处理左右端点区间。
- 修改:调整修改操作,通过回滚还原到目标状态。
4. 实现步骤
- 按莫队排序查询,时间维度按顺序加入。
- 对于每个查询:
- 根据当前时间,动态应用或撤销之前的修改操作。
- 动态调整左右端点
[L, R]
。 - 将最终结果存储。
5. 代码实现
以下是一个回滚莫队的实现,支持区间和查询以及动态修改操作:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
const int BLOCK=317;
int arr[MAXN]; // 原始数组
int currentSum=0; // 当前区间和
int result[MAXN]; // 查询结果
vector<pair<int, int>> updates; // 记录所有更新操作
struct Query {
int L, R, time, idx;
} queries[MAXN];
bool compare(Query a, Query b) {
if(a.L/BLOCK != b.L/BLOCK) return a.L/BLOCK < b.L/BLOCK;
if(a.R/BLOCK != b.R/BLOCK) return a.R/BLOCK < b.R/BLOCK;
return a.time < b.time; // 同块内按时间排序
}
void add(int idx) {
currentSum += arr[idx];
}
void remove(int idx) {
currentSum -= arr[idx];
}
void applyUpdate(int time, bool undo) {
int idx = updates[time].first;
int newValue = undo ? updates[time].second : arr[idx];
int oldValue = undo ? arr[idx] : updates[time].second;
remove(idx);
arr[idx] = newValue;
add(idx);
}
int main() {
int n, q;
cin >> n >> q;
for(int i=0; i<n; i++) cin >> arr[i];
int queryCount=0, updateCount=0;
for(int i=0; i<q; i++) {
int type;
cin >> type;
if(type == 1) { // 查询
int l, r;
cin >> l >> r;
queries[queryCount++] = {l-1, r-1, updateCount, i};
} else { // 修改
int idx, x;
cin >> idx >> x;
updates.push_back({idx-1, arr[idx-1]}); // 记录修改前的值
arr[idx-1] = x; // 实际修改
updateCount++;
}
}
sort(queries, queries+queryCount, compare); // 按莫队排序
int L=0, R=-1, currentTime=0;
for(int i=0; i<queryCount; i++) {
while(currentTime < queries[i].time) applyUpdate(currentTime++, false);
while(currentTime > queries[i].time) applyUpdate(--currentTime, true);
while(R < queries[i].R) add(++R);
while(R > queries[i].R) remove(R--);
while(L < queries[i].L) remove(L++);
while(L > queries[i].L) add(--L);
result[queries[i].idx] = currentSum;
}
for(int i=0; i<queryCount; i++) cout << result[i] << endl;
return 0;
}
6. 代码解析
- 维护三维状态:
- 左端点
L
,右端点R
,以及“时间”维度currentTime
。
- 左端点
- 处理修改操作:
- 每次修改操作保存在
updates
中,包含修改位置及旧值。 - 调用
applyUpdate
方法应用或撤销修改。
- 每次修改操作保存在
- Undo操作:
- 回滚时恢复原值,调整区间统计。
7. 时间复杂度
- 每个修改或查询操作时间复杂度为
O(sqrt(n))
。 - 总复杂度为
O((n+q) * sqrt(n))
。
五、二维莫队
1. 概述
二维莫队是普通莫队在二维平面问题上的扩展,主要解决以下类型的问题:
- 平面上点的某种统计。
- 矩形范围内的查询问题,例如统计范围内的点数或其他属性。
二维莫队将问题转化为离线查询,通过对查询的特殊排序,动态维护区间状态。
2. 适用场景
- 统计平面上的点数:如统计矩形范围内有多少点满足特定条件。
- 处理二维矩形范围查询:如查询矩形范围内的最大值、最小值或其他统计信息。
二维莫队通常适用于点分布稠密、矩形查询多的情况。
3. 算法思想
-
查询排序:
- 将查询按照左下角或右上角的坐标排序,保证查询顺序尽量减少状态切换的复杂度。
-
动态维护区间:
- 使用类似普通莫队的方式,动态维护当前统计范围。
- 动态调整的区间可能是二维矩形范围,包含左端点、右端点、上下边界。
-
块排序优化:
- 按照二维坐标分块处理,将块分布与查询顺序紧密结合。
4. 实现步骤
- 按二维分块对查询排序。
- 动态调整当前二维范围。
- 在矩形范围内计算统计值。
5. 代码实现
以下是二维莫队的完整实现,解决平面上矩形查询点数的问题。
题目描述:给定一组点 (x, y)
和多个查询,每次查询统计某个矩形范围内的点数。
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
const int BLOCK=316; // 块大小
struct Point {
int x, y, id;
};
struct Query {
int x1, y1, x2, y2, idx, ans;
};
int freq[MAXN]; // 记录每个点的频率
int pointCount=0; // 当前矩形范围内的点数
vector<Point> points;
vector<Query> queries;
// 查询排序规则
bool compare(Query a, Query b) {
int blockX1 = a.x1 / BLOCK, blockX2 = b.x1 / BLOCK;
if(blockX1 != blockX2) return blockX1 < blockX2;
int blockY1 = a.y1 / BLOCK, blockY2 = b.y1 / BLOCK;
return blockY1 == blockY2 ? a.y2 < b.y2 : blockY1 < blockY2;
}
// 修改矩形范围时的处理逻辑
void add(Point p) {
if(freq[p.id]++ == 0) pointCount++;
}
void remove(Point p) {
if(--freq[p.id] == 0) pointCount--;
}
int main() {
int n, q;
cin >> n >> q;
points.resize(n);
for(int i=0; i<n; i++) {
cin >> points[i].x >> points[i].y;
points[i].id = i; // 每个点一个唯一id
}
queries.resize(q);
for(int i=0; i<q; i++) {
cin >> queries[i].x1 >> queries[i].y1 >> queries[i].x2 >> queries[i].y2;
queries[i].idx = i;
}
sort(queries.begin(), queries.end(), compare);
// 初始化当前矩形范围
int Lx=0, Rx=-1, Ly=0, Ry=-1;
for(Query& query : queries) {
// 动态调整矩形范围
while(Rx < query.x2) add(points[++Rx]);
while(Rx > query.x2) remove(points[Rx--]);
while(Lx < query.x1) remove(points[Lx++]);
while(Lx > query.x1) add(points[--Lx]);
while(Ry < query.y2) add(points[++Ry]);
while(Ry > query.y2) remove(points[Ry--]);
while(Ly < query.y1) remove(points[Ly++]);
while(Ly > query.y1) add(points[--Ly]);
// 存储查询结果
query.ans = pointCount;
}
// 按原查询顺序输出结果
sort(queries.begin(), queries.end(), [](Query a, Query b) {
return a.idx < b.idx;
});
for(Query& query : queries) {
cout << query.ans << endl;
}
return 0;
}
6. 代码解析
- 查询排序:
- 按照二维坐标
x1
和y1
分块排序,确保查询尽量在同一块中连续进行。
- 按照二维坐标
- 矩形范围调整:
- 使用四个边界变量
Lx
,Rx
,Ly
,Ry
动态维护当前范围。
- 使用四个边界变量
- 统计结果:
- 统计范围内的点数,通过
freq
记录当前范围内每个点的出现次数。
- 统计范围内的点数,通过
7. 时间复杂度
- 查询排序:
O(q * log(q))
- 每次查询:调整区间的复杂度为
O(sqrt(n) + sqrt(q))
- 总时间复杂度:
O((n+q) * sqrt(n))
。
8. 适用例题
- 查询二维平面上某矩形范围的点数。
- 求解二维矩形的最大值或最小值。
- 统计范围内满足条件的点的属性。
六、莫队二次离线
1. 概述
莫队二次离线是在普通莫队算法的基础上,通过进一步分离和优化动态修改与查询操作而得的一种改进技术。相比普通莫队,二次离线更适合处理同时包含大量修改与查询的复杂场景,且在某些情况下可以进一步降低时间复杂度。
2. 适用场景
- 动态修改与查询同时存在:
- 需要在数组上既执行修改操作,又对数组某些区间进行查询统计。
- 修改与查询交替进行,修改会影响后续的查询。
- 适用于无法直接通过回滚实现的场景。
3. 算法思想
普通莫队通过维护左右端点和简单的修改序列来动态调整状态,而莫队二次离线通过以下思想优化:
-
时间轴上的分块:
- 将时间维度也划分为多个块,每个块处理特定范围的修改和查询操作。
- 按时间顺序逐块处理,块内操作离线排序,块间按顺序处理。
-
按修改排序:
- 每个块内,将修改操作与查询操作按照一定规则重新排序,以减少状态切换。
-
操作分离:
- 将修改操作与查询操作分离,避免在处理查询时频繁回退或重新应用修改操作。
4. 实现步骤
- 按时间分块,划分修改与查询操作。
- 在每个时间块内,分别维护当前的左右端点
[L, R]
以及当前时间。 - 动态调整区间和修改状态,按顺序离线处理每个块的查询操作。
5. 代码实现
以下是莫队二次离线的代码实现,用于同时处理数组的动态修改和区间查询问题。
题目描述:
- 给定一个数组,支持两种操作:
- 修改数组的某个位置的值。
- 查询数组某区间
[L, R]
内的最大值。
- 输出所有查询的结果。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
const int BLOCK=316; // 块大小(根号分块)
int arr[MAXN], temp[MAXN]; // 原始数组和临时数组
int result[MAXN]; // 查询结果
vector<pair<int, int>> updates; // 修改操作列表
struct Query {
int L, R, time, idx;
};
vector<Query> queries;
bool compare(Query a, Query b) {
int blockL = a.L / BLOCK, blockR = a.R / BLOCK;
if(blockL != blockR) return blockL < blockR; // 按左端点分块
return a.time < b.time; // 同一块按时间排序
}
// 区间统计的辅助变量
int currentMax = 0;
void add(int idx) {
currentMax = max(currentMax, arr[idx]);
}
void remove(int idx) {
// 对于区间删除不需要显式修改 currentMax,莫队查询本质是离线的。
}
void applyUpdate(int time, bool undo) {
int idx = updates[time].first;
int newValue = undo ? temp[idx] : updates[time].second; // 新值或旧值
temp[idx] = arr[idx];
arr[idx] = newValue;
}
int main() {
int n, q;
cin >> n >> q;
for(int i=0; i<n; i++) cin >> arr[i];
memcpy(temp, arr, sizeof(arr)); // 初始化临时数组
int queryCount = 0, updateCount = 0;
for(int i=0; i<q; i++) {
int type;
cin >> type;
if(type == 1) { // 查询操作
int l, r;
cin >> l >> r;
queries.push_back({l-1, r-1, updateCount, queryCount++});
} else { // 修改操作
int idx, x;
cin >> idx >> x;
updates.push_back({idx-1, x});
updateCount++;
}
}
sort(queries.begin(), queries.end(), compare);
// 初始化状态
int L = 0, R = -1, currentTime = 0;
for(Query& query : queries) {
// 调整时间到目标状态
while(currentTime < query.time) applyUpdate(currentTime++, false);
while(currentTime > query.time) applyUpdate(--currentTime, true);
// 调整区间左右端点
while(R < query.R) add(++R);
while(R > query.R) remove(R--);
while(L < query.L) remove(L++);
while(L > query.L) add(--L);
// 保存查询结果
result[query.idx] = currentMax;
}
for(int i=0; i<queryCount; i++) cout << result[i] << endl;
return 0;
}
6. 代码解析
- 离线处理:
- 查询和修改操作被离线保存,分别记录在
queries
和updates
中。
- 查询和修改操作被离线保存,分别记录在
- 时间维度分块:
- 按查询的时间
time
分块,每块内的操作按照时间顺序进行。
- 按查询的时间
- 动态状态调整:
- 每次查询前,通过
applyUpdate
方法调整当前数组状态到目标时间。
- 每次查询前,通过
- 区间查询与维护:
- 使用区间维护方法
add
和remove
动态统计[L, R]
区间的最大值。
- 使用区间维护方法
7. 时间复杂度
- 查询排序:
O(q * log(q))
- 每次查询的区间调整复杂度为
O(sqrt(n))
。 - 时间维度的调整复杂度为
O(sqrt(n))
。 - 总时间复杂度:
O((n+q) * sqrt(n))
。
8. 适用例题
- 动态区间最大值:查询数组中某区间的最大值,并支持实时修改。
- 区间和与动态更新:同时支持区间和查询和数组元素的实时更新。
- 动态统计问题:如统计区间中满足某种条件的元素个数。
七、莫队配合 bitset
优化
1. 概述
莫队算法结合 bitset
是一种高效处理状态表示复杂的问题的技术,常用于:
- 大范围数据的快速统计和处理。
- 特定情况下的大规模布尔运算优化。
通过 bitset
代替常规数组或位运算,可以有效减少内存占用,同时利用 bitset
的硬件加速能力,提高复杂计算的速度。
2. 适用场景
- 区间内的子集问题:
- 判断区间内是否存在某些数字的子集。
- 布尔运算统计:
- 统计区间内布尔值的特定运算结果,如逻辑与、或、异或。
- 大范围问题的动态处理:
- 当数组的值域很大(如值域高达 $10^5$ 甚至更高)时,
bitset
的使用可以避免直接处理高维数组。
- 当数组的值域很大(如值域高达 $10^5$ 甚至更高)时,
3. 算法思想
-
bitset
高效位操作:bitset
是一种定长位数组,支持按位运算(如与、或、异或等)。bitset
的每次位操作(如按位统计)可以在常数时间内完成。
-
与莫队结合:
- 通过
bitset
动态维护区间内的状态。 - 在莫队的左右端点移动过程中,利用
bitset
高效更新状态,快速计算查询结果。
- 通过
-
状态表达:
- 用
bitset
表示某种属性的集合(如某数字是否出现、某状态是否有效)。 - 查询区间状态通过位运算快速计算。
- 用
4. 实现步骤
- 根据问题定义,初始化
bitset
用于存储状态信息。 - 按莫队的区间查询逻辑排序和分块。
- 动态调整区间的左右端点
[L, R]
,使用bitset
更新区间状态。 - 根据查询要求,通过
bitset
的位运算计算结果。
5. 代码实现
题目描述:
给定一个长度为 $n$ 的数组和 $q$ 个查询,每个查询要求判断区间 $[L, R]$ 内是否存在若干数字的和为某固定值 $S$ 的子集。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100000;
const int BLOCK=316; // 块大小(根号分块)
const int MAXS=1000; // 目标和的最大值
int arr[MAXN];
bitset<MAXS> currentState; // 当前区间的子集和状态
vector<int> results;
struct Query {
int L, R, S, idx;
};
vector<Query> queries;
// 查询排序规则
bool compare(Query a, Query b) {
int blockL = a.L / BLOCK, blockR = b.R / BLOCK;
if(blockL != blockR) return blockL < blockR;
return a.R < b.R;
}
// 在当前状态中添加或移除一个数字
void add(int val) {
currentState |= (currentState << val); // 添加新值,更新所有可能的子集和
}
void remove(int val) {
// 为简单起见,重新构建状态以避免复杂逆运算
currentState.reset(); // 清空状态
currentState[0] = 1; // 初始化空集和
for(int i=0; i<val; i++) add(arr[i]);
}
int main() {
int n, q;
cin >> n >> q;
for(int i=0; i<n; i++) cin >> arr[i];
queries.resize(q);
for(int i=0; i<q; i++) {
int l, r, s;
cin >> l >> r >> s;
queries[i] = {l-1, r-1, s, i};
}
sort(queries.begin(), queries.end(), compare);
results.resize(q);
// 初始化状态
int L = 0, R = -1;
currentState.reset();
currentState[0] = 1; // 初始空集和
for(Query& query : queries) {
// 动态调整区间左右端点
while(R < query.R) add(arr[++R]);
while(R > query.R) remove(arr[R--]);
while(L < query.L) remove(arr[L++]);
while(L > query.L) add(arr[--L]);
// 计算查询结果
results[query.idx] = currentState[query.S];
}
// 输出查询结果
for(int res : results) {
cout << (res ? "YES" : "NO") << endl;
}
return 0;
}
6. 代码解析
-
状态初始化:
- 使用
bitset
存储子集和的状态,currentState[i] = 1
表示当前区间内存在和为i
的子集。 - 初始状态为空集,
currentState[0] = 1
。
- 使用
-
动态区间调整:
add
函数通过位移操作将新的数字添加到子集和的状态中。remove
函数简单地重建状态,以避免复杂的逆操作。
-
查询计算:
- 直接检查
currentState[S]
是否为 1,表示是否存在子集和为S
。
- 直接检查
7. 时间复杂度
- 区间调整:每次调整的复杂度为
O(MAXS)
(bitset
的最大长度)。 - 查询排序:
O(q * log(q))
。 - 总复杂度:
O((n+q) * sqrt(n) * MAXS)
。
8. 优化思路
- 减少
MAXS
的长度:- 通过压缩值域,降低
bitset
的规模。
- 通过压缩值域,降低
- 优化删除操作:
- 使用更高效的方法实现逆运算,避免状态重建。
9. 适用例题
- 子集和问题:
- 判断区间内是否存在子集满足特定和。
- 布尔运算问题:
- 区间内的布尔值操作统计(如按位与、或运算)。
- 值域压缩问题:
- 数据范围大,需通过
bitset
实现高效统计。
- 数据范围大,需通过
八、莫队算法总结与进阶
经过前面的逐步讲解,我们已详细解析了莫队算法的基本应用及其各种优化,包括普通莫队、带修改莫队、树上莫队、回滚莫队、二维莫队、二次离线优化以及莫队结合 bitset
。在本节中,将进一步总结莫队算法的应用场景,分析优缺点,并探讨更多进阶内容,如与其他算法的结合及特殊优化场景。
1. 莫队算法的核心思想回顾
莫队算法通过离线排序和分块思想解决查询问题,其核心特点为:
- 分块减少复杂度:将区间查询问题划分为多个小块,每块内单独维护状态,降低总体时间复杂度。
- 离线排序优化查询顺序:通过重新排列查询顺序,使得区间调整的代价最小化。
- 动态区间调整:通过左右端点移动 (
add
和remove
操作) 动态维护区间状态。
2. 莫队算法的适用场景
莫队算法适用于以下类型的问题:
- 静态区间查询问题:
- 如区间和、区间最大值、区间最小值、区间众数等。
- 动态修改与查询混合问题:
- 如带修改的区间统计、区间最大值等需要同时支持修改和查询的问题。
- 复杂统计或布尔问题:
- 如区间内的子集和、区间内满足某条件的元素统计。
- 结构简单但查询次数多的问题:
- 数据规模大、查询次数多但要求无需实时更新(离线计算)。
3. 莫队算法的优缺点
优点:
- 时间复杂度低:常规问题可降至 $O((n+q) \cdot \sqrt{n})$。
- 简单易实现:只需实现
add
和remove
操作。 - 灵活性强:可扩展到多种复杂问题,通过结合其他技术(如回滚、
bitset
、二维分块等)进一步优化。
缺点:
- 在线性更新场景不适用:必须离线排序,无法处理在线查询。
- 对数据结构要求高:需要根据问题设计高效的区间维护方法,部分问题难以实现逆运算。
- 对于小规模问题效率低:当数据量和查询次数都较小时,莫队的常数较大,表现可能不如简单算法。
4. 莫队算法的扩展与优化
以下是一些莫队算法的进阶应用和优化思路:
(1)结合其他数据结构
- 与树状数组结合:
- 适用于维护区间内频率统计问题。例如,计算区间内某值出现的次数。
- 在
add
和remove
操作中动态更新树状数组。
- 与线段树结合:
- 用线段树替代分块,用于处理区间的动态统计,如最小值或最大值。
(2)多维场景的优化
- 二维莫队:
- 适用于二维平面上的查询问题,如点对之间的距离统计。
- 按二维坐标离线排序,结合左右上下四个方向的动态调整。
- 多维分块:
- 对于高维数据,可以通过对多维索引进行分块实现动态调整。
(3)值域压缩与位优化
- 值域压缩:
- 将大范围数据的值域映射到较小范围,减少内存占用和运算复杂度。
- 结合
bitset
优化:- 利用
bitset
的硬件加速特性快速处理布尔运算和大范围统计问题。
- 利用
(4)特殊问题的改进
- 稀疏数组问题:
- 当数据稀疏且分布不均时,可采用分块大小动态调整或跳表辅助优化。
- 带权统计问题:
- 将每个区间元素赋予权值,并动态维护加权结果。
5. 与其他算法的结合
莫队算法在许多场景中可以结合其他算法提升性能或扩展适用范围:
(1)与双指针结合
- 双指针法:快速调整左右端点,减少
add
和remove
的执行次数。
(2)与回滚技术结合
- 回滚莫队:用于解决动态修改问题,回滚状态避免重复计算。
(3)与图算法结合
- 树上莫队:处理树形结构上的查询问题,结合树的重心分解或欧拉序优化分块。
6. 进阶例题与分析
以下是一些具有挑战性的例题,体现莫队的应用价值及扩展能力。
(1)区间内的最大异或值
- 给定一个数组,查询每个区间内的子数组的最大异或值。
思路:
- 使用莫队结合
trie
数据结构动态维护当前区间的异或值集合。
(2)二维平面最近点对统计
- 在二维平面上,查询每个矩形区域内最近的两点距离。
思路:
- 将二维平面分块,按二维坐标排序查询,结合莫队动态调整。
(3)动态修改的众数问题
- 查询每个区间的众数,并支持动态修改数组的元素值。
思路:
- 结合莫队的回滚技术,通过计数器动态维护当前区间的众数。
7. 常见问题的复杂度比较
问题类型 | 算法 | 时间复杂度 |
---|---|---|
静态区间查询 | 普通莫队 | $ O((n+q) \cdot \sqrt{n}) $ |
带修改查询 | 带修改莫队 | $ O((n+q) \cdot \sqrt{n}) $ |
树上查询 | 树上莫队 | $ O((n+q) \cdot \sqrt{n}) $ |
二维范围查询 | 二维莫队 | $ O((n+q) \cdot \sqrt{n+m}) $ |
子集和统计 | 莫队 + bitset |
$ O((n+q) \cdot \sqrt{n} \cdot m) $ |
8. 总结与下一步
莫队算法的强大在于它的通用性和扩展性,通过与其他算法或数据结构结合,可以轻松应对从简单查询到复杂动态问题的广泛场景。如果需要进一步了解特定应用的实现(如二维莫队、结合 trie
等),或者更深入的理论分析,请告诉我!
标签:idx,int,查询,Helped,queries,区间,ChatGPT,莫队 From: https://www.cnblogs.com/mycbxzd/p/18600285