分块算法
分块技巧
假设给我们一个长度为n的数组,要求支持区间更新,区间统计。
最简单的方式是将数组分块,每一块大小为m=√n。之后我们为每一块都额外维护一份辅助信息,第i块的辅助信息记作f(i)。
现在考虑区间更新问题,只要利用惰性标记就可以实现√n的时间复杂度。对于完全处于区间中的块,我们直接标记在辅助信息上(如果已经有标记了,就合并标记),这样的块最多有√n个。而对于与区间有交集的却不被完全覆盖的块,我们先下推标记,之后直接暴力修改,可以得知这样的块不会超过两个,总的修改的元素不超过2√n个。
查询操作也是相同的,对于被完全覆盖的块,直接从辅助信息中提取,否则直接暴力查询,时间复杂度为O(√n)。
题目1:给定n个数a1,…,an,要求处理m个请求,请求分两类:第一类某个区间加上一个数x(允许为负数)。第二类查询区间中小于x的数的数目。(其中x不同请求可能不一样),n,m≤105。
这个问题可以对数组进行分块,设B为每块的大小。之后对于修改操作,中间的区间打标记,两端的区间暴力更新(合并两个有序区间可以线性完成),因此修改请求的时间复杂度为O(nB+B)。而对于查询请求,中间的用二分查找法,两端的暴力查找,时间复杂度为O(2B+nBlog2B)。
因此时间复杂度的上限为m(B+nlog2BB),我们可以选择B=√nlog2B,则每个请求的时间复杂度为√nlog2B,总的时间复杂度为O(m√nlog2B)。
实际上这个问题还可以用Fractional Cascading的技术,优化到O(m√n),但是由于实现复杂,很可能常数会大于√log2n。
提供一道题目。
题目2:给定n个数a1,…,an,要求处理m个请求。请求分为两类:第一类请求要求将区间[l,r]中所有大于等于x的数全部减去x,第二类请求查询区间[l,r]中有多少数正好等于x(不同请求的x可以不同)。满足1≤n,m,ai,x≤105。
我们可以按B对数组进行分块,并记录一个数组L,其中L(i,j)代表第i块中值恰好为j的所有数组成的链表。
先考虑查询操作,两端的区间暴力处理即可,时间复杂度为O(B)。而中间的每个区间,我们可以直接查表O(1)得出结果,因此总的时间复杂度为O(B+nB)。
在考虑修改操作,两端的区间暴力处理,时间复杂度为O(B)。而中间的区间,我们记区间中的最大值为k。
- 2x≤k,我们可以暴力查表找出所有小于x的数,并将其增加x。之后我们给整个块打上一个减x的标记。(这时候最大值为k−x)
- 2x>k,我们可以暴力查表找出所有大于等于x的数,并将其减去x。
对于第一个操作,我们每查找一个数,最大值都要减少1。而对于第二个操作,最大值减少的量不少于我们查找数的数目。因此对于每个块,我们可以断定扫描次数一定不超过这个块中的最大值。记所有数的最大值为M,那么扫描的总次数不超过MnB。
综合上面所说的,时间复杂度为O(mB+nmB+nMB)。这里我们可以记N=max(n,m,M)即可得到O(N√N)时间复杂度。
一道题目。
树上分块
题目1:给定n个顶点组成的树,第i个顶点上写着一个数字ai。令顶点1作为根。现在给出q个请求,每个请求指定两个顶点u,v,其中保证u是v的祖先。这时候给u到v路径上任意顶点分配权重,顶点x的权重为ax⊕dist(v,x),对于每个请求,找出u与v决定的唯一路径上,最大的顶点权重。其中1≤n≤6×104,0≤ai≤n,1≤q≤2×105。
发现所有涉及的数最多只有16位(二进制),我们可以将每个数拆解为高8位和低8位。预处理出数组dp(v,i),记A表示所有顶点v的祖先中距离顶点v小于28的顶点组成的集合,则dp(v,i)=maxu∈Aau⊕dist(u,v)⊕(i⋅28)。
我们一旦计算出dp(v,i),对于每个请求,我们可以从路径底部每次向上跳28距离,如果距离顶部小于28,则暴力计算。因此一次请求的处理时间复杂度最多为6⋅10428+28≤29。请求处理的总时间为O(29q)。
那么该如何计算dp(v,i)呢,最简单的暴力计算方法就是枚举所有满足条件的祖先,这样处理每个顶点的时间复杂度为O(216),有点大。但是可以发现dp(v,i)与dp(v,j)的区别仅在于前者拿au⊕dist(u,v)亦或上i⋅28,后者亦或上j⋅28。因此我们可以对一个顶点同时计算其相关的所有dp状态,用二叉树进行维护,这样时间复杂度就为O(8⋅28)。
总的时间复杂度为O(8⋅28n+29q)。
分块和分治
这里讲一类可以通过分块和分治解决的问题。考虑这样一个问题:
给你两个序列a1,…,an和b1,…,bn,要求你计算max1≤i≤j≤nbitcount(ai∧bj)。这里bitcount计算一个整数二进制中的1的数目,而∧表示二进制且运算。其中1≤n,ai,bi≤105=M
一般有i≤j这样的条件的问题,都需要用到分治算法,在计算时消除i≤j带来的复杂度。具体做法就是二分后,对左右两部分独立计算,计算完后再讨论左右两边对结果的影响。
一般问题到这里也就结束了,但是上面的问题的合并的过程中,我们需要借助到FWT算法,而FWT算法是和值域有关而与你数据量关系不大的。因此假如真的直接使用分治,算法时间复杂度将达到O(nMlog2M)。
我们这里可以将整个序列分成k块,每块大小为nk。对于每一块,我们用暴力算法计算。之后我们对这些块进行分治,而分治树上仅有O(k)个顶点,因此总的时间复杂度为O(n2k+kMlog2M)。
在最坏的情况下n=M,因此结果为O(n2k+knlog2n),取k=√nlog2n,此时结果为O(n√nlog2n)。