凸包/旋转卡壳/半平面交学习总结

 

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;
}


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值