凸包/旋轉卡殼/半平面交學習總結

 

1.凸包

參考博客:https://blog.csdn.net/qq_34374664/article/details/70149223

定義:假設平面上有若干個點,過某些點作一個多邊形,使這個多邊形能把所有點都“包”起來。當這個多邊形是凸多邊形的時候,我們就叫它“凸包”。

求法:目前我只掌握了Graham掃描法,但我覺得夠用了。

步驟:

    1.把所有點放在二維座標系中,則縱座標最小的點一定是凸包上的點,如圖中的P0。
    2.把所有點的座標平移一下,使 P0 作爲原點,如上圖。
    3.計算各個點相對於 P0 的幅角 α ,按從小到大的順序對各個點排序。當 α 相同時,距離 P0 比較近的排在前面。例如上圖得到的結果爲 P1,P2,P3,P4,P5,P6,P7,P8。我們由幾何知識可以知道,結果中第一個點 P1 和最後一個點 P8 一定是凸包上的點。
    (以上是準備步驟,以下開始求凸包)
    以上,我們已經知道了凸包上的第一個點 P0 和第二個點 P1,我們把它們放在棧裏面。現在從步驟3求得的那個結果裏,把 P1 後面的那個點拿出來做當前點,即 P2 。接下來開始找第三個點:
    4.連接P0和棧頂的那個點,得到直線 L 。看當前點是在直線 L 的右邊還是左邊。如果在直線的右邊就執行步驟5;如果在直線上,或者在直線的左邊就執行步驟6。
    5.如果在右邊,則棧頂的那個元素不是凸包上的點,把棧頂元素出棧。執行步驟4。
    6.當前點是凸包上的點,把它壓入棧,執行步驟7。
    7.檢查當前的點 P2 是不是步驟3那個結果的最後一個元素。是最後一個元素的話就結束。如果不是的話就把 P2 後面那個點做當前點,返回步驟4。

最後,棧中的元素就是凸包上的點了。
以下爲用Graham掃描法動態求解的過程:

自己了理解的步驟:

1.找到最靠左下的點p

2.把p視爲原點,進行相對極角排序,若極角相同距離近的的靠前。

3.把前兩個點放進棧,每次加點判斷是否是向左轉,不是則將棧頂出棧。

代碼(POJ1228):

int sgn(double x)
{
	if(fabs(x)<eps) return 0;
	else if(x<0) return -1;
	else return 1;
}
struct Point
{
	double x,y;
	Point(){}
	Point(double x,double y):x(x),y(y){}
	Point operator -(const Point& b)const//相減 
	{
		return Point(x-b.x,y-b.y);
	}
	double operator ^(const Point& b)const//叉乘 
	{
		return x*b.y-y*b.x;
	}
	double operator *(const Point& b)const//點乘 
	{
		return x*b.x+y*b.y;
	}		
}p[N],q[N],st[N],c[N];
struct Line
{
    Point s,e;
    Line(){}
    Line(Point _s,Point _e)
    {
        s = _s;
        e = _e;
    }
};
//求兩點間距離 
double dis(Point a,Point b)
{
	return sqrt((a-b)*(a-b));
}
//以c[0]爲原點,按極角排序 
bool cmp(const Point& a,const Point& b)
{
	x=(a-c[0])^(b-c[0]);
	if(sgn(x)==0)
	{
		return dis(c[0],a)<dis(c[0],b);
	}
	else if(x>0) return 1;
	else return 0;
}
//求凸包時判斷是否向左轉,包括共線點 
bool check(Point a,Point b,Point c)
{
	x=(b-a)^(c-a); 
	return sgn(x)<0;
}
//將點按相對極角排序 
void sort_point(Point *p,int n)
{
	pos=0;		
	for(int i=0;i<n;i++)
		if(p[i].y<p[pos].y || (p[i].y==p[pos].y&&p[i].x<p[pos].x))
			pos=i;
	swap(p[0],p[pos]);
	for(int i=0;i<n;i++)
		c[i]=p[i];
	sort(c+1,c+n,cmp);
	for(int i=0;i<n;i++)
		p[i]=c[i];	
}
//求凸包 
void getconv(Point *p,int& n)
{
	st[0]=p[0];
	st[1]=p[1];
	top=1;
	for(int i=2;i<n;i++)
	{
		//必須向左轉,共線點不算時,check函數加上“=”即可 
		while(top>1&&check(st[top-1],st[top],p[i])) 
			top--;			
		st[++top]=p[i];
	}
	n=top;		
}

 

