UVa OJ 137 - Polygons (多邊形)

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:
給定兩個可能相互重疊的凸多邊形。如果存在重疊,其重疊的角度和方向各異。你要寫一個程序讀入兩個凸多邊形各角點的座標,並計算出二者“異或”得到的區域的面積,也就是在二者全部區域中僅存在其中一個多邊形的區域。要求的面積如下圖所示:

 

 f1

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
分析

這道題的解題過程非常複雜,要是想從零開始寫一個完全正確的程序比較困難。裏面牽扯很多算法,還有很多特殊情況,若中間某一步出錯則很難爲錯誤定位。因此不建議拿此題練手。

本題主要包括的算法有(均在二維平面上):判斷線段相交、求線段交點、求點集的凸包、判斷點在多邊形內、求多邊形面積。我用最簡單的一種思路來解決這個問題,用到的都是以前寫過的成熟算法。可能有點暴力,但能保證不出錯。

首先求得兩凸多邊形的面積,可按以下公式計算:

fom1然後求得兩多邊形互在對方內部的點,加入交集頂點數組。這裏用判斷點在多邊形內的算法。首先要保證凸多邊形上的點是按順時針或逆時針方向給出的,對於每一個點(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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章