最近做了一道計算幾何的題目,以前做題總是草草,現在終於認真了起來,決定認真細心的研究總結算法。判斷線段相交是計算幾何中一個重要的基礎問題,經過閱讀一些書和文獻並自己做了一下實現,現在總結如下。
一.矢量基本知識
因爲後面的計算需要一些矢量的基本知識,這裏只是簡單的列舉如下,如果需要更加詳細的信息,可以自行搜索wikipedia或google。
1.矢量的概念
:如果一條線段的端點是有次序之分的,我們把這種線段成爲有向線段(directed segment)。如果有向線段p1p2的起點p1在座標原點,我們可以把它稱爲矢量(vector)p2。
2.矢量加減法
:設二維矢量P = ( x1, y1 ),Q = ( x2 , y2 ),則矢量加法定義爲: P + Q = ( x1 + x2 , y1 + y2 ),同樣的,矢量減法定義爲: P - Q = ( x1 - x2 , y1 - y2 )。顯然有性質 P + Q = Q + P,P - Q = - ( Q - P )。
3.矢量的叉積
:計算矢量叉積是與直線和線段相關算法的核心部分。設矢量P = ( x1, y1 ),Q = ( x2, y2 ),則矢量叉積定義爲由(0,0)、p1、p2和p1+p2所組成的平行四邊形的帶符號的面積,即:P × Q = x1*y2 - x2*y1,其結果是一個標量。顯然有性質 P × Q = - ( Q × P ) 和 P × ( - Q ) = - ( P × Q )。一般在不加說明的情況下,本文下述算法中所有的點都看作矢量,兩點的加減法就是矢量相加減,而點的乘法則看作矢量叉積。
叉積的一個非常重要性質是可以通過它的符號判斷兩矢量相互之間的順逆時針關係:
若 P × Q > 0 , 則P在Q的順時針方向。
若 P × Q < 0 , 則P在Q的逆時針方向。
若 P × Q = 0 , 則P與Q共線,但可能同向也可能反向。
4.折線段的拐向判斷
:折線段的拐向判斷方法可以直接由矢量叉積的性質推出。對於有公共端點的線段p0p1和p1p2,通過計算(p2 - p0) × (p1 - p0)的符號便可以確定折線段的拐向:
若(p2 - p0) × (p1 - p0) > 0,則p0p1在p1點拐向右側後得到p1p2。
若(p2 - p0) × (p1 - p0) < 0,則p0p1在p1點拐向左側後得到p1p2。
若(p2 - p0) × (p1 - p0) = 0,則p0、p1、p2三點共線。
這一條判斷也可用來判斷點在線段或直線的哪一測。
爲了後文的敘述方便,先定義幾個結構:
- struct point{
- int x;
- int y;
- };
- struct v{
- point start;
- point end;
- };
計算兩條直線的叉積(cross production), 這裏由於定義的都是二維的情況,本質上說,在平面上兩個向量的叉積應該是垂直平面的,這裏函數返回的整數值即爲z軸上的值:
- int crossProduct(v* v1, v* v2){
- v vt1, vt2;
- int result = 0;
- vt1.start.x = 0;
- vt1.start.y = 0;
- vt1.end.x = v1->end.x - v1->start.x;
- vt1.end.y = v1->end.y - v1->start.y;
- vt2.start.x = 0;
- vt2.start.y = 0;
- vt2.end.x = v2->end.x - v2->start.x;
- vt2.end.y = v2->end.y - v2->start.y;
- result = vt1.end.x * vt2.end.y - vt2.end.x * vt1.end.y;
- return result;
- }
二.判斷兩條直線相交
先來看一個簡單的情況,即判斷兩條直線是否相交。
第一個可能會想到的辦法,就是判斷斜率,這個在中學時代就學過了,不過斜率需要考慮垂直的特殊情況,比較麻煩。更好的辦法或許是計算兩個向量的叉積,如果爲0,則是平行或者重合的,否則兩直線相交。
代碼就不貼了,直接調用上面的函數就ok了。
三.判斷兩線段相交
經典方法,就是跨立試驗了,即如果一條線段跨過另一條線段,則線段的兩個端點分別在另一條線段的兩側。但是,還需要檢測邊界情況,即兩條線段中可能某條線段的某個端點正好落在另一條線段上。這也是算法導論中介紹的算法。
程序模擬如下:
- int direction(point* pi, point* pj, point* pk){
- point p1, p2;
- p1.x = pk->x - pi->x;
- p1.y = pk->y - pi->y;
- p2.x = pj->x - pi->x;
- p2.y = pj->y - pi->y;
- return crossProduct(&p1, &p2);
- }
- int onSegment(point* pi, point* pj, point* pk){
- int minx, miny, maxx, maxy;
- if (pi->x > pj->x){
- minx = pj->x;
- maxx = pi->x;
- }
- else {
- minx = pi->x;
- maxx = pj->x;
- }
- if (pi->y > pj->y){
- miny = pj->y;
- maxy = pi->y;
- }
- else {
- miny = pi->y;
- maxy = pj->y;
- }
- if (minx <= pk->x && pk->x <= maxx && miny <= pk->y && pk->y <= maxy)
- return 1;
- else
- return 0;
- }
- int segmentIntersect(point* p1, point* p2, point* p3, point* p4){
- int d1 = direction(p3, p4, p1);
- int d2 = direction(p3, p4, p2);
- int d3 = direction(p1, p2, p3);
- int d4 = direction(p1, p2, p4);
- if (d1 * d2 < 0 && d3 * d4 < 0)
- return 1;
- else if (!d1 && onSegment(p3, p4, p1))
- return 1;
- else if (!d2 && onSegment(p3, p4, p2))
- return 1;
- else if (!d3 && onSegment(p1, p2, p3))
- return 1;
- else if (!d4 && onSegment(p1, p2, p4))
- return 1;
- else
- return 0;
- }
實際上,如果想改進上述算法,還可以在跨立試驗前加一步,就是先做快速排斥試驗。那就是,先分別判斷以兩條線段爲對角線的矩形是否相交,如果不相交,則兩個線段肯定不相交。
四.判斷兩條線段相交,然後計算交點
設一條線段爲L0=P1P2, 另一條線段或直線爲L1=Q1Q2, 要計算的就是L0和L1的交點。
1.首先判斷L0和L1是否相交(方法已在前文討論過), 如果不相交則沒有交點, 否則說明L0和L1一定有交點, 下面就將L0和L1都看作直線來考慮.
2.如果P1和P2橫座標相同, 即L0平行於Y軸
a)若L1也平行於Y軸
i.若P1的縱座標和Q1的縱座標相同, 說明L0和L1共線, 假如L1是直線的話他們有無窮的交點, 假如L1是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
ii.否則說明L0和L1平行, 他們沒有交點;
b)若L1不平行於Y軸, 則交點橫座標爲P1的橫座標, 代入到L1的直線方程中可以計算出交點縱座標;
3.如果P1和P2橫座標不同, 但是Q1和Q2橫座標相同, 即L1平行於Y軸, 則交點橫座標爲Q1的橫座標, 代入到L0的直線方程中可以計算出交點縱座標;
4.如果P1和P2縱座標相同, 即L0平行於X軸
a)若L1也平行於X軸,
i.若P1的橫座標和Q1的橫座標相同, 說明L0和L1共線, 假如L1是直線的話他們有無窮的交點, 假如L1是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
ii.否則說明L0和L1平行, 他們沒有交點;
b)若L1不平行於X軸, 則交點縱座標爲P1的縱座標, 代入到L1的直線方程中可以計算出交點橫座標;
5.如果P1和P2縱座標不同, 但是Q1和Q2縱座標相同, 即L1平行於X軸, 則交點縱座標爲Q1的縱座標, 代入到L0的直線方程中可以計算出交點橫座標;
6.剩下的情況就是L1和L0的斜率均存在且不爲0的情況
a)計算出L0的斜率K0, L1的斜率K1;
b)如果K1 = K2
i.如果Q1在L0上, 則說明L0和L1共線, 假如L1是直線的話有無窮交點, 假如L1是線段的話可用"計算兩條共線線段的交點"的算法求他們的交點(該方法在前文已討論過);
ii.如果Q1不在L0上, 則說明L0和L1平行, 他們沒有交點.
c)聯立兩直線的方程組可以解出交點來
這個算法並不複雜, 但是要分情況討論清楚, 尤其是當兩條線段共線的情況需要單獨考慮, 所以在前文將求兩條共線線段的算法單獨寫出來. 另外, 一開始就先利用矢量叉乘判斷線段與線段(或直線)是否相交, 如果結果是相交, 那麼在後面就可以將線段全部看作直線來考慮. 需要注意的是, 我們可以將直線或線段方程改寫爲ax+by+c=0的形式, 這樣一來上述過程的部分步驟可以合併, 縮短了代碼長度, 但是由於先要求出參數, 這種算法將花費更多的時間.