题解:洛谷 P2709 小 B 的询问
【题目来源】
洛谷:P2709 【模板】莫队 / 小 B 的询问 - 洛谷
【题目描述】
小 B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)。
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:
\[\sum\limits_{i=1}^k c_i^2
\]
其中 \(c_i\) 表示数字 \(i\) 在 \([l,r]\) 中的出现次数。
小 B 请你帮助他回答询问。
【输入】
第一行三个整数 \(n,m,k\)。
第二行 \(n\) 个整数,表示小 B 的序列。
接下来的 \(m\) 行,每行两个整数 \(l,r\)。
【输出】
输出 \(m\) 行,每行一个整数,对应一个询问的答案。
【输入样例】
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6
【输出样例】
6
9
5
2
【核心思想】
-
问题分析:给定长度为 \(n\) 的序列(值域 \([1, k]\))和 \(m\) 个区间查询,每个查询求区间内各数字出现次数的平方和 \(\sum_{i=1}^{k} c_i^2\)。若对每个查询暴力统计,时间复杂度为 \(O(m \cdot n)\)。关键观察是:相邻查询的区间往往重叠,通过离线排序使指针移动总次数最小化,可以将复杂度优化至 \(O((n + m) \sqrt{n})\)。
-
算法选择:
- 莫队算法(Mo's Algorithm):离线处理区间查询,按分块排序后,用双指针维护当前区间,通过 \(O(1)\) 的增删操作调整区间
- 分块排序策略:按左端点所在块排序,同块内按右端点排序,减少指针跳跃
-
关键步骤:
- 读入与初始化:读入序列和查询,计算分块大小 \(B = \lfloor \sqrt{n} \rfloor\)
- 离线排序:按左端点所在块升序排序,同一块内按右端点升序排序
- 双指针维护区间:初始化 \(l = 1, r = 0\)(空区间),对每个查询:
while (l > q[i].l) add(a[--l]):左边界左扩while (r < q[i].r) add(a[++r]):右边界右扩while (l < q[i].l) del(a[l++]):左边界右缩while (r > q[i].r) del(a[r--]):右边界左缩
- \(O(1)\) 增删维护答案:
add(x):sum -= cnt[x]^2; cnt[x]++; sum += cnt[x]^2del(x):sum -= cnt[x]^2; cnt[x]--; sum += cnt[x]^2
- 按原序输出:用
ans[id]保存答案,最后按查询编号输出
-
时间/空间复杂度:
- 时间复杂度:\(O((n + m) \sqrt{n})\),排序 \(O(m \log m)\),指针移动总次数 \(O(n \sqrt{n})\)
- 空间复杂度:\(O(n + m)\),序列、计数器、查询和答案数组
-
莫队算法的核心思想:
- 离线排序降低移动代价:通过将查询按特定顺序排列,使得相邻查询的区间变化尽可能小,从而将暴力枚举的 \(O(n)\) 每次查询降为均摊 \(O(\sqrt{n})\)
- 分块平衡左右指针:左端点按块排序确保左指针在同一块内只单向移动,右端点排序确保右指针在块间跳跃时总体移动可控
- 增量维护答案:
add和del操作利用 \(\Delta = (c+1)^2 - c^2 = 2c + 1\) 在 \(O(1)\) 内更新答案,避免了每次重新计算 - 奇偶性优化(Hilbert 曲线):进阶版本可用 Hilbert 曲线排序进一步减少指针移动,但基础分块已足够应对大多数场景
- 适用于离线区间查询、区间统计、需要频繁增删维护答案的问题,是暴力与数据结构之间的优雅折中
【算法标签】
提高+ #莫队
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// 定义长整型别名,便于处理大数据
#define int long long
// 定义数组最大容量
const int N = 200005;
// 全局变量声明
int n, m, k, B; // n: 数组长度, m: 询问次数, k: 未使用参数, B: 分块大小
int a[N]; // 原始数组
int cnt[N]; // 计数器,记录每个数值出现的次数
int ans[N]; // 存储每个询问的答案
int sum; // 当前区间内所有数的出现次数的平方和
// 询问结构体
struct Q
{
int l, r; // 区间左右端点
int id; // 询问编号
} q[N];
// 莫队排序比较函数(奇偶性优化)
bool cmp(Q a, Q b)
{
if (a.l / B != b.l / B)
return a.l < b.l; // 不同块按左端点升序
return a.r < b.r; // 同块按右端点升序
}
// 添加一个数到当前区间
void add(int x)
{
sum -= cnt[x] * cnt[x]; // 减去旧贡献
cnt[x]++; // 增加计数
sum += cnt[x] * cnt[x]; // 加上新贡献
}
// 从当前区间删除一个数
void del(int x)
{
sum -= cnt[x] * cnt[x]; // 减去旧贡献
cnt[x]--; // 减少计数
sum += cnt[x] * cnt[x]; // 加上新贡献
}
// 主函数入口
signed main()
{
// 读取数组长度、询问次数和未使用的参数k
cin >> n >> m >> k;
// 计算分块大小
B = sqrt(n);
// 读取原始数组
for (int i = 1; i <= n; i++)
cin >> a[i];
// 读取所有询问
for (int i = 1; i <= m; i++)
{
cin >> q[i].l >> q[i].r;
q[i].id = i; // 记录原始顺序
}
// 按照莫队顺序排序询问
sort(q + 1, q + m + 1, cmp);
// 初始化当前区间为空,l=1, r=0表示空区间
for (int i = 1, l = 1, r = 0; i <= m; i++)
{
// 移动指针到目标区间
while (l > q[i].l) // 左边界向左扩展
add(a[--l]);
while (r < q[i].r) // 右边界向右扩展
add(a[++r]);
while (l < q[i].l) // 左边界向右收缩
del(a[l++]);
while (r > q[i].r) // 右边界向左收缩
del(a[r--]);
// 存储当前区间的答案
ans[q[i].id] = sum;
}
// 按原始顺序输出所有答案
for (int i = 1; i <= m; i++)
cout << ans[i] << endl;
return 0;
}
【运行结果】
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6
6
9
5
2
浙公网安备 33010602011771号