凸包问题--卷包裹法

写在前面---

现在我们学习凸包的有关算法(终于开始凸包的学习了)。也算是初步接触计算几何了。

凸包的概念是在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包,表示为:

S=\bigcap_{K is convex}^{X\subseteq K\subseteq V}K

在二维空间中,凸包也可以形象的理解为最小的包含所有点的凸多边形。

X的凸包可以用X=(x1,x2,x3,...,x12)的线性组合来构造,即:S=\{\sum_{j=1}^{n}t_jx_j|x_j\in X,\sum_{j=1}^nt_j=1,t_j\in[0,1]\}

在计算几何中,凸包问题是一类比较常见的问题,不仅在计算机程序设计竞赛中,在统计等现实问题中也有着广泛的应用。

现在已经证明了凸包算法的时间复杂度下界是O(n*logn),但是当凸包的顶点数h也被考虑,Krik-patrick和Seidel的剪枝搜索算法可以达到O(n*logh),在渐进意义下达到最优。最常用的凸包算法是Graham扫描法和Jarvis步进法。本章主要学习Graham扫描法,其正确性的证明和Jarvis步进法可以参考《算法导论》

卷包裹法

原理:

卷包裹法的原理比较简单:先找一个最边缘的点(一般位于最下方,如果有多个点,则选择最左方的点),假设有一条绳子,以该点为端点向右边逆时针旋转直到碰到另一个点为止,此时找出凸包的一条边;然后再用新找到的点作为端点,继续旋转绳子,找到下一个端点;重复这一步骤直至围成一个凸多边形,即可得到这个点集的凸包。卷包裹法的时间复杂度为O(n^2)。

步骤:

Step1:选择点集中最下面的点,如果有多个,则选择最下面的点中最左边的一个,所选择的点是凸包的第一个点。

Step2:以水平向右的方向作为初始射线方向,逆时针旋转,选择第一条在初始射线之上的射线作为当前射线,当前射线经过凸包的第二个点。

Step3:以当前射线为基准,继续逆时针旋转找到最靠近该射线的一条射线,从而找到凸包的另一个点。把这条射线作为当前射线,这个过程一直继续,直至回到第一个点。

大概的过程就是这个图所示了(灵魂画图)

对于旋转射线这个操作,当然不能真的旋转,我们可以用以下几种方法来实现:

A.把每条射线与其他n-2条射线比较,每步的效率是O(n^2)。

B.通过计算各射线与射线AB的夹角的方式,效率为O(n),但由于计算夹角存在浮点运算,会导致浮点误差。

C.直接使用叉积运算求各条射线斜率的相对关系,从而得到另一条射线,效率是O(n),不存在浮点误差。

哪个方法好一目了然,所以你选择C项,并将其所对应的答题卡涂成黑色。

如果出现寻找的射线上有多个点的时候,一般只是用和保留距离当前端点最远的哪一点。

实现:

看起来似乎挺简单的,实现一下也不难:

写一个板子,输入n个点,然后输出它的凸包的边长。

这个代码和样例模拟的就是上面说的步骤了:

首先得到最下面的点,发现有多个最下面的点,得到最左边的。

三点入栈,然后判断是否严格左转。发现不是,H出栈。

如图所示,后面的正常入栈,直到C点的时候,发现不是严格左转,所以D点出栈,凸包变成了如图所示,连接CE点。

再找到B点,发现又不是严格左转,C出栈,判断E,E出栈,判断F,GFE构成严格左转,B入栈。凸包变成了IGFB.

剩下的就是还是一步步的走,然后判断即可。

最后得出的凸包为LGFAL。

Code:

//#pragma comment(linker, "/STACK:1024000000,1024000000")
  
#include<stdio.h>
#include<string.h> 
#include<math.h> 
   
#include<map>  
//#include<set>
#include<deque> 
#include<queue> 
#include<stack> 
#include<bitset>
#include<string> 
#include<fstream>
#include<iostream> 
#include<algorithm> 
using namespace std; 
  
#define ll long long 
#define Pair pair<int,int>
//#define max(a,b) (a)>(b)?(a):(b)
//#define min(a,b) (a)<(b)?(a):(b)
#define clean(a,b) memset(a,b,sizeof(a))// 水印
//std::ios::sync_with_stdio(false);
//  register
const int MAXN=1e3+10;
const int INF32=0x3f3f3f3f;
const ll INF64=0x3f3f3f3f3f3f3f3f;
const ll mod=1e9+7;
const double EPS=1.0e-8;
const double PI=acos(-1.0);

