凸包的几种算法 主要Graham-Scan算法的水平序法 另加poj113 wall的解题

在说这个题目之前,我想给大家介绍一些这几天我了解到的有关凸包的知识:

1、Gift-Wrapping(卷包裹算法)

这个算法在《算法艺术》上说的很清楚了(p391-393),如果理解的还不是很清楚,在这里讲解的特别好,特别清楚,由于这个很简单,所以就不谈论它了。我对这个算法的理解是:时间复杂度是O(N*H),N是点的个数,H是在凸包上点的个数。所以卷包裹算法很适合凸包上的点很少的时候,通常情况下,随机数据会很快。但是如果构造出的凸包上的点很多的时候,它就会很慢了,不如说,所有点都在一个园上的时候。

2、Graham-Scan算法

《算法艺术》(p393-396)。我觉得上面讲的很详细很详细了。同样,如果理解的不好可以看看这里

这个算法是基于一个有序的点,所以必须要排序的。关键就在这里,排序分两种:极角排序和水品排序。估计大多数人都会第一种。

先说第一种。首先要找到一个一定在凸包上的点p0,这个点怎么找?自己想想吧。然后就以这个点为基点,对所有点按照极角大小排序。这里注意一下,如果极角相等,那么就按距离从小到大排序。这个是防止共线问题,关于这个下面好好讨论。然后把排序后的p0,p1,p2放入栈中,接着就是遍历每个点了,始终保证非“右手”方向就好了,具体实现去网上搜,很多。

然后就是第二种的水平序。排序准则是先按y大小排序,如果y相等,就按x排序。相对于第一种,这个排序简单的多。然后和上面的思想一样,只不过要分两步,右链和左链,先做右链,从0到排序最后点,然后再反过来,进行左链。具体的细节可以参考下面的代码。可能你在想这样有什么好处呢,我要说的是,这样可以很完美的解决共线问题。

我对这个算法的理解是:对于随机数据,可能没有卷包裹算法快。但是也是可以接受的。之所以喜欢的原因是,它可以完美的解决共线这个一直让人头大的问题。

3、Melkman算法

首先要说的是:很多人都认为这个是最好的算法。这个算法可以在个点有序的前提下,每次获得一个点就可以将先前的凸包改造成新的凸包,因此,这个是一个在线算法,它有着其他算法无法比拟的优势。1987年Melkman提出的的凸包算法,它不再使用堆栈了,转而使用双向表,这为凸包算法的历史掀开了崭新的一夜。

具体实现我就不说了,相信看过上面几句话的,现在都已经忍不住要学习了。

今天看了一下午《算法艺术》和网上的一些资料,终于又有了一些理解,那就补充一下吧。之前都是模模糊糊的,现在明白了,可能我现在理解的还是错误的,但是我还是要说出来:Melkman算法的前提是“各个点有序”。所以melkman是用来求简单多边形的凸包算法,可以在线性时间求出最小凸包。而其他两种则可以求点集的凸包的算法。如果要用Melkman算法来求点集的凸包,那么首先也是要排序的,通过排序可以形成一个简单的多边形,然后才可以在线性的时间求出最小凸包。

所以最终得到的结论是:求点集的凸包,时间复杂度的底线是O(nlogn)。

最后讨论一下共线的问题:

假设有这么几个点

0 0

0 1

0 2

2 2

1 1

和这几个点

1 1

2 2

3 3

4 4

对于上面的两组数据,如果用卷包裹算法和Graham-Scan的极角排序法做,要求只输出凸包上的定点,会出现什么样的问题??

如果用Graham-Scan的水平序来写,会不会出现同样的问题??

大家可以好好想想!!!!

还有很多凸包方面的算法,比如Jarvis步进法、增量、溶解、QuickHull等,这些多用于数学中,实践意义不大,所以就不说了。有兴趣了解凸包算法的发展史,可以看看蓝点大神的《漫话二维凸包》。。