2.對踵點對:凸包上彼此距離最遠的點對。

參考博客:https://blog.csdn.net/ACMaker/article/details/3561145

如果兩個點 pq (屬於 P) 在兩條平行切線上, 那麼他們就形成了一個對踵點對。

兩條不同的平行切線總是確定了至少一對的對踵點對。 根據線與多邊形的相交方式, 呈現出三種情況:

  1. “點-點”對踵點對
  2. “點-邊”對踵點對
  3. “邊-邊”對踵點對

情況1如圖所示, 發生在切線對與多邊形只有兩個交點的時候。 途中的黑點構成了一個對踵點對。

情況2如圖所示,發生在其中一條切線與多邊形交集爲其一條邊, 並且另一條切線與多邊形的切點唯一的時候 。 此處注意這種切線的存在必然包含兩個不同“點-點”對踵點對的存在。

情況3發生在切線與多邊形交於平行邊的時候。 這種情況下, 切線同樣確定了四個不同的“點-點”對踵點對。

2.旋轉卡殼

參考博客:https://blog.csdn.net/ACMaker

https://www.cnblogs.com/xdruid/archive/2012/07/01/2572303.html

自己理解:旋轉卡殼完全就是在凸包(凸多邊形)上的算法,卡殼就是找一對對踵點,然後做平行線,這樣就把凸多邊形卡住了。然後,同時一起旋轉這對平行線,直到有一條線和邊重合。由於點和邊組成的面積大小和對踵點對都按逆時針變化,我們就可以很快的求出全部對踵點對,然後根據對踵點對求一些譬如平面上最近/遠的點對,凸包的長/寬,兩凸包的最近/遠距離等等。

自己理解步驟:

1.找出ymin和ymax點

2.枚舉邊和對踵點直到某個特徵變化(例如距離變小,角度變小等)

3.記下對踵點或者直接更新答案

代碼(POJ3608):

int sgn(double x)
{
	if(fabs(x)<eps) return 0;
	else if(x<0) return -1;
	else return 1;
}
struct Point
{
	double x,y;
	Point(){}
	Point(double x,double y):x(x),y(y){}
	Point operator -(const Point& b)const//相減 
	{
		return Point(x-b.x,y-b.y);
	}
	double operator ^(const Point& b)const//叉乘 
	{
		return x*b.y-y*b.x;
	}
	double operator *(const Point& b)const//點乘 
	{
		return x*b.x+y*b.y;
	}		
}p[N],q[N],st[N],c[N];
struct Line
{
    Point s,e;
    Line(){}
    Line(Point _s,Point _e)
    {
        s = _s;
        e = _e;
    }
};
double dis(Point a,Point b)
{
	return sqrt((a-b)*(a-b));
}
bool cmp(const Point& a,const Point& b)
{
	x=(a-c[0])^(b-c[0]);
	if(sgn(x)==0)
	{
		return dis(c[0],a)<dis(c[0],b);
	}
	else if(x>0) return 1;
	else return 0;
}
//線段L上,距離P最近的點 
Point NearestPointToLineSeg(Point P,Line L)
{
    Point result;
    double t = ((P-L.s)*(L.e-L.s))/((L.e-L.s)*(L.e-L.s));
    if(t >= 0 && t <= 1)
    {
        result.x = L.s.x + (L.e.x - L.s.x)*t;
        result.y = L.s.y + (L.e.y - L.s.y)*t;
    }
    else
    {
        if(dis(P,L.s) < dis(P,L.e))
            result = L.s;
        else result = L.e;
    }
    return result;
}
//點p0到線段p1p2上的點的最近距離
double pointtoseg(Point p0,Point p1,Point p2)
{
    return dis(p0,NearestPointToLineSeg(p0,Line(p1,p2)));
}
//平行線段p0p1和p2p3的距離
double dispallseg(Point p0,Point p1,Point p2,Point p3)
{
    double ans1 = min(pointtoseg(p0,p2,p3),pointtoseg(p1,p2,p3));
    double ans2 = min(pointtoseg(p2,p0,p1),pointtoseg(p3,p0,p1));
    return min(ans1,ans2);
}
//得到向量a1a2和b1b2的位置關係
double Get_angle(Point a1,Point a2,Point b1,Point b2)
{
    Point t = b1 - ( b2 - a1 );
    return (a2-a1)^(t-a1);
} 
//求凸包時比較 
bool check(Point a,Point b,Point c)
{
	x=(b-a)^(c-a);
	return sgn(x)<=0;
}
//將點按相對極角排序 
void sort_point(Point *p,int n)
{
		pos=0;		
		for(int i=0;i<n;i++)
		{
			scanf("%lf%lf",&p[i].x,&p[i].y);
			if(p[i].y<p[pos].y || (p[i].y==p[pos].y&&p[i].x<p[pos].x))
				pos=i;
		}
		swap(p[0],p[pos]);
		for(int i=0;i<n;i++)
			c[i]=p[i];
		sort(c+1,c+n,cmp);
		for(int i=0;i<n;i++)
			p[i]=c[i];	
}
//求凸包 
void getconv(Point *p,int& n)
{
	st[0]=p[0];
	st[1]=p[1];
	top=1;
	for(int i=2;i<n;i++)
	{
		//逆時針必須向左轉 
		while(top>1&&check(st[top-1],st[top],p[i])) 
			top--;			
		st[++top]=p[i];
	}	
	n=top+1;
	for(int i=0;i<n;i++)
		p[i]=st[i];
}
double getarea(Point a,Point b,Point c)
{
	return (a-c)^(b-c);	
}