struct Point{
	double x,y;
	Point(double _x=0,double _y=0){
		x=_x;y=_y;
	}
	friend Point operator + (const Point &a,const Point &b){
		return Point(a.x+b.x,a.y+b.y);
	}
	friend Point operator - (const Point &a,const Point &b){
		return Point(a.x-b.x,a.y-b.y);
	}
	friend double operator ^ (Point a,Point b){//向量叉乘 
		return a.x*b.y-a.y*b.x;
	}
};
struct V{
	Point start,end;double ang;
	V(Point _start=Point(0,0),Point _end=Point(0,0),double _ang=0.0){
		start=_start;end=_end;ang=_ang;
	}
	friend V operator + (const V &a,const V &b){
		return V(a.start+b.start,a.end+b.end);
	}
	friend V operator - (const V &a,const V &b){
		return V(a.start-b.start,a.end-b.end);
	}
};
struct Circle{
	double r;
	Point centre;
	Circle(Point _centre=Point(0,0),double _r=0){
		centre=_centre;r=_r;
	}
};
Point Dots[MAXN];
Point stk[MAXN];int top;
int n;

double Distance(Point a,Point b){
	return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double Parellel(double key){
	return fabs(key)<EPS?0:key;
}
int Cmp(Point a,Point b){
	double res=Parellel((a-Dots[1])^(b-Dots[1]));
	if(res>0) return 1;
	if(res==0&&Distance(a,Dots[1])<Distance(b,Dots[1])) return 1;
	return 0;
}
void Graham(){
	sort(Dots+2,Dots+1+n,Cmp);
	top=2;stk[1]=Dots[1];stk[2]=Dots[2];
	for(int i=3;i<=n;++i){
		while(top>=2&&((stk[top]-stk[top-1])^(Dots[i]-stk[top-1]))<EPS) --top;
		stk[++top]=Dots[i];
	}stk[top+1]=stk[1];
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%lf%lf",&Dots[i].x,&Dots[i].y);
	}int k=1;
	for(int i=2;i<=n;++i){
		if(Dots[i].y<Dots[k].y||(Dots[i].y==Dots[k].y&&Dots[i].x<Dots[k].x)){
			k=i;
		}
	}swap(Dots[1],Dots[k]);
	Graham();
	double ans=0;
	for(int i=1;i<=top;++i){
		ans+=Distance(stk[i],stk[i+1]);
	}printf("%lf\n",ans);
}

/*
9 
200 400
300 400
300 300
400 300
400 400
500 400
500 200
350 200
200 200

output 1000.0
*/


还有一个写法,选择一个必定在凸包上的一个点,然后以该点为中心维护上凸壳和下凸壳。

说一下思路:这种写法是重复遍历两次,每次从1~n点的顺时针,然后是从n~1点的顺时针。sort的时候也只用按坐标排序即可。

这个方法与上面的方法类似,但是可能可以用在其他方面,比如只用得到半个凸壳的题目:https://blog.csdn.net/qq_40482358/article/details/88826317

Code:

//#pragma comment(linker, "/STACK:1024000000,1024000000")
  
#include<stdio.h>
#include<string.h> 
#include<math.h> 
   
#include<map>  
//#include<set>
#include<deque> 
#include<queue> 
#include<stack> 
#include<bitset>
#include<string> 
#include<fstream>
#include<iostream> 
#include<algorithm> 
using namespace std; 
  
#define ll long long 
#define Pair pair<int,int>
//#define max(a,b) (a)>(b)?(a):(b)
//#define min(a,b) (a)<(b)?(a):(b)
#define clean(a,b) memset(a,b,sizeof(a))// 水印
//std::ios::sync_with_stdio(false);
//  register
const int MAXN=1e3+10;
const int INF32=0x3f3f3f3f;
const ll INF64=0x3f3f3f3f3f3f3f3f;
const ll mod=1e9+7;
const double PI=acos(-1.0);
const double EPS=1.0e-8;

struct Point{
	double x,y;
	Point(double _x=0,double _y=0){
		x=_x;y=_y;
	}
	friend Point operator + (const Point &a,const Point &b){
		return Point(a.x+b.x,a.y+b.y);
	}
	friend Point operator - (const Point &a,const Point &b){
		return Point(a.x-b.x,a.y-b.y);
	}
	friend double operator ^ (Point a,Point b){//向量叉乘 
		return a.x*b.y-a.y*b.x;
	}
}x[MAXN];
struct V{
	Point start,end;double ang;
	V(Point _start=Point(0,0),Point _end=Point(0,0),double _ang=0.0){
		start=_start;end=_end;ang=_ang;
	}
	friend V operator + (const V &a,const V &b){
		return V(a.start+b.start,a.end+b.end);
	}
	friend V operator - (const V &a,const V &b){
		return V(a.start-b.start,a.end-b.end);
	}
};
struct Circle{
	double r;
	Point centre;
	Circle(Point _centre=Point(0,0),double _r=0){
		centre=_centre;r=_r;
	}
};
int ans[MAXN],cnt;
int sta[MAXN],tail;
int n;