现在说poj1113 wall:

题目:poj1113 wall

意思就是给一个城堡的墙角的坐标,让你用最短的围墙围起来,并且围墙离城堡的距离不能少于L。

说白了就是一个很裸的凸包问题。对于那个不能小于L,等作出了凸包,然后把边向外移动L,自己画画就看出来了,每个角处的弧,加起来刚好是一个半径为L的圆,所以最终结果就是圆的周长加上凸包的周长。

值得注意的是:由于精度问题,对于那些共线的点,应该取最远处那个计算长度,不然可能会一直WA,这个就体现了Graham-Scan算法的水平序的优势了。大家好好品味。。

比如上面提到的第一组数据,计算的顶点应该是0 0,0 2和2 2,而不是所有的点,即使所有的点都在凸包上,如果计算所有的点,误差就会大很多,肯能就是一直WA的。

代码如下:


//突然想用类写个模板,由于c++没学好,所以弄了一上午才弄万,不好的地方欢迎大家指点。。。。。
#include <stdio.h>
#include <iostream>
#include <algorithm>
#include <string.h>
#include <math.h>

using namespace std;
const int N=1100;

struct Node{
	int x,y;
	bool operator<(Node a)const{
	return y<a.y||(y==a.y&&x<a.x);
	}
};

class Graham_Scan{
public:

	Graham_Scan(int r){
		num_node=r;
		for(int i=0;i<=num_node;i++)
			visit[i]=true;
	}
	bool judg(){//判断给的点是否符合条件能够成凸包
		if(num_node<2)
			return 0;
	}
	void init();
	void fun();
	void print_node(bool jud);//输出凸包上的点,如果jud真就输出所有点,否则就输出定点
	double print_per();//输出凸包的周长
private:
	int num_node,stack_all[N],stack[N],top_all,top;
	Node node[N];
	double dis(Node a,Node b);
	void Graham_scan();//Graham_Scan算法水平序的实现
	bool visit[N];
	int turn(int a,int b,int c);
};

void Graham_Scan::init()
{
	for(int i=0;i<num_node;i++)
		scanf("%d%d",&node[i].x,&node[i].y);
}

double Graham_Scan::dis(Node a,Node b)
{
	double c;
	c=(b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y);
	return sqrt(c);
}

int Graham_Scan::turn(int a,int b,int c)
{
	return (node[b].x-node[a].x)*(node[c].y-node[a].y)-(node[b].y-node[a].y)*(node[c].x-node[a].x);
}

void Graham_Scan::fun()
{
	Graham_scan();
}

void Graham_Scan::Graham_scan()
{
	int i;
	sort(node,node+num_node);
	stack_all[0]=stack[0]=0;
	stack_all[1]=stack[1]=1;
	top=top_all=1;
	visit[1]=false;
	//执行右链,同时标记已经在右链上的点
	for(i=2;i<num_node;i++)
	{
		while(top>0&&turn(stack[top-1],stack[top],i)<=0)	
			top--;
		stack[++top]=i;

		while(top_all>0&&turn(stack_all[top_all-1],stack_all[top_all],i)<0)
		{
			visit[stack_all[top_all]]=true;
			top_all--;
		}
		stack_all[++top_all]=i;
		visit[i]=false;
	}
	//现在的top,top_all点一定是最上边,最右边的那个点
	//执行左链,逃过在右链上已经在凸包上的点,看别人写的时候没有跳过,不过算法书上说是要跳过的,所以还是跳过吧
	int top1=top,top1_all=top_all;
	stack[++top]=num_node-2;
	stack_all[++top_all]=num_node-2;
	for(i=num_node-3;i>=0;i--)
	{
		while(visit[i]&&top>top1&&turn(stack[top-1],stack[top],i)<=0)
			top--;
		stack[++top]=i;

		while(visit[i]&&top_all>top1_all&&turn(stack_all[top_all-1],stack_all[top_all],i)<0)
			top_all--;
		stack_all[++top_all]=i;
	}	
	//现在的top,top_all点一定是0点
}

