参考自 APIO2022 清华大学李欣隆的课件 《范围修改查询问题》。
其实感觉目前实用性不强(
问题描述
给定集合 \(I\),令 \(n=|I|\)。给定交换半群 \((D,+)\),半群 \((M,*)\)。
这里 \(I\) 就是要维护的点集。下述的 “范围” 指的就是 \(I\) 的一个子集。
\(D\) 对应范围维护的信息,\(+\) 对应范围信息的合并。\(M\) 对应标记的信息,\(*\) 对应标记信息的合并。
注意二者是可能不同类的,所以才用两个群来表示。例如询问是区间 \(\max\) 而修改是区间加(这里只是运算符的不同,但实际上 \(D,M\) 也是可能不同的,比如修改是区间翻转,只不过一般 \(D,M\) 都能用数来表示罢了)。
交换半群意味着运算满足交换律和结合律,半群意味着运算只需满足结合律。即我们能保证我们的算法将按顺序执行标记结合操作。而对于范围的信息,一般询问的时候都是不要求顺序的,即运算满足交换律。
给定二元运算符 \(*:D\times M\to D\)。
即将修改作用于范围的信息上。这里为了直观和方便,重复使用了记号 \(*\)。
规定满足:
-
结合律:\(\forall_{d\in D,m_1,m_2\in M},(d*m_1)*m_2=d*(m_1*m_2)\)。
即,我们可以先把标记合并,再把它们作用到范围的信息上。
其实,用于标记合并的运算符 \(*:M\times M\to M\) 已经说明了这一点(不然规定这个运算符就没用了)。
-
分配律:\(\forall_{d_1,d_2\in D,m\in M},(d_1+d_2)*m=d_1*m+d_2*m\)。
即,我们可以把标记分别作用到子范围的信息上,再合并得到新的范围信息。
一个反例是,将区间内的数全部变成该区间的最大值(当然你可以先询问区间最大值,再把操作变为区间置数)。
显然根据上述定义,\((D,+)\) 和 \((M,*)\) 中都不需要有幺元和逆元。但为了方便,我们可以假设 \((M,*)\) 存在幺元 \(e_M\),它代表标记 “不需要修改”。
给定 \(d_0:I\to D\)。即每个点的初始权值。
按顺序执行 \(m\) 个操作。第 \(t\) 个操作为下列两种中的一种:
-
范围修改:给出 \(q_t\subseteq I\),和 \(f_t\in M\)。
定义 \(d_t:I\to D\) 满足:对于任意 \(x\in I\),若 \(x\in q_t\) 则 \(d_t(x):=d_{t-1}(x)*f_t\);否则 \(d_t(x):=d_{t-1}(x)\)。
-
范围查询:给出 \(q_t\subseteq I\)。
定义 \(d_t:I\to D\) 满足:对于任意 \(x\in I\) 有 \(d_t(x):=d_{t-1}(x)\)。
需要返回 \(\sum_{x\in q_t}d_t(x)\)。
\(d_0\) 和所有操作均离线给出。不过也可以一定程度上半在线:只需保证 \(q_t\) 是离线给出的即可。
离线最优范围修改查询算法
首先规定离线的意义是我们知道了每次操作的范围。一个例子是:对于序列上的区间修改查询,假设用线段树维护。那么在线的话时间复杂度为 \(O(n+m\log n)\),但离线的话可以先把所有点离散化合并后再建线段树,此时可以做到 \(O(n+m\log m)\)。实际的算法有用到该例子的类似思想,但还具有其他差别。
我们将在算法过程中穿插在具体问题上的对应解释,这里的具体问题我们选择为:在二维平面上的半平面离线修改查询问题。
先考虑在操作序列上设计一个分治算法。
对于 \(x,y\in I\),定义等价关系 \(R_{l,r}(x,y)\),它为真当且仅当 \(\forall_{i\in [l,r]},x\in q_i\iff y\in q_i\)。
具体问题上,\(q_l,\cdots,q_r\) 这些半平面将整个二维平面划分为很多个区域,而 \(R_{l,r}(x,y)\) 就代表 \(x,y\) 这两个点在同一个区域内。
若对于任意 \(x,y\in I\) 有 \(R_{1}(x,y)\implies R_{2}(x,y)\),那么我们记为 \(R_{2}\subseteq R_{1}\)。显然对于 \([l',r']\subseteq [l,r]\) 有 \(R_{l',r'}\subseteq R_{l,r}\)。
可将 \(R_{2}\subseteq R_{1}\) 理解为 \(R_1\) 在 \(R_2\) 的基础上将所有点集划分为更细的等价类。
定义等价关系 \(R_0(x,y)\) 使得任意 \(I\) 中不同的点都在不同等价类内。显然对于任意等价类 \(R\) 都有 \(R\subseteq R_0\)。
为了支持动态范围的修改和查询,我们维护一个动态有根森林 \(T\)。其叶子对应 \(I\) 中的点,且满足两个叶子在同一个子树中,当且仅当在当前等价关系下它们属于同一个等价类。
初始时 \(T\) 中每个叶子都是单独的一棵树,这对应等价关系 \(R_0\)。
假设当前 \(T\) 对应于 \(R_1\),给出 \(R_2\) 满足 \(R_2\subseteq R_1\)。考虑将 \(T\) 从 \(R_1\) 移动到 \(R_2\),这对应 \(T\) 中一些有根树的合并,那么对于一些要合并到一起的有根树,我们可以新建一个根并把它作为这些有根树的父亲。同样地,若 \(T\) 对应于 \(R_2\),那么我们可以撤销一些合并操作使得 \(T\) 回到 \(R_1\) 状态。
对于点 \(x\in T\),我们维护两个变量 \(s_x\in D,t_x\in M\)。
使得,对于任意点 \(x\in T\),假设 \((x_1,\cdots,x_k=x)\) 是当前 \(x\) 所在树的根到 \(x\) 路径上的点序列,那么当前 \(x\) 子树内所有叶子的信息和为 \(S_x=s_x*t_{x_{k-1}}*t_{x_{k-2}}*\cdots*t_{x_1}\)。从而若 \(x\) 为树的根,有 \(S_x=s_x\)。
这里 \(t_x\) 其实就是懒标记,而 \(s_x\) 是未完全下传懒标记时 \(x\) 记录的子树信息,\(S_x\) 是完全下传懒标记后 \(x\) 真正的子树信息。
当我们对某棵以 \(x\) 为根的树(注意非“子树”)进行所有叶子信息和查询的时候,直接返回 \(s_x\) 即可。
当我们对某棵以 \(x\) 为根的树进行所有叶子修改 \(y\in M\) 的时候,直接将 \(s_x,t_x\) 都乘上 \(y\) 即可。
当我们新建节点 \(z\) 作为根 \((x_1,\cdots,x_k)\) 的父亲的时候,置 \(s_z:=\sum_{i=1}^k s_{x_k},t_z=e_M\) 即可。
当我们将根 \(z\) 删除的时候,假设其儿子分别为 \((x_1,\cdots,x_k)\),那么对于每个 \(i\in [1,k]\),将 \(s_{x_i},t_{x_i}\) 都乘上 \(t_{z}\) 即可。
那么我们对 \([l,r]\) 的分治算法为:假设当前 \(T\) 对应的等价关系为 \(R_{l,r}\),对应的信息为 \(d_{l-1}\),我们欲处理完 \(q_l,\cdots,q_r\),使得 \(T\) 对应的信息为 \(d_r\),并仍使其对应的等价关系为 \(R_{l,r}\)。那么我们先将 \(T\) 从 \(R_{l,r}\) 移动到 \(R_{l,mid}\),然后对于 \([l,mid]\) 分治,然后再将 \(T\) 从 \(R_{l,mid}\) 撤销回 \(R_{l,r}\),然后对于右半边同样操作。当 \(l=r\) 时 \(T\) 中只有两棵树,且恰有一棵树对应的范围是我们要修改/查询的范围,那么直接按上面说的方法操作即可。
定义 \(F(k)\) 表示 \(k\) 个操作最多能将点集划分为多少个等价类。
我们考虑刚刚说的分治算法在代数结构上的操作次数(在 \((D,+),(M,*)\) 上的运算次数)的量级。设最开始从 \([L,R]\) 开始分治,且 \(F(K)\leq n\),其中 \(K=R-L+1\)。
代数结构上的操作次数与每次 \(T\) 从 \(R_{l,r}\) 分别移动到 \(R_{l,mid}/R_{mid+1,r}\) 时所减少的等价类个数有关(撤销回来所需的操作次数是一样多的,所以我们只考虑向下移动时的操作次数)。我们有一个估计是:
\[T_1(k)=2T_1(k/2)+O(F(k)) \]这里 \(O(F(k))\) 等价于 \(R_{l,r}\) 的等价类个数。这看起来估得并不太紧,因为我只会把 \(R_{l,r}\) 中的等价类部分而不是全部合并。而且从分治树上根到每个叶子路径上每条边对应的操作次数的总和应当有一个 \(F(K)\) 的上界。
但其实若我们考虑一下最坏情况,可以发现这个估计是紧的。由于每个叶子到根路径上的边的操作次数之和至多为 \(F(K)\),那么我们肯定要把更多的操作次数尽量地往更底层调。那么应当构造为从 \(R_{l,r}\) 移动到 \(R_{l,mid}/R_{mid+1,r}\) 时等价类的个数恰从 \(F(k)\) 减少到 \(F(k/2)\),其中 \(k=r-l+1\):这是减少量的下界,且恰好满足叶子到根路径上所有减少量加起来等于 \(F(K)\)。那么分治树上一条边的贡献为 \(F(k)-F(k/2)\),发现这等价于每个点都有 \(2F(k)-F(k)=F(k)\) 的贡献。于是最坏情况下恰好满足 \(T_1(k)=2T_1(k/2)+O(F(k))\)。
现在来介绍完整的算法。定义 \(G(n)\) 表示最小的 \(k\) 使得 \(F(k)\geq n\),即最少的操作次数将点集划分为至少 \(n\) 个等价类。
我们不妨假设 \(R_0=R_{1,m}\)。否则我们先将 \(R_0\) 移动到 \(R_{1,m}\)(这和一开始讲的那个例子类似,我们先在算法之前根据操作范围对所有的点离散化)。设此时 \(R_{1,m}\) 划分出 \(N\) 个等价类,那么 \(N\leq \min(n,F(m))\)。
然后我们将整个长度为 \(m\) 的操作序列按 \(G(N)\) 分块。对于每个块 \([L,R]\),我们先从 \(R_0\) 移动到 \(R_{L,R}\),然后使用刚刚提到的分治算法,然后再将 \(R_{L,R}\) 撤销回 \(R_0\)。
现在考虑计算完整算法在代数结构上的操作次数量级。最坏情况下显然是 \(R_0=R_{L,R}\),依据这个写出操作次数上界(紧界):
\[\begin{aligned} T_2(n,m)&=O(n)+\frac{m}{G(N)}T_1(G(N))\\ T_1(k)&=2T_1(k/2)+O(F(k)) \end{aligned} \]可以证明,该算法在代数结构运算次数上是渐进最优的。当然有时 \((D,+),(M,*)\) 会有其他特殊性质。
于是在该结论的基础上,我们可以得到不同范围上的修改查询问题的代数结构运算次数下界(这也是时间复杂度的下界):
-
序列区间、树上简单路径:\(F(k)=O(k)\),\(T_2(n,m)=O(n+m\log\min(n,m))\)。
-
半平面、矩形、多边形、圆、椭圆、双曲线等:\(F(k)=O(k^2)\),\(T_2(n,m)=O(n+m\min(m,\sqrt n))\)。
-
\(c\) 维半空间、单纯形、球、椭球等:\(F(k)=O(k^c)\),\(T_2(n,m)=O(n+m\min(m^{c-1},n^{1-1/c}))\)。
这有利于我们判定一些问题的时间复杂度,而避免一些不必要的思考。
等价关系的转移
我们最优化了算法在代数结构上的操作次数,但我们并未对算法的时间复杂度说明过什么。
事实上,将该算法套用到具体问题上真正的难点就是如何实现 \(R_{l,r}\) 到 \(R_{l,mid}\) 等价类的转移。
若能将这个过程实现到 \(O(F(k))\),即 \(R_{l,r}\) 的等价类个数,那么就能使时间复杂度和代数结构上的运算次数同阶。
一般来说怎么实现要具体问题具体分析。比如若 \(R_{l,r}\) 中的等价类是序列上的区间,那么容易在 \(O(F(k))\) 的时间内从 \(R_{l,r}\) 转移到 \(R_{l,mid}\)。
有一种较为通用的做法是哈希。我们先给 \(R_{l,r}\) 中每个等价类中找一个代表元出来。为找到 \(R_{l,mid}\) 对应的等价类,依次考虑 \(q_l,\cdots,q_r\),每次将 \(q_i\) 范围中的代表元都加上同一个随机数。那么两个点在同一个等价类内就可以等价于它们的哈希值相等。
但转化后的问题和原来的问题其实有点类似:我们要实现 \(O(k)\) 次范围加,但是查询变为了输出所有 \(O(F(k))\) 个点的值,而且是在最后进行。相比于原来的问题,相当于去掉了时间维度和范围查询要求。相当于我们还是要在一个类似原问题但新增两个性质的新问题上,找到一个至少比 \(O(F(k)\cdot k)\) 优的算法,否则原问题的时间将退化为 \(O(nm)\) 的暴力。
所以说为保证算法的时间复杂度,该算法并不是万能的,它还是需要结合具体问题具体分析。例如,对于半平面范围,我们可以用平面图点定位(扫描线)的方法实现这个新问题,做到 \(O((k^2+F(k))\log k)=O(F(k)\log k)\)。
具体范围的划分树
对于一些具体范围,人们找到了该范围上的最优点集划分树,使得在 \(n\leq F(m)\) 的前提下,利用最优点集划分树,也能做到离线最优算法所证明的下界,从而找到了此前提下该范围问题的在线最优方法,且方便了代码实现。
不同范围的最优点集划分树形态非常不同。我们列举一些如下:
-
序列区间:线段树。
-
树上简单路径:全局平衡二叉树、top tree、点分树等。
-
二维平面矩形、高维正交范围、高维满点集正交范围:KDT。
注意,根据前一节的结论,可以发现,即使我们是在高维满点集(即 \(I\) 为某正交范围内的所有点)的基础上进行范围修改查询,其最优时间复杂度下界和散点集是一样的,而且都能通过 KDT 达到这个下界。
-
二维半平面:Optimal Partition Tree,但常数较大且难以实现。
注意,二维半平面作为范围的情况下,KDT 的时间复杂度是错的。具体卡的方法是:随机一条斜着的直线,并沿着这条直线两侧铺满点,那么对于该直线对应半平面的操作,我们需要在 KDT 上递归到每一个叶子进行修改,因为此时不太可能能用一个矩形框住直线同一侧的两个点且该矩形不穿过直线。但是在随机数据的情况下 KDT 的复杂度是对的。
除了难以实现的 Optimal Partition Tree 之外,还有一些针对二维半平面的点集划分树,但它们都不能达到复杂度下界:
-
四分树:每次通过两条相交的斜线将点集均分为 4 个部分。那么一次半平面操作只会递归进 4 个儿子中的 3 个:因为半平面所对应的那条直线至多只会经过 4 个部分中的 3 个部分,于是有一个部分要么完全在半平面内,要么完全在半平面外,可以直接处理。
时间复杂度为 \(O(n+m n^{1/\log_{3}4})=O(n+mn^{0.79})\)。
-
六分树:和四分树类似,只不过每次通过三条交于一点的斜线将点集均分为 6 个部分,一次半平面操作只会递归进 6 个儿子中的 4 个。
时间复杂度为 \(O(n+mn^{1/\log_46})=O(n+mn^{0.77})\)。
-
-
高维单纯形:Optimal Partition Tree。
-
圆范围:可以转为三维半空间(见 PPT)再用 Optimal Partition Tree,但不是最优复杂度。
二维半平面范围查询问题-在线算法
上面我们说到,对于二维半平面操作还没有较好的具体范围划分树用于在线问题。但我们介绍一种不用划分树的对于二维半平面范围的在线方法,但这种方法只针对于只有查询的问题。
考虑将点集随意划分为大小为 \(B\) 的块。由于两个形式不同的半平面可能实际上对应了同一个点集,所以这里我们规定半平面的标准型为:将询问的半平面一直向下平移,直到碰到第一个点,然后再将询问的半平面绕这个点沿某一方向旋转,直到再碰到一个点。于是得到的半平面能用两个原来点集中的点来表示。
于是对于一个大小为 \(B\) 的块,只有至多 \(O(B^2)\) 个半平面标准型,尽管它们之中还是可能有对应相同点集的。
考虑对这 \(O(B^2)\) 个半平面标准型预处理出答案。我们已经知道每个半平面标准型是由连接 \(B\) 个点中某两点的直线确定的了。考虑枚举其中一个点 \(u\),然后将其他所有点 \(v\) 按射线 \((u,v)\) 的极角排序,然后再通过双指针来遍历所有可能半平面标准型。那么我们可以在 \(O(B^2\log B)\) 的时间复杂度内对一个块预处理。总预处理时间复杂度为 \(O(\frac{n}{B}B^2\log B)=O(nB\log B)\)。
对于一次询问,我们遍历每一个块,然后在 \(O(\log B)\) 的时间复杂度内找到在该块内对应的半平面标准型,然后直接用我们预处理的答案。于是,我们可以在 \(O(q\frac{n}{B}\log B)\) 的时间内在线回答所有询问。
//这里还不太搞得懂是怎样 \(O(\log B)\) 找到半平面标准型的。感觉需要询问和点集反演,那这样不如在块内直接把原问题整个反演了。
取 \(B=\sqrt q\) 最优,时间复杂度 \(O(n\sqrt q\log \sqrt{q})\),空间复杂度 \(O(n\sqrt q)\),代数结构运算次数 \(O(n\sqrt q)\)。
标签:复杂度,笔记,查询,修改,算法,等价,平面,我们,范围 From: https://www.cnblogs.com/ez-lcw/p/16843021.html