Time limit: 3.000 seconds
限时:3.000秒
Problem
问题
Given two convex polygons, they may or may not overlap. If they do overlap, they will do so to differing degrees and in different ways. Write a program that will read in the coordinates of the corners of two convex polygons and calculate the 'exclusive or' of the two areas, that is the area that is bounded by exactly one of the polygons. The desired area is shaded in the following diagram:
给定两个可能相互重叠的凸多边形。如果存在重叠,其重叠的角度和方向各异。你要写一个程序读入两个凸多边形各角点的坐标,并计算出二者“异或”得到的区域的面积,也就是在二者全部区域中仅存在其中一个多边形的区域。要求的面积如下图所示:
Input
输入
Input will consist of pairs of lines each containing the number of vertices of the polygon, followed by that many pairs of integers representing the x,y coordinates of the corners in a clockwise direction. All the coordinates will be positive integers less than 100. For each pair of polygons (pair of lines in the data file), your program should print out the desired area correct to two decimal places. The input will end with a line containing a zero (0).
输入由多行组成,每两行一组。每行第一个数字为该多边形顶点的数量,后面是多组整型x, y坐标值,并按顺时针方向排列。所有坐标都为正整数且小于100。对于每两个多边形(输入的每两行数据),你的程序应打印出要求的区域并保留2位小数。输入的最后一行为0。
Output
输出
Output will consist of a single line containing the desired area written as a succession of eight (8) digit fields with two (2) digits after the decimal point. There will not be enough cases to need more than one line.
只有一行输出,将求出的面积按8位数字输出,且小数点后只保留2位(译注:前面不够8位的以空格填充)。不会有太多的测试用例,因此一行就够。
Sample input
输入
3 5 5 8 1 2 3
3 5 5 8 1 2 3
4 1 2 1 4 5 4 5 2
6 6 3 8 2 8 1 4 1 4 2 5 3
0
Sample output
示例输出
△△△△ 0.00 △△△ 13.50
where △ represents a single space.
其中每个小三角代表一个空格。
Analysis
分析
这道题的解题过程非常复杂,要是想从零开始写一个完全正确的程序比较困难。里面牵扯很多算法,还有很多特殊情况,若中间某一步出错则很难为错误定位。因此不建议拿此题练手。
本题主要包括的算法有(均在二维平面上):判断线段相交、求线段交点、求点集的凸包、判断点在多边形内、求多边形面积。我用最简单的一种思路来解决这个问题,用到的都是以前写过的成熟算法。可能有点暴力,但能保证不出错。
首先求得两凸多边形的面积,可按以下公式计算:
然后求得两多边形互在对方内部的点,加入交集顶点数组。这里用判断点在多边形内的算法。首先要保证凸多边形上的点是按顺时针或逆时针方向给出的,对于每一个点(x0, y0),与多边形上的每一条线段vi的起点pi构成向量vsi,若存在vi × vsi < 0(逆时针时为大于号)则点不在多边形内。原理是如果点在其内,那么点依次与多边形上的每个顶点形成的向量和以该顶点为起点的边的夹角必小于180度。
接下来对两多边形的各条线段两两求交,求出所有交点(包括重合的顶点)并加入交集顶点数组。这里用到的算法请见:平面内两条线段的位置关系(相交)判定与交点求解。
再对交集顶点数组求凸包,参见:Graham's Scan法求解凸包问题。其实此时得到的交集顶点已经是凸包了,只是没有按照顺序排列,且可能出现很多重复点,因此用凸包算法再将其整理一遍而已。
最后用两多边形面积之和减去交集面积的二倍即为结果。整个过程在代码的注释中都解释的很清楚了,还是看代码吧。
事实上找出交集所有的顶点还有一个O(n)的算法,就是先找出两个多边形相交的第一个顶点,然后依次向顺时针旋转,添加两多边形的下一个顶点或交点中旋转较多的一点。如此往复,即可一次性得到顺时针排列的交集顶点。但这种算法实现比较复杂,有很多特殊情况需要处理,更重要的是我手头没有成熟的实现,如果贸然将其应用到这种复杂的算法题中,很可能永远也得不到AC,且无法查出错在哪里。
如果您有更好的思路,望不吝赐教。
Solution
解答
#include <algorithm> #include <iostream> #include <vector> #include <cmath> using namespace std; struct POINTF { float x; float y; }; //保证精度,两个淫点数之差小于0.0001的认为相等 bool Equal(float f1, float f2) { return (abs(f1 - f2) < 1e-4f); } //判断两点是否相等 bool operator==(const POINTF &p1, const POINTF &p2) { return (Equal(p1.x, p2.x) && Equal(p1.y, p2.y)); } //比较两点坐标大小,先比较x坐标,若相同则比较y坐标 bool operator>(const POINTF &p1, const POINTF &p2) { return (p1.x > p2.x || (Equal(p1.x, p2.x) && p1.y > p2.y)); } //计算两向量外积 float operator^(const POINTF &p1, const POINTF &p2) { return (p1.x * p2.y - p1.y * p2.x); } //判定两线段位置关系,并求出交点(如果存在)。 //有重合:完全重合(6),1个端点重合且共线(5),部分重合(4) //无重合:两端点相交(3),交于线上(2),正交(1),无交(0),参数错误(-1) int Intersection(POINTF p1, POINTF p2, POINTF p3, POINTF p4, POINTF &Int) { //保证参数p1!=p2,p3!=p4 if (p1 == p2 || p3 == p4) { return -1; //返回-1代表至少有一条线段首尾重合,不能构成线段 } //为方便运算,保证各线段的起点在前,终点在后。 if (p1 > p2) { swap(p1, p2); } if (p3 > p4) { swap(p3, p4); } //求出两线段构成的向量 POINTF v1 = {p2.x - p1.x, p2.y - p1.y}, v2 = {p4.x - p3.x, p4.y - p3.y}; //求两向量外积,平行时外积为0 float Corss = v1 ^ v2; //判定两线段是否完全重合 if (p1 == p3 && p2 == p4) { return 6; } //如果起点重合 if (p1 == p3) { Int = p1; //起点重合且共线(平行)返回5;不平行则交于端点,返回3 return (Equal(Corss, 0) ? 5 : 3); } //如果终点重合 if (p2 == p4) { Int = p2; //终点重合且共线(平行)返回5;不平行则交于端点,返回3 return (Equal(Corss, 0) ? 5 : 3); } //如果两线端首尾相连 if (p1 == p4) { Int = p1; return 3; } if (p2 == p3) { Int = p2; return 3; }//经过以上判断,首尾点相重的情况都被排除了 //将线段按起点坐标排序。若线段1的起点较大,则将两线段交换 if (p1 > p3) { swap(p1, p3); swap(p2, p4); //更新原先计算的向量及其外积 swap(v1, v2); Corss = v1 ^ v2; } //处理两线段平行的情况 if (Equal(Corss, 0)) { //做向量v1(p1, p2)和vs(p1,p3)的外积,判定是否共线 POINTF vs = {p3.x - p1.x, p3.y - p1.y}; //外积为0则两平行线段共线,下面判定是否有重合部分 if (Equal(v1 ^ vs, 0)) { //前一条线的终点大于后一条线的起点,则判定存在重合 if (p2 > p3) { Int = p3; return 4; //返回值4代表线段部分重合 } }//若三点不共线,则这两条平行线段必不共线。 //不共线或共线但无重合的平行线均无交点 return 0; } //以下为不平行的情况,先进行快速排斥试验 //x坐标已有序,可直接比较。y坐标要先求两线段的最大和最小值 float ymax1 = p1.y, ymin1 = p2.y, ymax2 = p3.y, ymin2 = p4.y; if (ymax1 < ymin1) { swap(ymax1, ymin1); } if (ymax2 < ymin2) { swap(ymax2, ymin2); } //如果以两线段为对角线的矩形不相交,则无交点 if (p1.x > p4.x || p2.x < p3.x || ymax1 < ymin2 || ymin1 > ymax2) { return 0; }//下面进行跨立试验 POINTF vs1 = {p1.x - p3.x, p1.y - p3.y}, vs2 = {p2.x - p3.x, p2.y - p3.y}; POINTF vt1 = {p3.x - p1.x, p3.y - p1.y}, vt2 = {p4.x - p1.x, p4.y - p1.y}; float s1v2, s2v2, t1v1, t2v1; //根据外积结果判定否交于线上 if (Equal(s1v2 = vs1 ^ v2, 0) && p4 > p1 && p1 > p3) { Int = p1; return 2; } if (Equal(s2v2 = vs2 ^ v2, 0) && p4 > p2 && p2 > p3) { Int = p2; return 2; } if (Equal(t1v1 = vt1 ^ v1, 0) && p2 > p3 && p3 > p1) { Int = p3; return 2; } if (Equal(t2v1 = vt2 ^ v1, 0) && p2 > p4 && p4 > p1) { Int = p4; return 2; } //未交于线上,则判定是否相交 if(s1v2 * s2v2 > 0 || t1v1 * t2v1 > 0) { return 0; } //以下为相交的情况,算法详见文档 //计算二阶行列式的两个常数项 float ConA = p1.x * v1.y - p1.y * v1.x; float ConB = p3.x * v2.y - p3.y * v2.x; //计算行列式D1和D2的值,除以系数行列式的值,得到交点坐标 Int.x = (ConB * v1.x - ConA * v2.x) / Corss; Int.y = (ConB * v1.y - ConA * v2.y) / Corss; //正交返回1 return 1; } // 比较向量中哪个与x轴向量(1, 0)的夹角更大 bool CompareVector(const POINTF &pt1, const POINTF &pt2) { //求向量的模 float m1 = sqrt(pt1.x * pt1.x + pt1.y * pt1.y); float m2 = sqrt(pt2.x * pt2.x + pt2.y * pt2.y); //两个向量分别与(1, 0)求内积 float v1 = pt1.x / m1, v2 = pt2.x / m2; //如果向量夹角相等,则返回离基点较近的一个,保证有序 return (v1 < v2 || v1 == v2 && m1 < m2); } //计算凸包 bool CalcConvexHull(vector<POINTF> &Src) { //点集中至少应有3个点,才能构成多边形 if (Src.size() < 3) { return false; } //查找基点 vector<POINTF>::iterator i; POINTF ptBase = Src.front(); //将第1个点预设为最小点 for (i = Src.begin() + 1; i != Src.end(); ++i) { //如果当前点的y值小于最小点,或y值相等,x值较小 if (i->y < ptBase.y || (i->y == ptBase.y && i->x > ptBase.x)) { //将当前点作为最小点 ptBase = *i; } } //计算出各点与基点构成的向量 for (i = Src.begin(); i != Src.end();) { //排除与基点相同的点,避免后面的排序计算中出现除0错误 if (*i == ptBase) { i = Src.erase(i); } else { //方向由基点到目标点 i->x -= ptBase.x, i->y -= ptBase.y; ++i; } } //按各向量与横坐标之间的夹角排序 sort(Src.begin(), Src.end(), &CompareVector); //删除相同的向量 Src.erase(unique(Src.begin(), Src.end()), Src.end()); //点集中至少还剩2个点,加上基点才能构成多边形 if (Src.size() < 2) { return false; } //计算得到首尾依次相联的向量 for (vector<POINTF>::reverse_iterator ri = Src.rbegin(); ri != Src.rend() - 1; ++ri) { vector<POINTF>::reverse_iterator riNext = ri + 1; //向量三角形计算公式 ri->x -= riNext->x, ri->y -= riNext->y; } //依次删除不在凸包上的向量 for (i = Src.begin() + 1; i != Src.end(); ++i) { //回溯删除旋转方向相反的向量,使用外积判断旋转方向 for (vector<POINTF>::iterator iLast = i - 1; iLast != Src.begin();) { float v1 = i->x * iLast->y, v2 = i->y * iLast->x; //如果叉积大于0,则没有逆向旋转 //如果叉积等于0,还需用内积判断方向是否相逆 if (v1 > v2 || (v1 == v2 && i->x * iLast->x > 0 && i->y * iLast->y > 0)) { break; } //删除前一个向量后,需更新当前向量,与前面的向量首尾相连 //向量三角形计算公式 i->x += iLast->x, i->y += iLast->y; iLast = (i = Src.erase(iLast)) - 1; } } //将所有首尾相连的向量依次累加,换算成坐标 Src.front().x += ptBase.x, Src.front().y += ptBase.y; for (i = Src.begin() + 1; i != Src.end(); ++i) { i->x += (i - 1)->x, i->y += (i - 1)->y; } //添加基点,全部的凸包计算完成 Src.push_back(ptBase); return (Src.size() >= 3); } //计算凸多边形面积 float CalcArea(vector<POINTF> &Covex) { float fArea = 0; vector<POINTF>::iterator i, j; //遍例多边形每一个顶点 for (i = Covex.begin(); i != Covex.end(); ++i) { //j为i的下一个顶点 if ((j = i + 1) == Covex.end()) { j = Covex.begin(); } //累加面积 fArea += j->x * i->y - i->x * j->y; } return fArea / 2.0f; } //判定点是否在多边形内 bool PointInPolygon(POINTF pt, vector<POINTF> &Poly) { //遍例多边形每一个顶点 for (int i = 0; i < (int)Poly.size(); ++i) { //j为i的下一个顶点 int j = (i + 1) % (int)Poly.size(); //构成由给定点到顶点的向量 POINTF p1 = {pt.x - Poly[i].x, pt.y - Poly[i].y}; //构成由当前顶点到下一顶点的向量 POINTF p2 = {Poly[j].x - Poly[i].x, Poly[j].y - Poly[i].y}; //根据外积作出判断 float fCross = p1 ^ p2; if (fCross < 0) { return false; } } return true; } //主函数 int main(void) { vector<float> Result; //循环读入每一组多边形数据 for (int nNum; cin >> nNum && nNum != 0; ++nNum) { vector<POINTF> Poly1, Poly2; for (POINTF pt; nNum-- > 0 && cin >> pt.x >> pt.y; Poly1.push_back(pt)); cin >> nNum; for (POINTF pt; nNum-- > 0 && cin >> pt.x >> pt.y; Poly2.push_back(pt)); //去掉每个多边形中,相临的重复点 unique(Poly1.begin(), Poly1.end()); unique(Poly2.begin(), Poly2.end()); if (Poly1.size() < 3 || Poly2.size() < 3) { printf("%8.2f", 0.0f); } //计算两多边形的面积和 float fAreaUnion = CalcArea(Poly1); fAreaUnion += CalcArea(Poly2); vector<POINTF> IntPoly; //添加多边形1在多边形2中的点到交集中 for (int i = 0; i < (int)Poly1.size(); ++i) { if (PointInPolygon(Poly1[i], Poly2)) { IntPoly.push_back(Poly1[i]); } } //添加多边形2在多边形1中的点到交集中 for (int i = 0; i < (int)Poly2.size(); ++i) { if (PointInPolygon(Poly2[i], Poly1)) { IntPoly.push_back(Poly2[i]); } } //求出两多边形所有的交点,含重合的顶点,添加到交集中 for (int i = 0; i < (int)Poly1.size(); ++i) { for (int j = 0; j < (int)Poly2.size(); ++j) { POINTF Int; int nr = Intersection( Poly1[i], Poly1[(i + 1) % (int)Poly1.size()], Poly2[j], Poly2[(j + 1) % (int)Poly2.size()], Int); if (nr == 6) { IntPoly.push_back(Poly1[i]); IntPoly.push_back(Poly1[(i + 1) % (int)Poly1.size()]); } else if(nr > 0) { IntPoly.push_back(Int); } } } //为交集求凸包,并计算面积 float fIntArea = CalcConvexHull(IntPoly) ? CalcArea(IntPoly) * 2 : 0; //保存结果 Result.push_back(fAreaUnion - fIntArea); } //按格式输出结果 for (vector<float>::iterator i = Result.begin(); i != Result.end(); ++i) { printf("%8.2f", *i); } cout << endl; return 0; }
作者:王雨濛;新浪微博:@吉祥村码农;来源:《程序控》博客 -- http://www.cnblogs.com/devymex/ 此文章版权归作者所有(有特别声明的除外),转载必须注明作者及来源。您不能用于商业目的也不能修改原文内容。 |