学习笔记-莫队
普通莫队
如何求在某个区间内,有多少种不同的数字,每个数字总共出现了多少次?
可以开个桶,对于每个区间暴力求解。
但是如果有很多组询问呢?
显然有更优的算法,可以将一个询问的状态转移到另一个询问的状态。
这就是莫队算法。
实现过程:两个指针 l
和 r
,随着询问的范围在数组上移动。移动时顺便记录增加的元素或是减少的元素。
void add(int pos) { cnt[a[pos]]++; if (cnt[a[pos]] == 1) tot++; } void del(int pos) { cnt[a[pos]]--; if (cnt[a[pos]] == 0) tot--; }
为了减少不必要的移动,我们考虑离线,把所有询问都存下来然后排序。
那么这些询问怎么排序呢?考虑以左端点排序。那么在这时,左端点最多移动 n 次,但是右端点却有可能反复横跳,从而把时间复杂度拉到 $O(n^2)$。
按右端点排序也是一样的。
这个时候就要请出我们的分块思想了。
我们将数组进行分块(注意,这里并没有实际分块,只是借助了分块的思想)。
然后以左端点所在块号为第一关键字排序,以右端点位置为第二关键字排序。
这样,左端点只可能在块内反复横跳,右端点也最多只用跳 $n\sqrt n$ 次。
总的时间复杂度降到了 $O(n\sqrt n)$。
完整实现代码:
排序&指针移动:
struct query { int l, r, id; }q[200010]; int ans[200010]; bool operator < (const query &xx, const query &yy) { if (getblock(xx.l) == getblock(yy.l)) { return xx.r < yy.r; } return getblock(xx.l) < getblock(yy.l); } void add(int pos) { cnt[a[pos]]++; if (cnt[a[pos]] == 1) tot++; } void del(int pos) { cnt[a[pos]]--; if (cnt[a[pos]] == 0) tot--; }
询问处理:
sort(q + 1, q + tt + 1); int nowl = 1, nowr = 0; for (int i = 1; i <= tt; i++) { int l = q[i].l, r = q[i].r; while (nowr < r) { add(++nowr); } while (nowr > r) { del(nowr--); } while (nowl < l) { del(nowl++); } while (nowl > l) { add(--nowl); } ans[q[i].id] = tot; } for (int i = 1; i <= tt; i++) { cout << ans[i] << "\n"; }
带修莫队
有的时候数组并不是静态的,在询问过程中有可能产生修改。
莫队如何处理这些修改?
其实我们在原来莫队的基础上加一个时间轴概念就好了。
对于每一个询问,我们记录它上次所做修改的编号。
运用莫队思想,将数组还原到刚做这个修改之后的状态就行了。
void modify(int pos, int val, int nowl, int nowr) { if (pos >= nowl && pos <= nowr) { del(pos); } a[pos] = val; if (pos >= nowl && pos <= nowr) { add(pos); } }
注意:
1. 如果修改的位置在查询范围之内,也要同时更新计数数组。
2. 经过一番玄学而严谨的数学证明,带修莫队的块长应该开成 $n^{\frac{2}{3}}$ 才最优。
总结
这几天对于分块和莫队的学习让我意识到了暴力算法也可以这么优雅。
其实,不管是分块还是莫队,我认为优化暴力时间复杂度的关键在于平衡块内和块外的操作次数。 这也是为什么一般的分块会把块长设为 $\sqrt n$。
本文作者:aaaaaaqqqqqq
本文链接:https://www.cnblogs.com/aaaaaaqqqqqq/p/17976965
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步