//旋轉卡殼,求兩凸包間最近距離
//枚舉一個凸包的邊,去找另一凸包中的最遠點 
double rc(Point *p,int np,Point *q,int nq)
{

	double ans=1e40,tmp;
	int pp=0,qq=0,temp;
	//ymin即0
	//找ymax 
	for(int i=0;i<nq;i++)
		if(sgn(q[i].y-q[qq].y)>0)
			qq=i;
	for(int i=0;i<np;i++)
	{
         //兩種寫法,都是看距離能不能變遠 
		//while(sgn(tmp = get_angle(p[pp],p[(pp+1)%np],q[qq],q[(qq+1)%nq])) < 0 )
        while(sgn(tmp = getarea(p[pp],p[(pp+1)%np],q[qq])-getarea(p[pp],p[(pp+1)%np],q[(qq+1)%nq]))<0) 
		    qq = (qq + 1)%nq;
        //分類討論,不能直接求點到線段的距離,因爲距離該點最近的點可能不在線段上,
		//即過點作垂線時,垂足不在線段上    
        if(sgn(tmp) == 0)
            ans = min(ans,dispallseg(p[pp],p[(pp+1)%np],q[qq],q[(qq+1)%nq]));
        else ans = min(ans,pointtoseg(q[qq],p[pp],p[(pp+1)%np]));
		pp=(pp+1)%np;
	}
	return ans;
} 

3.半平面交

參考博客:https://blog.csdn.net/qq_40861916/article/details/83541403

https://www.cnblogs.com/Harry-bh/p/9998850.html

定義:我們知道一條直線可以把平面分爲兩部分,其中一半的平面就叫半平面。半平面交,就是多個半平面的相交部分。我們在學習線性規劃時就有用過。(例如,對於一個半平面,我們可以用直線方程式如:ax+by>=c 表示,更常用的是用直線表示。)

應用:

1.求解一個區域,可以看到給定圖形的各個角落。(多邊形的核:如果多邊形中存在一個區域使得在區域中可以看到多邊形中任意位置(反之亦然),則這個區域就是多邊形的核。可以用半平面交來求解。)
2.求可以放進多邊形的圓的最大半徑,等等。

步驟:

