骗分带师: 莫队Ⅰ
莫队算法入门
离线算法, 解决多次区间询问问题. 算法的条件是一个问题区间 询问的答案可以由 , , , 中的任意某个区间的答案以 的复杂度求出. 通过将区间排序, 然后暴力地用上一个答案求下一个答案. 复杂度为 .
结合莫队的经典模板题小Z的袜子来讲解.
题面简述
一行 只袜子, 求区间 中随机挑出两只后, 这两只袜子颜色相同的几率, 次询问.
算法框架
开一个数组 存储当前区间每种袜子数量, 表示当前区间第 个颜色的袜子个数.
开一个数组 存每个询问的答案, 用于按询问的输入顺序输出答案. 由于要输出最简分数, 所以每个答案是两个整数, 分别代表分子和分母.
分块, 以 的块数为第一关键字, 为第二关键字排序. 依次枚举所有的询问, 暴力地从上一个答案推得本次答案.
增量
增量就是用相邻区间 (即只有一个端点相差为 , 另一个端点相同的两个区间) 的答案求目标区间答案的操作. 莫队算法复杂度的正确性要求增量操作复杂度为 .
设第 只袜子颜色为 , 区间长度为 .
暴力维护 数组, 用 的 求 的 时, 需要将 减少 . 复杂度 . 已知 或 求 的情况同理.
保证了 数组的 up to date, 接下来求概率. 考虑某种颜色的袜子被选中一双的概率. 第一只被选中的袜子颜色为 的几率为 , 第二只仍为 的几率为
但是如果每个颜色单独算, 单次查询时间复杂度 , 一定会超时. 所以考虑一起计算. 如果一次增量中, 区间长度从 变成了 (当然, ). 没有变化的某种颜色 的袜子被选中一双的概率从 变成 .
所以对于每个 没有变化的颜色, 被选中一双的概率都会变成原来的 .
那么对于被加入或删除了一个袜子的颜色 , 选中一双颜色为 的袜子的概率从原来的 变成 .
因此, 设原答案为 新的答案 就是:
分类讨论, 推式子
- 当区间增长了 , 即
- 当区间缩短了 , 即
考虑约分的操作. 因为分数的计算需要大量的乘积, 所以需要随时约分. 答案都是真分数, 分母最多是 , 分子不大于分母, 所以答案的分子分母不超过 , 不超过 unsigned
的范围.
使用 unsigned long long
进行中间计算, 在每次计算后约分. 使用欧几里得算法求分子分母的 gcd, 然后分子分母一起除以这个 gcd.
特别地, 在 的情况下, , 分母为零无意义, 也存在概率为零的情况, 样例表明, 概率为 时, 输出 .
当概率为 时, 答案为整数, 同样有样例表明, 这时输出 .
但是这样的复杂度外面就会乘上一个 , 变成 , 因为每次增量要求 GCD. 所以考虑更优的解法. 考虑从一开始单个颜色袜子选中一双的概率入手, 推得一个式子, 整理.
这样, 只要维护区间袜子数量平方和即可 求答案. 由于平方和, 区间长度的平方不大于 , 所以无需每次增量约分, 只要约分 次即可.
维护平方和, 对于某个 的变化对平方和的影响, 如果 在原来的基础上增加了 , 即 , 则
对于 的情况, 有
复杂度证明
因为莫队的前提是一次增量时间复杂度 O(1), 所以只要证明莫队对长度为 的序列的 次区间查询的复杂度是 .
在分块的块长选择 的前提下, 块数就是 . 使用 algorithm
中自带的排序, 复杂度是 .
接下来对当前答案进行增量, 对于左端点和上一个询问同块的情况, 左端点最多增量 次. 对于左端点在新的块的询问, 最多增量 次, 也是 . 所有左端点增量次数复杂度为 . 左端点这个块内的所有询问, 右端点递增, 所以这些询问右端点总共增量最多 次. 所以右端点总共的增量数应该是 . 综上, 左右端点增量数复杂度为
分析更一般的情况, 如果块长选 , 块数就是 . 用上面的方式分析, 左端点的增量数复杂度是 , 右端点的增量数复杂度是 , 总复杂度为 . 使用均值不等式, 最优复杂度为 , 当 时取到等号. 整理求块长:
所以, 一般莫队的最优块长是
代码实现
细节不多, 码量短小, 把推的式子写成代码就好了, 是骗分的好工具.
unsigned m, n, Cnt[50005], BlockLen, BlockCnt;
long long a[50005], Ans[50005][2], Tmp0(0), Tmp1(1), TmpG, TmpSquare(1);
struct Query{
unsigned L, R, Num, BelongToBlocks;
inline const char operator <(const Query &x) { // 按左端点所在块排序
return (this->BelongToBlocks ^ x.BelongToBlocks) ? this->BelongToBlocks < x.BelongToBlocks : this->R < x.R;
}
}Q[50005];
inline unsigned GCD(register unsigned x, register unsigned y) {
register unsigned tmp;
while(y) tmp = x, x = y, y = tmp % y;
return x;
}
int main() {
n = RD(), m = RD();
BlockLen = (m ^ 0) ? (n / sqrt(m)) + 1 : sqrt(n) + 1;
for (register unsigned i(1); i <= n; ++i) {
a[i] = RD();
}
for (register unsigned i(1); i <= m; ++i) {
Q[i].L = RD(), Q[i].R = RD(), Q[i].Num = i, Q[i].BelongToBlocks = (Q[i].L + BlockLen - 1) / BlockLen;
}
sort(Q + 1, Q + m + 1);
Tmp0 = 0, Tmp1 = 1, Q[0].L = 1, Q[0].R = 1, Cnt[a[1]] = 1; // 初始化当前区间为 [1, 1]
for (register unsigned i(1); i <= m; ++i) {
if(Q[i].L == Q[i].R) { // 特判, 单点查询
Ans[Q[i].Num][0] = 0, Ans[Q[i].Num][1] = 1;
continue;
}
register unsigned Col, Len(Q[i].R - Q[i].L + 1);
while (Q[0].L > Q[i].L) { // 左端点左移
++Cnt[Col = a[--Q[0].L]];
TmpSquare += (Cnt[Col] << 1) - 1; // Cnt[Col] 增加, 维护方差, 下同
}
while (Q[0].R < Q[i].R) { // 右端点右移
++Cnt[Col = a[++Q[0].R]];
TmpSquare += (Cnt[Col] << 1) - 1;
}
while (Q[0].L < Q[i].L) { // 左端点右移
--Cnt[Col = a[Q[0].L++]];
TmpSquare -= (Cnt[Col] << 1) + 1;
}
while (Q[0].R > Q[i].R) { // 右端点左移
--Cnt[Col = a[Q[0].R--]];
TmpSquare -= (Cnt[Col] << 1) + 1;
}
Ans[Q[i].Num][0] = TmpSquare - Len;
Ans[Q[i].Num][1] = Len * Len - Len;
TmpG = GCD(Ans[Q[i].Num][0], Ans[Q[i].Num][1]);
Ans[Q[i].Num][0] /= TmpG;
Ans[Q[i].Num][1] /= TmpG;
}
for (register unsigned i(1); i <= m; ++i) {
printf("%u/%u\n", Ans[i][0], Ans[i][1]);
}
return Wild_Donkey;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具