ZOJ 1010. Area.
题目地址:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=1010
基本题意:此题目给出 n 个点的坐标 (xi,yi ) ,要求求出这些点按顺序围成的多边形的面积。同时要求检测出不合法的多边形(例如非相邻边彼此相交的情况)。
此题求面积不难,样例很容易就过了,但是提交 10+ 次基本都是 WA ,几乎失去信心。做题最郁闷的就是你不知道问题到底出在什么地方,可能改了很多次都没有试对地方。后来跑到 88 的算法版搜了下,看到别人的代码,然后事后总结实际还是卡在了什么样的多边形是 Impossible 的,这个检测代码没有写好,耐心不够导致。
(1)求面积很简单,类似求某个曲线的区间积分
用所有多边形的边和X轴围成的梯形的面积累加起来就可以了。例如某条边 ( P1 - P2 ) 和在它在 x 轴的投影 ( p1' - P2' ) 所围成的梯形面积是:
S [ p1->p2 ] = 1/2 * ( y1 + y2 ) * ( x2 - x1 ) ; ----(I)
这个式子拆开后就是:
S [ p1->p2 ] = 1/2 * ( -x1 * y1 + x2 * y2 - x1 * y2 + x2 * y1 ) ; ---- ( 注:蓝色部分的 xi * yi 累加后被消掉 )
由于每个边都被累加一遍,每个点参与了组成边两次(前一条边的终点和后一条边的起始点),所以每个顶点自身坐标的乘积 ( xi * yi ) 累加的时候都被消掉了!最后就是累加所有的式(I):
Area = ∑ 1/2 * ( -x( i ) * y( i + 1 ) + x( i + 1 ) * y( i ) ) ;
----( i = 1,2,...,n,且P(n+1) = P(0) ); ----(II)
上面的式子,括号内是两个矢量的叉积 P[ i + 1 ] × P[ i ]。因此式II实际上是依次累加相邻点的叉积,最后在除以2。考虑叉积的含义,是以两个矢量为相邻边的平行四边形(由 O(0,0), P1, P2, (P1 + P2) 四个点组成)的面积(在平面坐标中用三角形面积相减可以很容易推导出该含义),因此这个方法的物理意义就非常明显了,我们可以直观的假定原点位于多边形内部,显然依次把相邻点和原点围成的三角形的面积累加起来就是这些点组成的多边形的面积。因此 1/2 ( P[ i + 1 ] X P[ i ] ) 就是三角形 O - P[ i + 1 ] - P[ i ] 的面积(平行四边形面积的一半,O 表示原点)。当然,由于叉积有符号,所以原点位于多边形外部也成立,只是在内部更直观,更容易理解。
备注:我们可以把存储顶点的数组扩充一个元素,然后把 P( n+1 ) 赋值为 P( 0 ),这样我们对上式的循环中就不必对顶点索引使用取模运算(MOD)。我自己写的代码采用的(I)这种计算方法,我参考的代码采用(II)中的计算方法,两者计算量相差不大,但后者不太直观,所以我就稍微解释了下两者本质是相同的。
注意式(I)中的梯形面积是有符号的,和 P1,P2 的点顺序相关( S[ p1->p2 ] = -S[ p2->p1 ] ) ,因此 Area 的符号和顶点顺序有关,所以最后别忘了对计算结果求绝对值。
(2)麻烦在于,什么样的多边形是 Impossible 的
做题的时候肯定我们就会着重考虑这一点。因为不管任何多边形,按照(1)都能算出个面积,但是多边形是否是有效的需要单独判断。设多边形有 n 个顶点和边,则:
2.1) n < 3 时,显然是 Impossible,因为一个点或一个线段不能组成多边形。
2.2) n > 3 时,依次判断每条边 和 它的所有非相邻边 不能相交(包含交点位于某一条线段上的情况),保证这一点即可。
2.3) n = 3 时,是三角形,无须做 2.2 中的检测。如果三角形的三个点共线的话,是不是应该输出 Impossible,我们没有考虑也 AC 了 (即我们会对这种情况认为是有效的而输出 0.00),估计 OL平台的测试输入也没有考虑这种情况。
下面我们考虑2.2中的检测:
总的边数为 n,则每条边的非相邻边为(n-3)条,所以检测完所有边的情况类似冒泡排序,以及绘制多边形所有顶点的连接线的金刚石图形,需要耗费O(n^2)的时间复杂度。
相邻边必然是相交于顶点的所以不用检测,即使相邻边向内重叠了,比如连续的三个顶点:(0,0),(0,5),(0,3)这种情况也不必担心,因为在下一条边检测时也会检测出相交情况的。
判断两个线段是否相交属于算法中的计算几何部分,主要原理是 如果两个线段相交,则从任何一个线段角度看,另一个线段的两个端点都位于它自己的两侧(该线段以任一端点为原点旋转到经过另一个线段的两个端点的时针方向相反)。 两个矢量的相对旋转方向利用矢量叉积求出。
我原来还考虑了浮点数和 0 的比较判断,实际上最后从提交结果发现无需考虑这个问题,直接和 0 比就是了。
(3)对顶点坐标的存储
为了检测多边形是不是Impossible的,就必须要存储所有顶点的坐标。如果不需要判断这个,就不需要任何额外的存储空间。那样就简洁多了。当然那样的话这道题也就毫无任何难度,也失去了对计算几何的考察点。
我看了我参考的解法,我居然没有想到像他开一个维度为1000的数组,ft!我是用了一个链表来存储所有的边。。。。汗。。。(当然应该用STL就可以,我习惯了自己写链表的代码),所以我的代码比我参考的那个人的多好多。。。(他的代码好简短!)
另外,由于我用链表存储的是线段,相当于每个顶点被我存储了两次,所以实际上还是浪费了一倍的堆上内存。另外还要加上链表使用的指针占用的空间。当然优点是动态内存管理相比静态数组在内存的使用上要灵活一些,这就无须多做解释了。
检测边是否相交实际上和那个人的代码的原理基本一致,这部分代码我基本是照着《算法导论》上的伪码直译过来的。
我提交的代码如下:
CODE_ZOJ1010
#include <stdio.h>#include <stdlib.h> //#define ZEROVALUE 0.00000000000001 #ifndef __max#define __max(a,b) (((a) > (b)) ? (a) : (b))#endif #ifndef __min#define __min(a,b) (((a) < (b)) ? (a) : (b))#endif typedef struct _LINE{double x1;double y1;double x2;double y2; _LINE* pNext;} LINE; //以Pi为原点,从 PiPj 到 PiPk 的方位(返回值的符号表示顺时针 or 逆时针)double Direction(double xi, double yi, //原点 double xj, double yj, //线段1结束点 double xk, double yk) //线段2结束点{return (xk - xi) * (yj - yi) - (xj - xi) * (yk - yi);} //如果点 Pk 位于线段 PiPj 所在直线上,则它是否位于该线段内int OnSegment(double xi, double yi, double xj, double yj, //线段1结束点 double xk, double yk) //线段2结束点{if(xk >= __min(xi, xj) && xk <= __max(xi, xj) && yk >= __min(yi, yj) && yk <= __max(yi, yj))return 1;elsereturn 0;} //判断两个线段P1P2,P3P4是否相交int intersect(double x1, double y1,double x2, double y2,double x3, double y3,double x4, double y4){double d1 = Direction(x3, y3, x4, y4, x1, y1);double d2 = Direction(x3, y3, x4, y4, x2, y2);double d3 = Direction(x1, y1, x2, y2, x3, y3);double d4 = Direction(x1, y1, x2, y2, x4, y4); if( ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)) )return 1; //相交 else if(d1 == 0 && OnSegment(x3, y3, x4, y4, x1, y1))return 1;else if(d2 == 0 && OnSegment(x3, y3, x4, y4, x2, y2))return 1;else if(d3 == 0 && OnSegment(x1, y1, x2, y2, x3, y3))return 1;else if(d4 == 0 && OnSegment(x1, y1, x2, y2, x4, y4))return 1;elsereturn 0; //绝对不相交} /* 增加线段到链表尾部 */ void AddLine(LINE** ppHead, LINE** ppTail,double x1, double y1,double x2, double y2){ LINE *pNode = (LINE*)malloc(sizeof(LINE)); pNode->pNext = NULL; pNode->x1 = x1; pNode->y1 = y1; pNode->x2 = x2; pNode->y2 = y2; if(*ppTail != NULL) { (*ppTail)->pNext = pNode; }else { *ppHead = pNode; } *ppTail = pNode;} void FreeList(LINE** ppHead, LINE** ppTail){ LINE* pNode = (*ppHead);while(pNode != NULL) { LINE* tmp = pNode->pNext; free(pNode); pNode = tmp; } *ppHead = NULL; *ppTail = NULL;} /* 检查是否这一系列点围成的多边形是否合法 */ int CheckValid(LINE* pHead){int index = 0; /* 当前正循环到的边的索引 */ LINE* pCur = pHead; LINE* pTest; while(pCur != NULL) { pTest = NULL;if(pCur->pNext != NULL) pTest = pCur->pNext->pNext; while(pTest != NULL) {//最后一条边(n-1) -- (0),不与第一条边 (0) -- (1) 检测 if(index == 0 && pTest->pNext == NULL)break; /* 非相邻边相交? */if(intersect(pCur->x1, pCur->y1, pCur->x2, pCur->y2, pTest->x1, pTest->y1, pTest->x2, pTest->y2)) {return 0; } pTest = pTest->pNext; } pCur = pCur->pNext; ++index; } return 1;} int main(int argc, char* argv[]){ LINE *pHead = NULL, *pTail = NULL;int i, n = 0, figure = 1;double x0, y0, x1, y1, x2, y2;double area = 0; //int test = intersect(0, 1, 1, 0, 1, -1, 0, 0.5); while(1) { scanf("%d", &n);if(n <= 0)break; //打印一个空行 if(figure > 1) printf("\n"); area = 0; scanf("%lf %lf", &x0, &y0); x1 = x0; y1 = y0;for(i = 1; i <= n; i++) {if(i < n) scanf("%lf %lf", &x2, &y2);else { x2 = x0; y2 = y0; } area += (y1 + y2) * (x2 - x1) * 0.5; AddLine(&pHead, &pTail, x1, y1, x2, y2); x1 = x2; y1 = y2; } if(area < 0) area *= -1; if(n < 3) printf("Figure %d: Impossible\n", figure);else if(!CheckValid(pHead)) printf("Figure %d: Impossible\n", figure);else printf("Figure %d: %.02lf\n", figure, area); FreeList(&pHead, &pTail); figure++; }return 0;}
另附上我参考的代码:
code_by_dynamic
发信人: dynamic (programming@math), 信区: ACMICPC标 题: Re: 1010里的错误多边形有多少可能性?发信站: 逸仙时空 Yat-sen Channel (Wed Apr 9 20:55:07 2003), 站内信件 我的程序
//zju 1010//by dynamic//on sunday july 28th
#include <stdio.h> double det(double x1,double y1,double x2,double y2,double x3,double y3){return x1*y2+x2*y3+x3*y1-x2*y1-x3*y2-x1*y3;}
int main(){int n,i,j,count=0,temp;double x[1000],y[1000],area;while(1) { scanf("%d",&n);if (n==0) return 0; count++;if (count>1) printf("\n"); printf("Figure %d: ",count);for(i=0;i<n;i++) scanf("%lf%lf",&x[i],&y[i]);if (n<3){ printf("Impossible\n");continue; }for(i=0;i<n;i++)for(j=(i+1)/n;j<i-1;j++){ temp=(i+1)%n;if (det(x[j],y[j],x[i],y[i],x[j+1],y[j+1])*det(x[j],y[j],x[temp],y[temp],x[j+1],y[j+1])<=0&& det(x[i],y[i],x[j],y[j],x[temp],y[temp])*det(x[i],y[i],x[j+1],y[j+1],x[temp],y[temp])<=0) { printf("Impossible\n");goto quit; } } area=0;for(i=0;i<n;i++){ area+=x[i]*y[(i+1)%n]; area-=y[i]*x[(i+1)%n]; }if (area<0) area*=-1; printf("%.2lf\n",area/2);quit:; }}
参考资料:
1. 《算法导论》,第33章(计算几何学)33.1 节(线段的性质)。