1.以逆時針方向建邊
2.對線段根據極角排序
3.去除極角相同的情況下,位置在右邊的邊
4.用雙端隊列儲存線段集合 L,遍歷所有線段
5.判斷該線段加入後對半平面交的影響,(對雙端隊列的頭部和尾部進行判斷,因爲線段加入是有序的。)
6.如果某條線段對於新的半平面交沒有影響,則從隊列中剔除掉。
7.最後剩下的線段集合 L,即使最後要求的半平面交。

自己理解步驟:

1.給點時,需要先按照逆時針方向連邊,得到邊(直線)後,按極角排序。極角相同時,留下最靠左的邊。

2.去重後,將前兩個直線放入隊列中,遍歷邊

3.如果隊尾/首的前兩條直線的交點在加入直線的右側,直接出隊。

4.留在隊列裏的直線就是半平面交。

代碼(POJ3335):

int sgn(double x)
{
	if(fabs(x)<eps) return 0;
	else if(x<0) return -1;
	else return 1;
}
struct Point
{
	double x,y;
	Point(){}
	Point(double x,double y):x(x),y(y){}
	Point operator -(const Point& b)const//相減 
	{
		return Point(x-b.x,y-b.y);
	}
	double operator ^(const Point& b)const//叉乘 
	{
		return x*b.y-y*b.x;
	}
	double operator *(const Point& b)const//點乘 
	{
		return x*b.x+y*b.y;
	}
};
struct Line
{
	Point s,e;
	double A;
	Line(){}
	Line(Point ss,Point ee)
	{
		s=ss,e=ee;
	}
	void getangle()//獲得極角 
	{
		A=atan2(e.y-s.y,e.x-s.x);
	}
	pair<Point,int> operator &(const Line& b)const//兩直線相對關係 
	{
		Point res=s;
		if(sgn((s-e)^(b.s-b.e))==0)
		{
			if(sgn((b.s-s)^(b.e-s))==0)//兩直線重合 
				return make_pair(res,0);
			else return make_pair(res,1);//兩直線平行 
		}
		double t=((s-b.s)^(b.s-b.e))/((s-e)^(b.s-b.e));
		res.x+=(e.x-s.x)*t;
		res.y+=(e.y-s.y)*t;
		return make_pair(res,2);//兩直線相交 
	}
};
Point ps[N];
Line ls[N],q[N];
double x;
int n;
double dis(Point a,Point b)
{
	return sqrt((a-b)*(a-b));
}
//按極角排序,相同時,靠左(或靠下)的在前面 
bool hpicmp(Line a,Line b)
{
	if(sgn(a.A-b.A)==0)	
		return sgn((a.s-b.s)^(b.e-b.s))<=0;		
	else return a.A<b.A;
}
//判斷l1與l2的交點是否在l3的右側 
bool onright(Line l1,Line l2,Line l3)
{
	Point p=(l1&l2).first;
	x=((l3.e-l3.s)^(p-l3.s));
	if(sgn(x)<0) return 1;
	else return 0;
}
//半平面交求核 
bool hpi()
{
	int he=0,ta=1,cnt=0;
    sort(ls,ls+n,hpicmp);
    //去重,只保留極角互不相同的直線 
	cnt=1;
    for(int i = 1;i < n;i++)
        if(sgn(ls[i].A-ls[i-1].A)>0)
        	ls[cnt++]=ls[i];
	//將前兩條直線放進隊列		 
    q[0]=ls[0];
    q[1]=ls[1];
	for(int i=2;i<cnt;i++)
	{ 
		//說是判斷共線,我覺得沒必要 
		/*
		if(he<ta&&sgn((q[ta].e-q[ta].s)^(q[ta-1].e-q[ta-1].s))==0
		||sgn((q[he].e-q[he].s)^(q[he+1].e-q[he+1].s))==0)
			return 0;
		*/
		//如果交點在要加入直線的右側,則出隊 
		while(he<ta&&onright(q[ta-1],q[ta],ls[i])) ta--;
		while(he<ta&&onright(q[he],q[he+1],ls[i])) he++;
		q[++ta]=ls[i];
	}
	while(he<ta&&onright(q[ta-1],q[ta],q[he])) ta--;
	while(he<ta&&onright(q[he],q[he+1],q[ta])) he++;
	if(ta-he+1<=2) return 0;
	else return 1;
}


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章