个人觉得分块和莫队都是相当暴力不讲道理的算法
分块
首先来说说分块。
顾名思义,把一段区间分成一块一块的。
分成一块块的有什么好处呢?如果我们要进行区间修改区间查询,当这个区间中包含了整个块时,如果我们知道了这个块的一些信息,就没必要对块内的元素一个个进行修改或查询,如此就达到了降低复杂度的目的。
于是分块的策略就很清楚了,对于当前操作的区间[L,R],对于其中的整块我们整体修改或查询,而两边可能有的非整块元素则暴力修改,再将两端所属的块更新,这些元素个数显然不会超过两个块的大小,所以时间复杂度就有了保证。
那么块的大小应该怎么确定呢?理论上,如果区间长度为$n$,则当块大小为$\sqrt{n}$时时间复杂度是最优的,每次操作复杂度为$O(\sqrt{n})$。
1 | //以区间加和区间求和为例 |
莫队
莫队是在分块的基础上,更暴力的一种算法。
对于只有查询的题目,我们可以通过移动l r指针,计算相应的添加或者删除某个元素得到的贡献来得到所有查询的答案,但是这样的话复杂度可能达到$n^2$。
由于只有查询,我们可以将这些要查询的区间改变顺序,使得两个指针移动的距离尽量减少,从而达到降低复杂度的目的。
考虑将整个区间分块,然后将所有查询进行双关键字排序,第一关键字为左端点所在的块,第二关键字为右端点本身。
这样的话,对于左端点,在同一块内的转移每次不会超过$\sqrt{n}$,不同块之间的转移不会超过$\sqrt{n}$;
对于右端点,在左端点在同一块时,右端点是单调不降的,即最多$n$,而在左端点不同块之间的转移不会超过$\sqrt{n}$
因此总时间复杂度为$O(n\sqrt{n})$。
1 | struct Query { |
带修莫队
带修莫队实际上就是在普通莫队的基础上增加一个时间轴,来表示修改的时间点。
分块策略与普通莫队类似,还是先l r分块,然后还要对同一块内的修改排序(假设为单点修改)
在移动完l r指针后,再去移动时间轴指针t,类似地进行相应修改,基本思想其实是一致的。
1 | struct Node { |
树上莫队
树上莫队用于解决树上问题,我们将树上问题转化为序列问题,从而使用莫队求解。
如何将一个树上的问题转化成一个序列问题呢?我们借助dfs序(括号序列)达到这一点。
首先指定根,对于每个点x,我们记录其在dfs过程中第一次遇到时dfs序为st[x],第二次遇到时dfs序为ed[x],并记录这两个dfs序对应的点euler[st[x]]=euler[ed[x]]=x。不难发现,在dfs序组成的长度为2n的序列中,如果在某一段区间[l,r]内st[x]和ed[x]同时存在,那么x必然不在原树上euler[l]和euler[r]的最短路径中,且为其中某个节点的后代。由此我们就能联想到,记录当前区间x是否出现过,进行相应的加或减操作,就可以统计出路径上的真实情况,形象地理解的话考虑从一个点到另一个点的过程中,先走到了中间某个点的其他分支,再走回来,走过去的过程中我们将中间的点都计入贡献,而回来时再将其删掉,这样就保证了正确性。
当然,上面给出的是一个相当模糊的概述,如果我们模拟一下就会发现,要分两种情况(设查询的点对为(x, y),其中st[x] < st[y]):
- LCA(x, y)为x,这时候只需要从ed[x]走到st[y]就可以了,从st[x]开始没有意义
- LCA(x, y)不为x,这时候要注意,我们的区间[st[x], st[y]]中显然是不包括LCA的(为什么),所以对于这个查询,我们还要记录下其LCA,在计算答案时将LCA计入答案,但是要记住,计入答案后要立刻删掉,不能影响到后面的答案计算,因为区间内实际上是没有LCA这个点的dfs序的。
下面给出树上数颜色的模板
洛谷SP10707 COT2 - Count on a tree II
1 | struct Query { |
回滚莫队
咕咕咕
v1.5.2