这个问题已经在这些比赛中发表过:
其解决方案(O(N 3 ) 算法)可以在此页面上找到: Bruce Merry 和 Jacob Steinhardt的“USACO DEC08 问题'围栏'分析”。
下面尝试解释这个算法。这也是我在 C++11 中对该算法的实现。此代码适用于 N<=250 和 0 .. 1023 范围内的整数坐标。没有三个点应该在同一条线上。这是生成满足这些要求的测试数据的Python 脚本。
简化问题的O(N 2 ) 算法
简化问题:找到形成凸多边形并包含给定集合的最左边点的最大点子集(或者如果有几个最左边的点,则该多边形应该包含其中的最上面的点)。
为了解决这个问题,我们可以通过有向线段连接每对点,然后(隐式)将每个线段视为一个图形节点,如图所示:
这里父节点和相应的段有蓝色。与其左子(红色)对应的线段从“父”线段的末端开始,它是相对于“父”线段方向左转最少的线段。与其右孩子(绿色)对应的线段从“父”线段的起点开始,也是相对于“父”线段方向左转最少的线段。
在最左边点结束的任何段对应于“叶”节点,即它没有子节点。图的这种构造保证了没有循环,换句话说,这个图是一个 DAG。
现在要找到点的最大凸子集,只需在该图中找到最长的路径即可。DAG 中最长的路径可以通过动态规划算法在 O(E) 时间内找到,其中 E 是图中的边数。由于每个节点只有 2 条出边,每条对应一对点,这个问题可以在 O(N 2 ) 时间内解决。
如果我们预处理输入数据,所有这些都可以实现,从同一点开始按角度对有向线段进行排序。这允许在图中执行深度优先遍历。我们应该记住从每个处理过的节点开始的最长路径,以便每个图边最多被访问一次,并且我们有 O(E) = O(N 2 ) 时间算法(暂时忽略预处理时间)。空间要求也是 O(N 2 ),因为我们需要存储每对点的排序方向,并且每个节点使用一个值(这也是一对点)。
这是此动态编程算法的 C++ 实现:
unsigned dpStep(ind_t i_left, ind_t from, ind_t to_ind)
{
ind_t child = sorted[from][to_ind];
while (child < i_left)
child = sorted[from][++to_ind];
if (child == i_left)
return 1;
if (auto v = memorize[from][child])
return v;
const auto x = max(dpStep(i_left, child, lefts[from][child]) + 1,
dpStep(i_left, from, static_cast<ind_t>(to_ind + 1)));
memorize[from][child] = static_cast<ind_t>(x);
return x;
}
输入参数是最左边的点和形成当前线段的一对点(其中线段的起点直接给出,而终点作为按角度数组排序的索引给出)。此代码片段中的“left”一词被稍微过度使用:它表示最左边的点 ( i_left
) 和包含图 ( ) 的左孩子的预处理数组lefts[][]
。
O(N 3 ) 算法
一般问题(最左边的点不固定)可以这样解决:
- 对左右方向的点进行排序
- 预处理这些点以获得两个数组:(a)每个点按相对于其他点的方向排序,(b)线段末端的第一个数组中的位置,使得相对于“父”段的方向最小可能左转。
- 使用每个点作为最左边的点,并使用“简化”算法找到最长的凸多边形。
- 定期修剪预处理数组中“最左边”点左侧的所有点。
- 取第 3 步中找到的最长路径。
第 4 步是可选的。它不会提高 O(N 3 ) 时间复杂度。但它通过排除不需要的点略微提高了 DP 算法的速度。在我的测试中,这提供了 33% 的速度提升。
预处理有几种选择。我们可以计算每个角度(用atan2
)并对它们进行排序,就像 Bruce Merry 和 Jacob Steinhardt 在示例代码中所做的那样。这是一种最简单的方法,但atan2
可能过于昂贵,尤其是对于较小的点集。
或者我们可以排除atan2
和排序切线而不是角度,就像在我的实现中所做的那样。这有点复杂但速度更快。
这两种选择都需要 O(N 2 log N) 时间(除非我们使用一些分布排序)。第三种选择是使用众所周知的计算几何方法(排列和对偶)来获得 O(N 2 ) 预处理。但我认为它对我们的问题没有用:由于常数因子大,对于较小的点集可能太慢了,对于较大的点集,它可能会提高一些速度,但太微不足道了,因为第 3 步将大大超过第 2 步。而且实施起来要困难得多。
其他一些结果:我尝试实现迭代 DP 而不是递归;这并没有明显改变速度。此外,我尝试一次执行两个 DP 搜索,将每个搜索的步骤交错(类似于在软件中实现的光纤或超线程);这可能会有所帮助,因为 DP 主要由具有高延迟和阻止 CPU 吞吐量的充分利用的内存访问组成;结果不是很令人印象深刻,只有大约 11% 的速度提升,很可能使用真正的超线程它会快得多。