int Cmp(Point a,Point b){
	return (a.y<b.y||a.y==b.y&&a.x<b.x);
}
int CrossLeft(Point p1,Point p2,Point p3){//是否严格左转,共线不算左转 
	//向量叉乘法判断三点是否左转。 
	//return ((p3.x-p1.x)*(p2.y-p1.y)-(p2.x-p1.x)*(p3.y-p1.y))<0;
	//上面公式写成叉乘形式为:
	return ((p3-p2)^(p2-p1))<0;
}
void Jarvis(){
	tail=cnt=0;sort(x,x+n,Cmp);
	sta[tail++]=0,sta[tail++]=1;
	for(int i=2;i<n;++i){
		while(tail>1&&CrossLeft(x[sta[tail-1]],x[sta[tail-2]],x[i])==0) --tail;
		sta[tail++]=i;
	}
	for(int i=0;i<tail;++i){
		ans[cnt++]=sta[i];
	}tail=0;sta[tail++]=n-1;sta[tail++]=n-2;
	for(int i=n-3;i>=0;--i){
		while(tail>1&&CrossLeft(x[sta[tail-1]],x[sta[tail-2]],x[i])==0) --tail;
		sta[tail++]=i;
	} 
	for(int i=0;i<tail;++i){
		ans[cnt++]=sta[i];
	}
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;++i){
		scanf("%lf%lf",&x[i].x,&x[i].y);
	}Jarvis();
//	for(int i=0;i<cnt-1;++i){
//		cout<<x[ans[i]].x<<" "<<x[ans[i]].y<<endl;
//	}cout<<endl;
	//double res=4*acos(0.0)*r;
	double res=0;
	for(int i=0;i<cnt-1;++i){
		res+=sqrt((x[ans[i]].x-x[ans[i+1]].x)*(x[ans[i]].x-x[ans[i+1]].x)+(x[ans[i]].y-x[ans[i+1]].y)*(x[ans[i]].y-x[ans[i+1]].y));
	}printf("%lf\n",res);
}

/*
9 
200 400
300 400
300 300
400 300
400 400
500 400
500 200
350 200
200 200

output 1000.0
*/
好的,以下是Graham-Scan算的C++实现。 ```cpp #include <bits/stdc++.h> using namespace std; struct Point { int x, y; }; // 叉积 int cross(const Point &a, const Point &b, const Point &c) { return (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x); } // 求凸包,返回点集 vector<Point> convexHull(vector<Point> &points) { int n = points.size(); // 先按照x坐标从小到大排序,x坐标相同按y坐标从小到大排序 sort(points.begin(), points.end(), [](const Point &a, const Point &b) { return a.x == b.x ? a.y < b.y : a.x < b.x; }); // 求下凸壳 vector<Point> lower; for (int i = 0; i < n; ++i) { while (lower.size() >= 2 && cross(lower[lower.size() - 2], lower.back(), points[i]) <= 0) lower.pop_back(); lower.push_back(points[i]); } // 求上凸壳 vector<Point> upper; for (int i = n - 1; i >= 0; --i) { while (upper.size() >= 2 && cross(upper[upper.size() - 2], upper.back(), points[i]) <= 0) upper.pop_back(); upper.push_back(points[i]); } // 合并下凸壳和上凸壳 vector<Point> ans(lower); ans.insert(ans.end(), upper.begin() + 1, upper.end() - 1); return ans; } int main() { int n; cin >> n; vector<Point> points(n); for (int i = 0; i < n; ++i) cin >> points[i].x >> points[i].y; vector<Point> hull = convexHull(points); cout << "Convex Hull:" << endl; for (const Point &p : hull) cout << p.x << " " << p.y << endl; return 0; } ``` 在上述代码中: - `Point` 结构体表示一个点,包含 `x` 和 `y` 坐标; - `cross` 函数用于计算向量 $\overrightarrow{AB}$ 和 $\overrightarrow{AC}$ 的叉积,即 $(\overrightarrow{AB} \times \overrightarrow{AC})$,结果为正表示 $\overrightarrow{AB}$ 在 $\overrightarrow{AC}$ 的逆时针方向,结果为负表示 $\overrightarrow{AB}$ 在 $\overrightarrow{AC}$ 的顺时针方向,结果为 $0$ 表示 $\overrightarrow{AB}$ 与 $\overrightarrow{AC}$ 共线; - `convexHull` 函数用于求解凸包,输入为点集 `points`,输出为凸包点集; - 在函数内部,先按照 x 坐标从小到大排序,x 坐标相同按 y 坐标从小到大排序; - 接着求下凸壳,利用单调栈维护; - 再求上凸壳,同样利用单调栈维护; - 最后将下凸壳和上凸壳合并,得到最终的凸包点集。 希望能对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值