题目来源:http://poj.org/problem?id=1113
题目大意:
如图所示,给定N个顶点构成的一个多边形和一个距离值L。建立一个围墙,把这个多边形完全包含在内,且围墙距离多边形任一点的距离不超过指定的距离L。求出满足条件的围墙长度最小值。
输入:第一行为N和L。3 <= N <= 1000, 1 <= L <= 1000。接下来N行每行两个整数代表一个点的坐标,(-10000 <= Xi, Yi <= 10000) ,每个点都不重合,输入保证了多边形的每条边不会相交,顶点顺序为沿多边形顺时针方向遍历顺序。
输出:满足题述要求的最小长度,保留精度到整数位。PS:结果四舍五入即可。
Sample Input
9 100 200 400 300 400 300 300 400 300 400 400 500 400 500 200 350 200 200 200
Sample Output
1628
最近恰好碰到了凸包相关的问题,就翻出来这道最基础的给做了。
首先要想清楚的是题中给出的不一定是凸多边形,假设是凹多边形,当我们把围墙也设为像凹多边形的形状时可以使得围墙距离恰好满足限制,但这不是我们需要的最优解情况,因为此时的围墙长度不是最短的。围墙最短的情况对应的应该是:对所有顶点求出其凸包(直观简单的理解就是一个最小的凸多边形把所有的点都包含在其内部),然后把凸包往外扩距离L,这样得到的还不是最短的,还需要在原来凸包的各顶点处画一个半径为L的圆,然后与凸包外扩后的边与切点处相连(相当于把角给磨圆了?),这样最终组成的形状才是满足要求的最短围墙。
有了上述思路后,我们就能明白围墙的最短长度应该等于 凸包的边长 + 半径L的圆的周长。因为根据上面的分析,我们在纸上画一画就可以发现围墙到凸包之间的部分实际上是由一些矩形和一些扇形组成的,这些扇形恰好会组成一个圆。
然后剩下的问题就是如何求顶点集合的凸包了。
对于凸包有两种形象的理解:
1. 把点看做是钉在板子上的钉子,用一个橡皮筋把所有顶点框在里面,橡皮筋的形状就是顶点的凸包。
2. 同样把点看做钉在板子上的钉子,拿一根绳子,找一个麻绳,一端绑在最外的一颗钉子上,然后拉着麻绳逆时针(或顺时针)一圈,把所有钉子包括进去。这样最终麻绳的形状也是顶点集的凸包。
上面的第二种理解实际上就是一种求凸包的最基础的算法卷包裹法(Gift Wrapping)的思想。但是这里面涉及到两个细节:
1. 第一个顶点的选择:选最左下的或最右上的都可。
2. 下一个顶点的选择:通过计算向量叉乘来选取。
回顾一下叉乘的意义:三维情况下,两向量的叉乘得到的是一个新的向量,方向是与原向量确定平面垂直的方向(有符号), 大小是原向量形成的平行四边形的面积。在二维情况下,数值大小的意义不变,符号表示原向量的方向关系。cross<a, b> = |a| * |b| * sin<a, b> > 0, 说明 a 到 b 的角为正向,即 b 在 a 的逆时针方向, 反之在其顺时针方向, 等于0说明平行。所以显然叉乘不满足交换律。此外,cross<(p,a), (p, b)> 的数值大小表示三角形pab的面积的两倍,所以当ab固定不变时,通过比较叉乘的数值大小可以比较点到线段的距离(这里的距离也是有符号的)。
所以利用叉乘运算可以解决卷包裹算法中的下一个顶点选择问题。一个特殊情况是:多点共线。共线时选点策略是:
卷包裹算法复杂度:O(n*h)。其中h为凸包上的点数。所以如果给出的点集,位于凸包上的点数比较少时,算法较快,但是最坏情况下可能所有点都位于凸包上,这样算法复杂度就是O(n^2)。
本题卷包裹算法的代码实现:
1 ///////////////////////////////////////////////////////////////// 2 // POJ1113 Wall (卷包裹Gift Wrapping算法版) 3 // Memory: 232K Time: 79MS 4 // Language: C++ Result : Accepted 5 ///////////////////////////////////////////////////////////////// 6 7 #include <iostream> 8 #include <algorithm> 9 #include <cmath> 10 11 using namespace std; 12 13 #define PI 3.141592653 14 15 using namespace std; 16 17 struct Point { 18 int x, y; 19 }; 20 21 Point p[1000]; 22 int n, L, ans[1000], cnt = 0; 23 bool mark[1000]; 24 25 double dis(Point &p1, Point &p2) { 26 double delta_x = p1.x - p2.x; 27 double delta_y = p1.y - p2.y; 28 return sqrt(delta_x * delta_x + delta_y * delta_y); 29 } 30 31 int cross(Point &ps, Point &pe, Point &qs, Point &qe) { 32 //二维向量叉乘公式: 33 //cross<(x1, y1), (x2, y2)> = x1 * y2 - x2 * y1 = || * |b| * sin<a, b> 34 //cross<p1, p2> = |p1| * |p2| * sin<p1, p2> 35 //> 0 表示 p1 在 p2 的 顺时针方向, < 0 在逆时针方向, = 0 表示共线 36 //函数所求为向量 ps->pe 和 qs->qe 的叉乘 37 return (pe.x - ps.x) * (qe.y - qs.y) - (qe.x - qs.x) * (pe.y - ps.y); 38 } 39 40 bool cmp(Point &a, Point &b, Point &v) { 41 //比较到参考点极角大小,极角相等时比较距参考点的距离 42 int res = cross(v, a, v, b); 43 if (res == 0) { 44 return dis(a, v) < dis(b, v); 45 } else { 46 return res > 0; 47 } 48 } 49 50 void GiftWrapping(void) { 51 int last = 0, next; 52 ans[cnt++] = 0; 53 //mark[0] = true; 54 while (true) { 55 //找到一个不在凸包上的点 56 next = 0; 57 for (int i = 1; i < n; ++i) { 58 if (!mark[i]) { 59 next = i; 60 break; 61 } 62 } 63 //与其它不在凸包上的点比较 64 for (int i = next + 1; i < n; ++i) { 65 if (!mark[i] && cmp(p[i], p[next], p[last])) { 66 next = i; 67 } 68 } 69 //没有不在凸包上的点了 或 应该与起点相连了 70 if (next == 0 || last != 0 && cmp(p[0], p[next], p[last])) { 71 ans[cnt++] = 0; 72 break; 73 } else { 74 ans[cnt++] = next; 75 mark[next] = true; 76 last = next; 77 } 78 } 79 } 80 81 int main(void) { 82 cin >> n >> L; 83 double circle = 2 * L * PI; 84 int ll = 0; 85 for (int i = 0; i < n; ++i) { 86 cin >> p[i].x >> p[i].y; 87 //找出y坐标最小的中x坐标最小的点(左下角点,一定在凸包上) 88 if (p[i].y < p[ll].y || p[i].y == p[ll].y && p[i].x < p[ll].x) { 89 ll = i; 90 } 91 } 92 //将左下角点放在p[0]处,作为参考点 93 Point t = p[ll]; 94 p[ll] = p[0]; 95 p[0] = t; 96 //运行核心Gift Wrapping算法 97 GiftWrapping(); 98 //计算围墙最小周长 99 double length = circle; 100 while (cnt > 1) { 101 length += dis(p[ans[cnt - 1]], p[ans[cnt - 2]]); 102 --cnt; 103 } 104 //四舍五入 105 cout << (int)(length + 0.5) << endl; 106 return 0; 107 }