作者:2007.10.11 文档类型/出处:NOI专刊 | 时间:
题目简述: 給出一个平面点集S,求一个面积最小的矩形使其包含S所有的点。 预备知识: 在求解这道题之前我们先要了解一些关于凸包的知识。 什么是凸包?简单地说,对于一个平面点集S,我们把完全包含该点集的最小的凸多边形叫做点集S的凸包H。 凸包一个很重要的性质就是它“凸”的性质。这个性质对我们理解和计算凸包都有很大的帮助。 I) 对点集S中任意一点a,当且仅当存在直线p过a点并使得S中除a外所有点均在p的一侧,则a为凸包上的一顶点。 II) 对点集S中任意两点a,b,当且仅当S中除a,b以外所有点都在过点a,b的直线p的一侧,则线段ab为凸包上的一条边。 III) 对点集S中任意四点a,b,c,d,当d在三角形abc中(包括边),则d不是凸包上的点。
上面的几条关于凸包“凸”的性质为我们计算凸包提供了一个基础。这里我们将介绍两种简单且被广泛运用的算法――Gift-Wrapping和Graham-Scan算法。
Gift-Wrapping算法: 通过性质(I),我们可以找到一个特殊点,如具有最小y坐标且x坐标尽可能小的点。将它作为计算凸包的第一个顶点。确定了起点后,我们就可以通过Gift-Wrapping算法计算出点集的凸包。下面的步骤很直观的描述了这个算法: 1) 把点集中所有点都看成是固定在平面上的柱子,想象我们在起始点柱子上系上一根身子。 2) 把绳子沿水平方向向右拉直,并逆时针旋转,当绳子碰上一根柱子,则对应了凸包上的一点 3) 继续旋转绳子,每次确定一个凸包上的顶点,直至绳子回到起点。 图一:Gift-Wrapping算法计算凸包的过程 每次通过旋转绳子找到下一个凸包顶点需要对点集中所有剩余点进行一次比较,所以这 一步的时间复杂度是O(n)。每个凸包上的顶点都需要进行一次旋转操作,而最坏情况下,凸包顶点个数可以和点集个数相等,所以整个Gift-Wrapping算法的时间复杂度是O(n2)的。
Graham-Scan算法: Gift-Wrapping算法无论从理解还是从实现上来说,它都是十分简单的。但由于它的复杂度并不理想,我们无法利用它来求解大规模的凸包问题。因而,我们将介绍一种高效的计算凸包的算法――Graham-Scan。 Graham-Scan算法主要可分成两部分: 1) 同Gift-Wrapping一样,需要先找出一个起始点。将这个点作为原点,进行夹角排序。 2) 先将起始点压入堆栈H中,再按照已经排好的顺序对每一个点进行扫描,同时维护堆栈H。这个堆栈表示的是到目前为止,所有已经扫描过的点对应的凸包。每当扫描一个点p的时候: a) 如果堆栈的元素少2个或者堆栈顶端的两个点与p构成左转关系,则将p压入堆栈中。 b) 否则,栈顶元素出栈并继续进行a的判断。 当所有点都扫描完后,堆栈H即为我们要求的凸包。 图二:Graham-Scan算法的扫描过程(堆栈H储存的即实线连接起来的点) 分 析Graham-Scan的复杂度:第一步中找出起点并进行极角排序的复杂度是 O(n log n)。第二步中每一个点仅会被扫描一次并相应维护一次堆栈H 。而维护堆栈过程中每次访问堆栈H中的点,要么这个点被删除,要么就停止堆栈的维护,所以所有堆栈维护加起来最多只访问了2n次。故这部分的复杂度是 O(n)。综合起来,Graham-Scan算法的时间复杂度是O(n log n)的。 算法分析: 现在考虑这道题目,题目要我们求出一个最小面积的矩形能够覆盖给定的所有点。易知矩形覆盖所有点当且仅当它覆盖这些点的凸包。故而,问题可以转化为对于一个凸包,求出一个面积最小的矩形来覆盖它。 那么这个覆盖凸包的最小矩形有什么性质呢? 首先,这个矩形的四条边上必然都有凸包的顶点。这个很容易想清楚,如果矩形的某条边没有碰上凸包的顶点,那么我们一定能把这条边向里压,从而得到一个更小的满足条件的矩形。 其次,这个矩形至少有一条边与凸包上的一边重合。这个性质不容易直观地想清楚,需要书面证明一下。由于完整的证明需要分成很多情况来讨论,比较繁琐,所以这里仅选取其中的一种情况来证明,其他情况可以类似地进行证明。 利 用反证法,我们假设覆盖凸包的最小矩形所有边都没有和凸包的边有重合,也就是说,最小矩形的每条边上仅有一个凸包的顶点。如图三所示,矩形ABCD是覆盖 凸包的最小矩形,M、N、P、Q为凸包在矩形四条边上的顶点。我们分别作MM’⊥ CD,NN’⊥ AD。则矩形ABCD的面积S = MP×Cos(∠PMM’)×NQ×Cos(∠QNN’)。我们将矩形旋转X度(顺时针为正,逆时针为负),仍使矩形覆盖凸包且M、N、P、Q分别在它的 四边上。则此时新矩形的面积S = MP×Cos(∠PMM’+ X)×NQ×Cos(∠QNN’- X) 。我们仅需考虑Cos(∠PMM’+ X)×Cos(∠QNN’- X)的单调性。 Cos(∠PMM’+ X)×Cos(∠QNN’- X) = 1/2[Cos(∠PMM’+ X + ∠QNN’- X) + Cos(∠PMM’+ X - ∠QNN’+ X)] = 1/2[Cos(∠PMM’+ ∠QNN’) + Cos(∠PMM’- ∠QNN’+ 2X)] ∵0≤∠PMM’< π/2 , 0≤∠QNN’< π/2 ∴-π/2 <∠PMM’- ∠QNN’< π/2 ∴Cos(∠PMM’- ∠QNN’)不可能取到最小值 ∴x在0左边的一个区间中f(x) = Cos(∠PMM’- ∠QNN’+ 2X)递增,或x在0右边一个区间中f(x) = Cos(∠PMM’- ∠QNN’+ 2X)递减。 因而,对于这样的矩形,我们总可以顺时针或逆时针旋转一个小角度,从而获得一个更小的矩形,这与假设矛盾。故最小矩形至少有一条边与凸包一边重合。 了解到最小矩形所具有的这两个性质后,我们就能够很容易的想到一种算法,枚举凸包上哪条边与矩形的边重合,再找出在这条直线投影的正负方向上最远的和到直线距离最远的三点,从而确定和计算出矩形的面积,最后选取最小值,即为覆盖凸包的最小矩形的面积。 我们用最朴素的方法去实现它,枚举每条边后再把剩余的点都扫描一遍,来找出另外三点,计算出矩形的面积。这样做时间复杂度是O(n2)得。就本题来说已经可以接受了。但如果规模再大一点,怎么办呢?我们能不能做得更好呢? 答案是能!我们还有一个很重要的信息没有利用到,对凸包上任意一条边,依次计算出凸包顶点到它的距离或投影距离,构成的序列总是一个先增再降的。同时,注意到如果逆时针顺序枚举重合的边时,每次找出来的另外三点也总是在向逆时针方向移动。 由此,我们就得到了一个更加高效的算法。枚举过程中,逆时针旋转到下一条边后不需要再重新扫描所有点,只要分别从上一条边确定的三点出发,向后比较,找到最大值,来更新这三个点即可。 在枚举过程中,三个点的指针都只会对每个顶点访问一次,所以这个过程的平摊复杂度是O(n)的。结合前面计算凸包的过程,在O(n log n)的时间内我们就能够圆满地解决这题了。 了解到最小矩形所具有的这两个性质后,我们就能够很容易的想到一种算法,枚举凸包上哪条边与矩形的边重合,再找出在这条直线投影的正负方向上最远的和到直线距离最远的三点,从而确定和计算出矩形的面积,最后选取最小值,即为覆盖凸包的最小矩形的面积。 |