动态凸包在窗口滑动更新下的处理与查询
一、特殊问题设置下的动态凸包处理
1.1 数据结构构建
在特殊问题设置中,假设存在一个分区线 $\ell$,将点集 $S$ 分为 $L$ 和 $R$ 两部分。设 $S_1 = S \cap L$,$S_2 = S \cap R$。
- 对于 $S_1$,使用列表 - 栈数据结构,即一个用于存储 $U(S_1)$ 顶点的双向链表和与 $S_1$ 中每个点关联的栈。
- 对于 $S_2$,仅使用双向链表来存储其凸包 $U(S_2)$ 的顶点。
- 同时,维护 $U(S_1)$ 和 $U(S_2)$ 的公共切线 $t_1t_2$,以及 $S$ 的最左和最右点。
1.2 输出凸包
利用上述数据结构,可以在 $O(|U(S)|)$ 时间内输出 $U(S)$,具体步骤如下:
1. 从 $S_1$ 的最左顶点开始,沿着右邻指针直到到达 $t_1$。
2. 输出 $t_1t_2$。
3. 从 $t_2$ 开始向右遍历 $U(S_2)$ 直到最右顶点。
1.3 插入操作
假设一个点 $q_j \in R$ 插入到 $S$ 中:
-
首次插入($j = 1$)
:
- 设置 $t_2 = q_1$。
- 通过从 $p_n$ 向左遍历 $U(S_1)$(即 Graham 扫描)找到 $t_1$,此操作耗时 $O(n)$,但在整个算法中仅发生一次,因此 $q_1$ 插入的分摊成本为 $O(1)$。
-
一般情况($j > 1$)
:
1. 使用 Graham 扫描更新 $U(S_2)$,此过程在所有 $n$ 次插入中总共耗时 $O(n)$,因此每次插入的分摊时间为 $O(1)$。
2. 设 $q$ 是使得 $qq_j$ 是新凸包 $U(S_2)$ 的边的顶点。
- 如果 $q$ 严格在 $t_2$ 的右侧,或者 $q = t_2$ 且 $t_1t_2$ 和 $t_2q_j$ 在 $t_2$ 处右转,则 $t_1t_2$ 仍然是公共切线,插入操作完成。
- 否则,更新 $t_2 = q_j$,并从当前 $t_1$ 向左遍历 $U(S_1)$ 找到新的 $t_1$,此过程称为插入类型切线搜索过程,耗时 $O(1 + k)$,其中 $k$ 是 $U(S_1)$ 中严格位于原始 $t_1$ 和新 $t_1$ 之间的顶点数。根据引理 1,每个点 $p \in L \cup R$ 在整个算法中最多参与一次插入类型切线搜索过程,因此该过程的分摊成本为 $O(1)$。
1.4 删除操作
假设一个点 $p_i \in L$ 从 $S_1$ 中删除:
-
最后一次删除($i = n$)
:只需使用 Graham 扫描维护 $U(S_2)$ 以进行未来的插入操作。
-
一般情况($i < n$)
:
1. 由于 $p_i$ 必须是当前凸包 $U(S_1)$ 的最左顶点,设 $p = r(p_i)$(即 $p$ 是 $U(S_1)$ 中 $p_i$ 的右邻),将 $p_i$ 从栈 $Q(p)$ 中弹出。
2. 如果 $p_i \neq t_1$,则 $p_i$ 严格在 $t_1$ 的左侧,$t_1t_2$ 仍然是新 $S_1$ 和 $S_2$ 的公共切线,删除操作完成。
3. 否则,通过同时从 $p$ 向左遍历 $U(S_1)$ 和从 $t_2$ 向左遍历 $U(S_2)$ 找到新的切线,此过程称为删除类型切线搜索过程,耗时 $O(1 + k_1 + k_2)$,其中 $k_1$ 是 $U(S_1)$ 中严格位于 $p$ 和新切点 $t_1$ 之间的点数,$k_2$ 是 $U(S_2)$ 中严格位于原始 $t_2$ 和新 $t_2$ 之间的点数。根据引理 2,每个点 $p \in L \cup R$ 在整个算法中最多参与一次删除类型切线搜索过程,因此该过程的分摊成本为 $O(1)$。
以下是特殊问题设置下插入和删除操作的流程图:
graph TD;
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{插入或删除?}:::decision;
B -->|插入| C{是否首次插入?}:::decision;
C -->|是| D(设置 t2 = q1, 从 pn 向左遍历 U(S1) 找 t1):::process;
C -->|否| E(使用 Graham 扫描更新 U(S2)):::process;
E --> F{q 是否在 t2 右侧或右转?}:::decision;
F -->|是| G(插入完成):::process;
F -->|否| H(更新 t2 = qj, 从当前 t1 向左遍历 U(S1) 找新 t1):::process;
H --> G;
B -->|删除| I{是否最后一次删除?}:::decision;
I -->|是| J(使用 Graham 扫描维护 U(S2)):::process;
I -->|否| K(弹出 pi 从栈 Q(p)):::process;
K --> L{pi 是否等于 t1?}:::decision;
L -->|否| M(删除完成):::process;
L -->|是| N(同时从 p 向左遍历 U(S1) 和从 t2 向左遍历 U(S2) 找新切线):::process;
N --> M;
D --> O([结束]):::startend;
G --> O;
J --> O;
M --> O;
1.5 特殊问题设置总结
在特殊问题设置中,每次插入和删除操作的分摊时间为 $O(1)$,并且每次更新后可以在 $|U(S)|$ 时间内输出上凸包 $U(S)$。
二、一般问题设置下的动态凸包处理
2.1 数据结构与初始设置
在一般问题设置中,不限制分区线 $\ell$ 的存在。在处理更新过程中,维护一条向右移动的垂直线 $\ell$,且 $\ell$ 始终包含 $S$ 中的一个点。设 $S_1$ 是 $S$ 中位于 $\ell$ 左侧(包括 $\ell$ 上的点)的子集,$S_2 = S \setminus S_1$。
- 对于 $S_1$,使用与特殊问题设置相同的列表 - 栈数据结构来维护 $U(S_1)$。
- 对于 $S_2$,仅使用双向链表来存储 $U(S_2)$ 的顶点。如果 $S_2 \neq \varnothing$,还维护 $U(S_1)$ 和 $U(S_2)$ 的公共切线 $t_1t_2$。
初始时,假设 $S$ 由两个点 $p_1$ 和 $p_2$ 组成,让 $\ell$ 穿过 $p_1$,则 $S_1 = {p_1}$,$S_2 = {p_2}$,$t_1 = p_1$,$t_2 = p_2$。
2.2 删除操作
假设一个点 $p_i$ 被删除:
-
$p_i$ 不是 $S_1$ 中唯一的点
:
- 如果 $p_i \neq t_1$,将 $p_i$ 从栈 $Q(p)$ 中弹出,其中 $p = r(p_i)$,不需要更新 $t_1t_2$。
- 如果 $p_i = t_1$,使用删除类型切线搜索过程更新 $t_1t_2$,根据引理 2(将 $L \cup R$ 替换为 $U$),该过程的分摊时间为 $O(1)$。
-
$p_i$ 是 $S_1$ 中唯一的点
:
1. 将 $\ell$ 移动到 $S_2$ 的最右点,新的 $S_1$ 包含旧 $S_2$ 中的所有点,新的 $S_2$ 为空。
2. 从 $S_1$ 的最右点向左运行 Graham 扫描构建 $S_1$ 的列表 - 栈数据结构,此过程称为左凸包构建过程,耗时 $|S_1|$。根据引理 3,每个点 $p \in U$ 在整个算法中最多参与一次左凸包构建过程,因此该过程的分摊成本为 $O(1)$。
2.3 插入操作
假设一个点 $p_j$ 被插入:
1. 使用 Graham 扫描更新 $U(S_2)$,此过程称为右凸包更新过程,耗时 $O(1 + k)$,其中 $k$ 是从旧 $U(S_2)$ 中移除的顶点数。根据标准 Graham 扫描,该过程的分摊成本为 $O(1)$。
2. 检查上切线 $t_1t_2$ 是否需要更新,如果需要(特别是插入前 $S_2 = \varnothing$),使用插入类型切线搜索过程找到新切线,根据引理 1(将 $L \cup R$ 替换为 $U$),该过程的分摊时间为 $O(1)$。
2.4 一般问题设置总结
在一般问题设置中,每次插入和删除操作的分摊时间为 $O(1)$,并且每次更新后可以在 $O(|U(S)|)$ 时间内输出上凸包 $U(S)$。
以下是一般问题设置下插入和删除操作的步骤总结表格:
|操作类型|情况|具体步骤|
| ---- | ---- | ---- |
|删除|$p_i$ 不是 $S_1$ 中唯一的点| - $p_i \neq t_1$:弹出 $p_i$ 从栈 $Q(p)$,不更新 $t_1t_2$
- $p_i = t_1$:使用删除类型切线搜索过程更新 $t_1t_2$|
|删除|$p_i$ 是 $S_1$ 中唯一的点|1. 移动 $\ell$ 到 $S_2$ 的最右点
2. 运行左凸包构建过程|
|插入| - |1. 使用 Graham 扫描更新 $U(S_2)$
2. 检查并更新上切线 $t_1t_2$|
三、支持对数时间凸包查询的数据结构增强
3.1 区间树的构建
为了支持对数时间的凸包查询,我们引入区间树。构建一个完全二叉搜索树 $T$,其叶子节点从左到右对应索引 $1$ 到 $|U|$,树的高度为 $O(\log |U|)$。
- 对于每个叶子节点,如果对应索引为 $i$,则将 $i$ 作为该叶子节点的索引。
- 对于每个内部节点 $v$,若 $i$ 是其左子树中最右叶子节点的索引,则将 $i + \frac{1}{2}$ 作为节点 $v$ 的索引(虽不是整数,但便于排序)。这样,树中所有节点的索引排序遵循节点的对称顺序。
对于连接 $U$ 中两点 $p_i$ 和 $p_j$ 的线段 $p_ip_j$,若节点 $v$ 的索引在区间 $[i, j]$ 内,则称线段 $p_ip_j$ 跨越节点 $v$。与基于点实际 $x$ 坐标定义的区间树不同,我们的树仅基于索引定义,更为抽象。
3.2 存储凸包到区间树
将算法维护的集合 $S$($S$ 是 $U$ 的子集)的上凸包 $U(S)$ 存储在 $T$ 中:对于 $U(S)$ 中连接两个顶点 $p_i$ 和 $p_j$ 的每条边,将 $p_ip_j$ 存储在 $T$ 中叶子节点 $i$ 和 $j$ 的最低公共祖先节点处(即 $p_ip_j$ 跨越的最高节点)。同样地,将 $S$ 的下凸包也存储在 $T$ 中,这样所有关于 $CH(S)$ 的查询(如定理 1 中指定的查询)可以在 $O(\log |U|)$ 时间内完成,主要思路是沿着从根到叶子的路径搜索,因为跨越节点 $v$ 的凸包边要么存储在 $v$ 处,要么存储在其祖先节点处,搜索过程中最多只需记住最接近 $v$ 的两条祖先边。
3.3 增强的数据结构
在第 4 节的数据结构基础上,进行如下增强:
- 初始时构建区间树 $T$,并在 $O(|U|)$ 时间内对 $T$ 进行预处理,使得任意两个节点的最低公共祖先可以在 $O(1)$ 时间内找到。
- 为 $T$ 中的每个节点 $v$ 关联一个初始为空的栈。对于 $U$ 中的任意两点 $p_i$ 和 $p_j$,用 $lca(p_i, p_j)$ 表示 $T$ 中对应这两点索引的叶子节点的最低公共祖先。
3.3.1 左凸包构建过程的变化
在计算 $S_1$ 的列表 - 栈数据结构的左凸包构建过程中:当处理顶点 $p_i$ 并将边 $(p_i, p_j)$ 添加到当前上凸包时,除了设置 $l(p_j) = p_i$,$r(p_i) = p_j$ 并将 $p_i$ 压入栈 $Q(p_j)$ 外,还将边 $(p_i, p_j)$ 压入 $T$ 中节点 $lca(p_i, p_j)$ 关联的栈中。由于最低公共祖先查询数据结构的 $O(1)$ 查询时间,此操作仅为原算法的每一步增加常数时间,不影响渐近时间复杂度。
3.3.2 右凸包更新过程的变化
在计算 $U(S_2)$ 的右凸包更新过程中:假设插入点 $p_j$,从右向左对 $U(S_2)$ 的顶点进行 Graham 扫描。当扫描到边 $pp’$ 并发现需要从当前上凸包中移除时,将 $pp’$ 从 $T$ 中节点 $lca(p, p’)$ 关联的栈中弹出。扫描完成后,设 $p_k$ 是新凸包中与 $p_j$ 相连的顶点(即 $p_kp_j$ 是新边),将 $p_kp_j$ 压入 $T$ 中节点 $lca(p_k, p_j)$ 关联的栈中。同样,这些变化不改变算法的渐近时间复杂度。
3.3.3 存储公共切线
将公共切线 $t_1t_2$ 存储在 $T$ 中节点 $lca(t_1, t_2)$ 关联的栈顶。
3.4 插入和删除操作的处理
3.4.1 删除操作
-
$p_i$ 不是 $S_1$ 中唯一的点
:
- 将公共切线 $t_1t_2$ 从 $T$ 中节点 $lca(t_1, t_2)$ 关联的栈中弹出。
- 当将 $p_i$ 从栈 $Q(p)$($p = r(p_i)$)中弹出时,将边 $p_ip$ 从 $T$ 中节点 $lca(p_i, p)$ 关联的栈中弹出。
- 将新切线 $(t_1, t_2)$ 压入 $T$ 中新的 $lca(t_1, t_2)$ 节点关联的栈中。
-
$p_i$ 是 $S_1$ 中唯一的点
:
- 对于 $U(S_2)$ 中的每条边 $p_kp_k’$,将其从 $T$ 中节点 $lca(p_k, p_k’)$ 关联的栈中弹出。
- 执行带有上述变化的左凸包构建过程。
3.4.2 插入操作
- 将切线 $t_1t_2$ 从 $T$ 中节点 $lca(t_1, t_2)$ 关联的栈中弹出。
- 执行带有上述变化的右凸包更新过程。
- 将新切线 $t_1t_2$(可能与原切线相同)压入 $T$ 中节点 $lca(t_1, t_2)$ 关联的栈中。
由于最低公共祖先数据结构的 $O(1)$ 查询时间,每次插入/删除操作的分摊时间仍然为 $O(1)$。
3.5 凸包查询的处理
我们的区间树 $T$ 与文献 [15] 中使用的区间树的不同之处在于,$T$ 的节点关联的栈中可能存储不在 $U(S)$ 上的边,因此不能直接使用文献 [15] 中的查询算法。为确保栈中的非凸包边不会影响查询,需要先证明以下引理(进而得到推论 1)。
以下是增强数据结构下插入和删除操作的流程图:
graph TD;
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{插入或删除?}:::decision;
B -->|插入| C(弹出 t1t2 从 lca(t1, t2) 关联的栈):::process;
C --> D(执行右凸包更新过程):::process;
D --> E(压入新 t1t2 到 lca(t1, t2) 关联的栈):::process;
E --> F([结束]):::startend;
B -->|删除| G{pi 是否是 S1 中唯一的点?}:::decision;
G -->|否| H(弹出 t1t2 从 lca(t1, t2) 关联的栈):::process;
H --> I(弹出 pi 从栈 Q(p), 弹出 pip 从 lca(pi, p) 关联的栈):::process;
I --> J(压入新切线到 lca(t1, t2) 关联的栈):::process;
J --> F;
G -->|是| K(弹出 U(S2) 所有边从对应 lca 关联的栈):::process;
K --> L(执行左凸包构建过程):::process;
L --> F;
3.6 总结
通过引入区间树并对数据结构进行增强,我们可以在一般问题设置下,在每次插入和删除操作的分摊时间为 $O(1)$ 的情况下,支持 $O(\log |U|)$ 时间的凸包查询。后续可以进一步去除 $|U|$ 大小已知的假设,并将查询时间降低到 $O(\log n)$($n = |S|$)。
整个动态凸包处理与查询算法的操作总结表格如下:
|操作场景|操作类型|具体步骤|分摊时间|输出时间|
| ---- | ---- | ---- | ---- | ---- |
|特殊问题设置|插入| - 首次插入:设置 $t_2 = q_1$,从 $p_n$ 向左遍历 $U(S_1)$ 找 $t_1$
- 一般情况:更新 $U(S_2)$,根据 $q$ 位置更新切线|$O(1)$|$|U(S)|$|
|特殊问题设置|删除| - 最后一次删除:维护 $U(S_2)$
- 一般情况:弹出 $p_i$,根据 $p_i$ 与 $t_1$ 关系更新切线|$O(1)$|$|U(S)|$|
|一般问题设置|插入|1. 更新 $U(S_2)$
2. 检查并更新上切线 $t_1t_2$|$O(1)$|$O(|U(S)|)$|
|一般问题设置|删除| - $p_i$ 非唯一:根据 $p_i$ 与 $t_1$ 关系更新切线
- $p_i$ 唯一:移动 $\ell$,执行左凸包构建过程|$O(1)$|$O(|U(S)|)$|
|增强数据结构|插入|1. 弹出 $t_1t_2$
2. 执行右凸包更新过程
3. 压入新 $t_1t_2$|$O(1)$| - |
|增强数据结构|删除| - $p_i$ 非唯一:弹出 $t_1t_2$、$p_i$ 和 $p_ip$,压入新切线
- $p_i$ 唯一:弹出 $U(S_2)$ 边,执行左凸包构建过程|$O(1)$| - |
|增强数据结构|查询|沿着从根到叶子的路径搜索区间树 $T$|$O(\log |U|)$| - |
综上所述,我们通过逐步优化数据结构和算法,实现了在窗口滑动更新下高效维护凸包并支持快速查询的目标。
超级会员免费看
383

被折叠的 条评论
为什么被折叠?



