前言
切線的構造屬於計算幾何中的基礎問題。存在高效的算法可以計算凸集的切線。對於凹集,其切線與對應凸集的切線相同,所以也能高效計算。因此,我們這裏僅僅討論凸多邊形的切線。
尋找切線的算法與尋找極值點的算法(凸多邊形的極值點)類似。通過運用相似的二分搜索方法,我們能構造O(logn)複雜度的切線尋找算法。
定義
直線L與多邊形S相切定義爲:L與S接觸,但不穿過S的邊界。也就是L僅僅與S的一個點相交或者與一條邊重合。
根據上面的定義,S中的所有點要麼在L上,要麼都在L的同一側。從外部任意一點,存在兩條切線與S相切。
不妨設,以逆時針方向給出。爲第i條邊,爲邊向量,由Vi指向Vi+1。想象一下,站在Vi點面朝的方向,如果P點在左手邊,那麼稱P點在左側。
如上圖所示。記P點爲多邊形S外部任意一點。對於任意一個頂點Vi,可以用下面的方法很方便地檢查直線PVi是否是切線:
考慮Vi前後的兩條邊和。如果點P在這兩條邊的同一側(同在左側或者同在右側),那麼Vi-1和Vi+1一定在直線PVi的不同側,PVi就一定不是切線。反之,若點P在的左側,而在的右側,或者相反,那麼點P就是一條切線。如上圖,點P在右側而在左側,那麼點P是最右切線;同樣地,點P在左側而在右側,那麼點P是最左切線。
對於凸多邊形,可以用上述方法求出切線。但對於凹多邊形,需要先求凸包,然後再用上述方法求其凸包的切線,其凸包的切線必然是原多邊形的切線。相對於原始多邊形,凸包往往點數大大減少,因此求解起來很快。多邊形凸包算法可以參考之前的博文Melkman Algorithm。
對於凹多邊形還有一種方法是:使用暴力搜索,先求出所有的局部切線,然後從這些切線中找到最右側的和最左側的,就是我們要求的兩條切線了。如下圖:
暴力算法
尋找任意多邊形切線的暴力算法很容易實現,通常當多邊形點數n較小時,採用這一算法比較高效。
其C++代碼如下:
// Assume that classes are already given for the objects:
// Point with coordinates {float x, y;}
//===================================================================
// isLeft(): test if a point is Left|On|Right of an infinite line.
// Input: three points P0, P1, and P2
// Return: >0 for P2 left of the line through P0 and P1
// =0 for P2 on the line
// <0 for P2 right of the line
// See: Algorithm 1 on Area of Triangles
inline float
isLeft( Point P0, Point P1, Point P2 )
{
return (P1.x - P0.x)*(P2.y - P0.y) - (P2.x - P0.x)*(P1.y - P0.y);
}
// tests for polygon vertex ordering relative to a fixed point P
#define above(P,Vi,Vj) (isLeft(P,Vi,Vj) > 0) // true if Vi is above Vj
#define below(P,Vi,Vj) (isLeft(P,Vi,Vj) < 0) // true if Vi is below Vj
//===================================================================
// tangent_PointPoly(): find any polygon's exterior tangents
// Input: P = a 2D point (exterior to the polygon)
// n = number of polygon vertices
// V = array of vertices for any 2D polygon with V[n]=V[0]
// Output: *rtan = index of rightmost tangent point V[*rtan]
// *ltan = index of leftmost tangent point V[*ltan]
void
tangent_PointPoly( Point P, int n, Point* V, int* rtan, int* ltan )
{
float eprev, enext; // V[i] previous and next edge turn direction
*rtan = *ltan = 0; // initially assume V[0] = both tangents
eprev = isLeft(V[0], V[1], P);
for (int i=1; i<n; i++) {
enext = isLeft(V[i], V[i+1], P);
if ((eprev <= 0) && (enext > 0)) {
if (!below(P, V[i], V[*rtan]))
*rtan = i;
}
else if ((eprev > 0) && (enext <= 0)) {
if (!above(P, V[i], V[*ltan]))
*ltan = i;
}
eprev = enext;
}
return;
}
//===================================================================
二分搜索
對於凸多邊形,存在速度更快的二分搜索算法。只需要在多邊形的點之間定義一個次序,就可以使用類似於凸多邊形的極值點中的相同的二分搜索算法。例如,如下圖,我們定義是增大的, 當Vi+1相對於點P在Vi之上。或者說點P在右側。
可以看到對於凸多邊形而言,從最下面的切點開始,先單調增大,到最上面大的切點之後,再單調減小。因此與凸多邊形的極值點算法相比,唯一的差別就是順序關係判斷函數發生了變化,其他部分基本完全一樣。下面直接給出一個C++實現:
// tangent_PointPolyC(): fast binary search for tangents to a convex polygon
// Input: P = a 2D point (exterior to the polygon)
// n = number of polygon vertices
// V = array of vertices for a 2D convex polygon with V[n] = V[0]
// Output: *rtan = index of rightmost tangent point V[*rtan]
// *ltan = index of leftmost tangent point V[*ltan]
void
tangent_PointPolyC( Point P, int n, Point* V, int* rtan, int* ltan )
{
*rtan = Rtangent_PointPolyC(P, n, V);
*ltan = Ltangent_PointPolyC(P, n, V);
}
// Rtangent_PointPolyC(): binary search for convex polygon right tangent
// Input: P = a 2D point (exterior to the polygon)
// n = number of polygon vertices
// V = array of vertices for a 2D convex polygon with V[n] = V[0]
// Return: index "i" of rightmost tangent point V[i]
int
Rtangent_PointPolyC( Point P, int n, Point* V )
{
// use binary search for large convex polygons
int a, b, c; // indices for edge chain endpoints
int upA, dnC; // test for up direction of edges a and c
// rightmost tangent = maximum for the isLeft() ordering
// test if V[0] is a local maximum
if (below(P, V[1], V[0]) && !above(P, V[n-1], V[0]))
return 0; // V[0] is the maximum tangent point
for (a=0, b=n;;) { // start chain = [0,n] with V[n]=V[0]
c = (a + b) / 2; // midpoint of [a,b], and 0<c<n
dnC = below(P, V[c+1], V[c]);
if (dnC && !above(P, V[c-1], V[c]))
return c; // V[c] is the maximum tangent point
// no max yet, so continue with the binary search
// pick one of the two subchains [a,c] or [c,b]
upA = above(P, V[a+1], V[a]);
if (upA) { // edge a points up
if (dnC) // edge c points down
b = c; // select [a,c]
else { // edge c points up
if (above(P, V[a], V[c])) // V[a] above V[c]
b = c; // select [a,c]
else // V[a] below V[c]
a = c; // select [c,b]
}
}
else { // edge a points down
if (!dnC) // edge c points up
a = c; // select [c,b]
else { // edge c points down
if (below(P, V[a], V[c])) // V[a] below V[c]
b = c; // select [a,c]
else // V[a] above V[c]
a = c; // select [c,b]
}
}
}
}
// Ltangent_PointPolyC(): binary search for convex polygon left tangent
// Input: P = a 2D point (exterior to the polygon)
// n = number of polygon vertices
// V = array of vertices for a 2D convex polygon with V[n]=V[0]
// Return: index "i" of leftmost tangent point V[i]
int
Ltangent_PointPolyC( Point P, int n, Point* V )
{
// use binary search for large convex polygons
int a, b, c; // indices for edge chain endpoints
int dnA, dnC; // test for down direction of edges a and c
// leftmost tangent = minimum for the isLeft() ordering
// test if V[0] is a local minimum
if (above(P, V[n-1], V[0]) && !below(P, V[1], V[0]))
return 0; // V[0] is the minimum tangent point
for (a=0, b=n;;) { // start chain = [0,n] with V[n] = V[0]
c = (a + b) / 2; // midpoint of [a,b], and 0<c<n
dnC = below(P, V[c+1], V[c]);
if (above(P, V[c-1], V[c]) && !dnC)
return c; // V[c] is the minimum tangent point
// no min yet, so continue with the binary search
// pick one of the two subchains [a,c] or [c,b]
dnA = below(P, V[a+1], V[a]);
if (dnA) { // edge a points down
if (!dnC) // edge c points up
b = c; // select [a,c]
else { // edge c points down
if (below(P, V[a], V[c])) // V[a] below V[c]
b = c; // select [a,c]
else // V[a] above V[c]
a = c; // select [c,b]
}
}
else { // edge a points up
if (dnC) // edge c points down
a = c; // select [c,b]
else { // edge c points up
if (above(P, V[a], V[c])) // V[a] above V[c]
b = c; // select [a,c]
else // V[a] below V[c]
a = c; // select [c,b]
}
}
}
}
//===================================================================