算法学习笔记(32): 分治

分治

分治,分而治之,是通过减少数据规模,然后合并的结果,从而减少复杂度的思想。

例如线段树,就是经典的分治思想的体现。

而本文所讲的分治,是几种经典的分治方法,或者说思想,其一般用于离线问题上。

分治的类型与适用范围

  • 序列分治

序列上的分治是最经典的分治,例如「COCI 2014.11.08」NORMA

  • CDQ 分治

CDQ 分治是经典的处理偏序问题的方法,其本质是对于时间的分治。

在序列的分治中,分治的区间之间不会互相影响,而 CDQ 就是用于处理分治区间有相互影响的情况。注意,需要保证修改操作对询问的贡献独立,修改操作相互不影响。

  • 线段树分治

这是一种将删除通过一个 \(O(\log n)\) 的代价变为撤销操作的技巧。

前提是我们可以知道每一个元素出现的时间,否则这个方法就不可行了。

  • 点分治/边分治

这是将序列分治扩展到树上了……见 # 算法学习笔记(36): 点分治,边分治小记

序列分治

序列分治的思想在于对于每一个点,求出经过这个点的那些区间的贡献。

为了不重不漏,需要钦定每一个点管辖的左右端点的区间。

考虑一般分治的过程,假定当前需要处理的区间为 \([l, r)\),中点 \(m = \lfloor \frac {l + r} 2 \rfloor\)

我们钦定中点管辖的区间为左端点在 \([l, mid)\),右端点在 \([mid, r)\) 的区间。

那么此时发现只剩下了两个端点都在 \([l, mid)\) 或者 \([mid, r)\) 的情况,于是变为数据规模减半的两个问题。

问题在于如何快速的算出中点所管辖的区间的贡献。

我们可以对于两侧 \(O(n)\) 或者 \(O(n \log n)\) 的预处理,然后 \(O(n)\) 或者 \(O(n \log n)\) 的合并信息。

具体来说,就是对于枚举每一个左端点,需要支持快速的求出其对于所有右端点所作的贡献。

总的时间复杂度可以为 \(T(n) = 2T(\frac n2) + O(n) \to T(n) = O(n \log n)\) 或者 \(T(n) = 2T(\frac n2) + O(n \log n) \to T(n) = O(n \log^2n)\)

对于询问的分治

或许看到这个标题会很疑惑,没关系,接下来会更加疑惑

首先,其经典形式为多次询问区间 \([l, r]\),每一次询问都可以通过 \(O(n \log n)\) 的序列分治来求解。

考虑到其实每一次分治中,对于同样的序列,分治结果是不会有变化的,我们就可以利用重复利用信息的思想将分治过程合并,从而优化算法。

例如被玩烂了的题:[NOIP2022] 比赛 - 洛谷

对于每一次询问,可以有一个非常简单的 \(O(n \log n)\) 的分治做法完成,于是可以得到 52 分的高分。

然而我们可以简单利用区间贡献不变的性质,使得重复利用某一个分治区间的信息。

然而问题在于如何处理不完整的区间的情况,这可以利用扫描线与线段树对于每一次分治新增 \(O(\log n)\) 的代价是的可以处理不完整区间的情况。

代码:提交记录 #661491 - Universal Online Judge


我们从一个一般化的问题开始,解决一个具体的问题

我们重新看待序列分治和将询问挂在分治区间上的操作,但是这次我们从点分治/边分治的角度看待。

序列上的分治其实就是链上的点分治,也就是说,能够用序列分治的题,部分可以拓展到树上,进行点分治。

而将询问挂在区间上的操作其实就是在点分树上进行询问,只是在序列上,点分树长得就是一棵线段树,也与分治树一样,所以可以直接挂在一棵线段树上。

于是如果我们从点分治和点分树的角度来理解将询问挂在区间上的操作,那么就会变得十分容易。


是否还有新的理解方式?

有的,这是我分享出来之后,SmallBasic 提出的理解方式。

将询问挂在区间上,本质是在对一个矩形进行 CDQ 分治。

对于某一个询问,实际上就是查询某个矩阵和,进行某个修改,实际上就是修改某个矩阵权值。

而将询问挂在分治区间上本质上做的就是这件事,只是可能比较抽象罢了。


然而可惜的是序列分治的题本来就少,使用这种套路复杂度至少是 \(O(n \log^2n)\) 起步,这使得这个方法的限制和上限不高,也就只是作为一个备用手段骗骗分,作为一个歪解罢了。

但是分治的思想,与二分/倍增同样的重要,是优化算法的一个重要手段。

有一段时间,我见到题第一个想法就是分治……不得不说,如果想出来了分治的做法,那么其实离复杂度更优秀的正解就近了。因为分治很可能用不到题目给出的所有性质,但是分治却很好的给出了一个简单的做法,我们只需要继续用其他信息进行优化即可。


猫树分治

猫树分治与序列分治何其相似!

一般的序列分治我们可能询问的是 \(\sum_{i = l}^r \sum_{j = i}^r f(l, r)\) 这种东西,而猫树分治询问的则是简简单单的某个区间 \([l, r]\),并且对于这个询问,我们可以利用线段做到 \(O(\log n f(n))\),其中 \(f(n)\) 指的是合并信息的复杂度。

不过有些时候,\(f(n)\) 比较大,是不允许我们在前面加上一个 \(\log n\) 的,也就是说我们只支持 \(O(f(n))\) 的查询的情况下,就可以用到猫树分治了。

不过有些时候,加入一个元素和合并两堆元素的复杂度严重不对等的情况也可以这么做,例如背包和线性基,不过我们这里看作是对等的,且都是 \(f(n)\)

具体来说,在线段树的每一次分裂成两个区间,我们维护其前后缀信息,这样就可以直接合并了。

一共有 \(O(\log n)\) 层,每层都是 \(O(n)\) 的,所以做前后缀的复杂度是 \(O(n f(n))\) 的,总复杂度则是 \(O(n \log n f(n) + q f(n))\),十分的优秀。

具体的应用大概就是:

用一个高级的说法来说,猫树分治可以对于每一个询问做到二区间合并,这是非常优秀的。


最值分治

他们说这是被 CF 出烂了的套路 QwQ

典例:AT_abc282_h [ABC282Ex] Min + Sum

最值分治一般与区间 \(\max, \min\) 挂钩,并且和 \(\sum_{l = 1}^n \sum_{r = l}^n\) 这种东西挂钩。

其具体过程为:

  • 进入分治区间 \([l, r]\),找到区间最值以及其所在位置 \(p\)
  • 计算所有左端点 \(\in [l, p]\),右端点 \(\in [p, r]\) 的区间的贡献
  • 递归进入分治区间 \([l, p - 1]\)\([p + 1, r]\)

由于这个算法在非随机数据\(p\) 可能分成一大一小,所以在计算贡献的时候有两种保证复杂度的做法:

  • 启发式合并,将小的那一边合并的大的那一边,并计算贡献。
  • 预处理和二分,枚举小的那边的端点,支持 \(O(\log n)\) 的计算对于整个左侧区间的贡献。

这样来说,对于第一种做法,(合并的分治的复杂度实际上是分开的)复杂度为 \(O(n \log n)\),对于第二种,复杂度为 \(O(n \log^2 n)\)

如果是随机数据,那么 \(O(len)\) 计算贡献复杂度期望是 \(O(n \log n)\) 的。


参考文献

posted @ 2023-10-27 22:12  jeefy  阅读(38)  评论(0编辑  收藏  举报