void Graham_Scan::print_node(bool jud)
{
	if(jud)
	{
		for(int i=0;i<top_all;i++)
			cout<<node[stack_all[i]].x<<" "<<node[stack_all[i]].y<<endl;
	}
	else
		for(int i=0;i<top;i++)
			cout<<node[stack[i]].x<<" "<<node[stack[i]].y<<endl;
}

double Graham_Scan::print_per()
{
	double ans=0.0;
	for(int i=1;i<=top;i++)
	{
		ans+=dis(node[stack[i]],node[stack[i-1]]);
	}
	return ans;
}

int main()
{
	int n,l;
	while(~scanf("%d%d",&n,&l))
	{
		double ans=acos(-1.0)*2*l;
		Graham_Scan solve(n);
		if(solve.judg())
		{
			cout<<"No\n"<<endl;
			continue;
		}
		solve.init();
		solve.fun();
		ans+=solve.print_per();
		printf("%0.lf\n",ans);
	}
}

参考资料:

《算法艺术与信息学竞赛》  刘汝佳 黄亮

《算法导论》 Thomas H.Cormen、Charles E.Leiserson、Ronald L.Rivest、Clifford Stein 潘金贵 顾铁成等人译

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是 Graham-Scan 算法的 C++ 实现,用于求解凸包问题: ```cpp #include <bits/stdc++.h> using namespace std; struct Point { int x, y; }; // 按照 x 坐标从小到大排序,若 x 坐标相等,则按照 y 坐标从小到大排序。 bool cmp(Point a, Point b) { if (a.x == b.x) return a.y < b.y; return a.x < b.x; } // 计算叉积。 int cross(Point a, Point b, Point c) { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } // Graham-Scan 算法求解凸包。 vector<Point> grahamScan(vector<Point> &points) { int n = points.size(); if (n <= 1) return points; sort(points.begin(), points.end(), cmp); vector<Point> hull(2 * n); int k = 0; // 构建下凸壳。 for (int i = 0; i < n; ++i) { while (k >= 2 && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) k--; hull[k++] = points[i]; } // 构建上凸壳。 for (int i = n - 2, t = k + 1; i >= 0; --i) { while (k >= t && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) k--; hull[k++] = points[i]; } // 去除重复点。 hull.resize(k - 1); return hull; } int main() { // 测试数据。 vector<Point> points = {{0, 3}, {1, 1}, {2, 2}, {4, 4}, {0, 0}, {1, 2}, {3, 1}, {3, 3}}; vector<Point> hull = grahamScan(points); // 输出凸包的顶点。 for (int i = 0; i < hull.size(); ++i) { cout << "(" << hull[i].x << ", " << hull[i].y << ")" << endl; } return 0; } ``` 注意点: 1. 为了方便起见,我直接使用了 C++11 的新特性,使用 vector 存储点集,如果你使用的是较老的编译器,可以使用数组代替 vector。 2. 实现中为了方便起见,我使用了三个点 $A(a_x,a_y)$、$B(b_x,b_y)$、$C(c_x,c_y)$ 的叉积 $cross(A,B,C)$ 表示向量 $\vec{AB}$ 和 $\vec{AC}$ 的叉积。当叉积 $cross(A,B,C)>0$ 时,表示 $\vec{AB}$ 在 $\vec{AC}$ 的逆时针方向;当叉积 $cross(A,B,C)<0$ 时,表示 $\vec{AB}$ 在 $\vec{AC}$ 的顺时针方向;当叉积 $cross(A,B,C)=0$ 时,表示 $\vec{AB}$ 和 $\vec{AC}$ 共线。 3. 为了避免精度误差,最好使用整数类型存储坐标,如 int 类型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值