凸包问题算法总结并将算法过程可视化

什么是凸包?

百度百科上是这样说的:
凸包(Convex Hull)是一个计算几何(图形学)中的概念。
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,…Xn)的凸组合来构造.

老师是用一个形象的例子来解释的:
在板子上钉钉子,钉了一定数量后用一个橡皮筋撑大到足以包括所有钉子,然后松手,橡皮筋所包围的最外一侧钉子形成的多边形就是这一堆钉子的凸包。这些外侧的钉子称为极点,他们形成的边称为极边

凸包问题正是计算几何的核心,也是开头的第一课,下面总结了老师介绍的几种得到凸包的算法,大多都是依据数据结构与算法中的排序算法推导出来的,老师管这种根据一种算法推导另一种算法时间复杂度的方法叫曹冲称象

排序算法的回顾看https://blog.csdn.net/derbi123123/article/details/104318134

下面所有实现的代码都加入了EasyX库https://easyx.cn/中的绘图函数让凸包算法的过程可视化

0.首先来看凸包重要的性质:

在这里插入图片描述
老师说眼睛就是我们发现问题和答案的直接来源,显然在这里对于每个极点我们看到总有一条通过极点的有向直线可以让所有的点都位于这条有向直线的同一侧,按照我们通用的逆时针方向,所有的点都会在这条有向直线的左侧。这正是称之为极点的原因。

在这里插入图片描述
把极点性质中的一条直线推广到两相邻极点间的连线,我们不难发现两相邻极点间的连线(逆时针方向),所有的点都会在这条有向直线的左侧,这就是称为极边的原因。

1.利用极点的性质的算法:

(1)算法说明:

这里老师仍然举了一个形象的例子:
大家知道在我们画水粉画的时候往往只有12或者24色,但大师却能用仅有的这数十种颜色画出绚丽的作品,大师正是用这些工厂生产的颜料通过不同的配比得到各种各样的颜色(也就是RGB中占各不同百分比),这恰好就是凸包问题,那些工厂生产的颜料并不是随意生产的,而是因为那些颜色的颜料没办法用现有的颜料配出来,这些颜色正是极点。

所以我们用这个例子体现出的极点性质来说明我们的策略:
对于每个极点我们都不可能发现它被点集中其它的三个点形成的三角形所包围。所以我们可以遍历点集中能够构造出来的所有三角形,去检验其它点是否在当前三角形中,如果在就不是极点。最终剩下的就是极点。

(2)具体代码:

#include <graphics.h>
#include <iostream>
#include <time.h>
#include <conio.h>
using namespace std;
struct Point{
	int x,y;
	bool isExtremePoint;//是否为极点
};
int Area2(Point a,Point b,Point c);
bool ToLeft(Point a,Point b,Point c);
bool InTriangle(Point a,Point b,Point c,Point d);
void IsExtremePoint(Point points[],int n);
int main()
{
	initgraph(600,600);//创建一个600*600的窗口
	srand((unsigned)time(NULL));//产生随机种子
	Point points[10];
	for(int i=0;i<10;i++){
		points[i].x=rand()%600;
		points[i].y=rand()%600;
		//画所有的点
		setlinecolor(RGB(255,0,0));
		setfillcolor(RGB(255,0,0));
		fillcircle(points[i].x,points[i].y,3);
	}
	IsExtremePoint(points,10);
	//找到极点
	POINT ExtremePoints[10];
	int ExtremePointNum=0;
	for(int i=0;i<10;i++){
		if(points[i].isExtremePoint){
			ExtremePoints[ExtremePointNum].x=points[i].x;
			ExtremePoints[ExtremePointNum].y=points[i].y;
			ExtremePointNum++;
		}
	}
	//连接橡皮筋
	setlinecolor(RGB(0,0,255));
	polygon(ExtremePoints, ExtremePointNum);
	getch();
	closegraph();
	return 0;
}
//利用行列式的几何意义,通过判断正负来判断点在直线向量的左侧还是右侧
int Area2(Point a,Point b,Point c){
	return a.x*b.y-a.y*b.x+b.x*c.y-b.y*c.x+c.x*a.y-c.y*a.x;
}
bool ToLeft(Point a,Point b,Point c){
	return Area2(a,b,c)>0;
}
//检查点d,看有没有点落在三角形abc中
bool InTriangle(Point a,Point b,Point c,Point d){
	if(ToLeft(a,b,d)&&ToLeft(b,c,d)&&ToLeft(c,a,d)){
		return true;
	}
}
//判断每个点是否为极点
void IsExtremePoint(Point points[],int n){
	int i,a,b,c;
	for(i=0;i<n;i++)
		points[i].isExtremePoint=true;
	//最笨的方法:遍历所有三角形,看有没有点落在其中
	for(a=0;a<n;a++)
		for(b=a+1;b<n;b++)
			for(c=b+1;c<n;c++)
				for(i=0;i<n;i++){
					if(i==a||i==b||i==c||!points[i].isExtremePoint) 
						continue;
					if(InTriangle(points[a],points[b],points[c],points[i]))
						points[i].isExtremePoint=false;
				}
}

(3)算法分析与评价:

IsExtremePoint函数中我们可以看到我们经历的可怕的四重循环,算法复杂度已经达到了O(n4),显然这在实际问题中是不可行的,不过我们下面的所有改进都是依据这个最直接的算法的性质。此外,这个算法只能保证我们得到所有的极点,并未有其顺序,所以连接时都是按其检测顺序连接的。

Area2ToLeft极为重要,我们后面会经常用ToLeft的返回值正负来判断点在直线向量的左侧还是右侧,这个方法的根据就是二维行列式的几何意义——有向的平行四边形的面积。

2.利用极边的性质的算法:

(1)算法说明:

根据极边的性质,我们可以遍历所有可能的边,检测其余点是否均在当前边的一侧,如果在就是极边,对应的两个端点就是极点,这样我们就可以解决1中无法画出顺序连接极点的问题(每次检测到是极边就画出来)。

(2)具体代码:

#include <graphics.h>
#include <iostream>
#include <time.h>
#include <conio.h>
using namespace std;
struct Point{
	int x,y;
	bool isExtremePoint;
};
int Area2(Point a,Point b,Point c);
bool ToLeft(Point a,Point b,Point c);
void checkEdge(Point S[],int n,int a,int b);
void markEE(Point points[],int n);
int main()
{
	initgraph(600,600);//创建一个600*600的窗口
	srand((unsigned)time(NULL));//产生随机种子
	Point points[10];
	for(int i=0;i<10;i++){
		points[i].x=rand()%600;
		points[i].y=rand()%600;
		setlinecolor(RGB(255,0,0));
		setfillcolor(RGB(255,0,0));
		fillcircle(points[i].x,points[i].y,3);
	}
	markEE(points,10);
	getch();
	closegraph();
	return 0;
}
//利用行列式的几何意义,通过判断正负来判断点在直线向量的左侧还是右侧
int Area2(Point a,Point b,Point c){
	return a.x*b.y-a.y*b.x+b.x*c.y-b.y*c.x+c.x*a.y-c.y*a.x;
}
bool ToLeft(Point a,Point b,Point c){
	return Area2(a,b,c)>0;
}
//检查是否为极边
void checkEdge(Point S[],int n,int a,int b){
	setlinecolor(RGB(0,0,255));
	line(S[a].x,S[a].y,S[b].x,S[b].y);
	bool LEmpty=true,REmpty=true;
	for(int i=0;i<n&&(LEmpty||REmpty);i++){
		if(i!=a&&i!=b){
			ToLeft(S[a],S[b],S[i]) ? LEmpty=false : REmpty=false ;
		}
	}
	Sleep(100);
	if(LEmpty||REmpty){
		S[a].isExtremePoint=S[b].isExtremePoint=true;
	}else{
		setlinecolor(RGB(0,0,0));
		line(S[a].x,S[a].y,S[b].x,S[b].y);
	}
}
//最终实现
void markEE(Point points[],int n){
	int i,a,b;
	for(i=0;i<n;i++)
		points[i].isExtremePoint=false;
	for(a=0;a<n;a++){
		for(b=a+1;b<n;b++){
			checkEdge(points,n,a,b);
		}
	}
}

(3)算法分析与评价:

markEE函数中外两层循环遍历所有边,checkEdge函数遍历其余点,所以时间复杂度达到了O(n3),虽然很大,但是有所进步,下面我们就要用排序算法来做优化了。

3.利用选择排序的方法理解Jarvis March的算法:

(1)算法说明:

选择排序就是每次都从待排序列中找到最大的放入排好序的序列中,直到待排序列中没有元素,用这种方式,我们可以从一个极点出发利用极边的性质找下一个极点,找下一个极点这很容易只需要看是否符合其他点都在这个点和上一个极点的连线的同一侧即可。关键在于如何找起始的极点
在这里插入图片描述
观察这个图,我们可以发现最下面的点(y值最大)一定是极点,然而还可能有y值相同的多个点的情况,这时我们取x值最小的点即可,这个点一定是极点(上图的点O)为起始点。我们称此为Lowest-Then-Leftmost,这个起始点简称为LTL。

(2)具体代码:

#include <graphics.h>
#include <iostream>
#include <time.h>
#include <conio.h>
using namespace std;
struct Point{
	int x,y;
	bool isExtremePoint;
	int next;//当前极点的下一个极点
};
int Area2(Point a,Point b,Point c);
bool ToLeft(Point a,Point b,Point c);
int LTL(Point S[],int n);
void Jarvis(Point S[],int n);
int main()
{
	initgraph(600,600);//创建一个600*600的窗口
	srand((unsigned)time(NULL));//产生随机种子
	Point points[10];
	for(int i=0;i<10;i++){
		points[i].x=rand()%600;
		points[i].y=rand()%600;
		points[i].next=-1;
		setlinecolor(RGB(255,0,0));
		setfillcolor(RGB(255,0,0));
		fillcircle(points[i].x,points[i].y,3);
	}
	Jarvis(points,10);
	//在Jarvis的循环中直接画了,还能体现出next对于顺序的好处
	/*int ltl=LTL(points,10),k=ltl,s=-1;
	do{
		int x1=points[k].x,y1=points[k].y;
		s=points[k].next;
		int x2=points[s].x,y2=points[s].y;
		setlinecolor(RGB(0,0,255));
		line(x1,y1,x2,y2);
		k=s;
	}while(k!=ltl);*/
	getch();
	closegraph();
	return 0;
}
//利用行列式的几何意义,通过判断正负来判断点在直线向量的左侧还是右侧
int Area2(Point a,Point b,Point c){
	return a.x*b.y-a.y*b.x+b.x*c.y-b.y*c.x+c.x*a.y-c.y*a.x;
}
bool ToLeft(Point a,Point b,Point c){
	return Area2(a,b,c)>0;
}
//找起始点
int LTL(Point S[],int n){
	int ltl=0;
	for(int i=0;i<n;i++){
		if(S[i].y>S[ltl].y||(S[i].y==S[ltl].y&&S[i].x<S[ltl].x)){
			ltl=i;
		}
	}
	return ltl;
}
//找下一个极点
void Jarvis(Point S[],int n){
	for(int i=0;i<n;i++)
		S[i].isExtremePoint=false;
	int ltl=LTL(S,n);
	int k=ltl,s=-1;
	do{
		S[k].isExtremePoint=true;
		for(int i=0;i<n;i++){
			if(i!=s&&i!=k&&(s==-1||!ToLeft(S[k],S[s],S[i])))
				s=i;
		}
		setlinecolor(RGB(0,0,255));
		line(S[k].x,S[k].y,S[s].x,S[s].y);
		Sleep(100);
		S[k].next=s;
		k=s;
	}while(k!=ltl);
}

(3)算法分析与评价:

观察Jarvis函数可以看到此时算法会根据点集中极点个数的不同其时间复杂度也会不同,如果所有的点都是极点,那么就相当于遍历所有的极边和极点(最坏的情况),时间复杂度是O(n2),而如果极点很少,我们可以趋近线性的找到所有极点,接近于O(n),我们把这种问题的难度取决于问题本身性质的算法的性质称为输出敏感性

4.Graham Scan算法:

(1)算法说明:

没有最好的算法,只有合适的算法,就像上一种方法,不同的情况有不同的效率,但是在排序算法中我们公认的最好的时间复杂度为O(nlogn),也就是最坏情况下也不超过nlogn,所以还有改进的空间。我们再来观察下图:
在这里插入图片描述
我们首先通过LTL函数找到起始点,我们可以发现所有点的序号恰好是按照他与起始点间极角的大小排序一致,这里我们是以点2为基准的,实际问题中我们可能并不会有这么一个和起始点平行的点,所以我们可以自己取基准。这样给点集排序的目的就是为了控制Jarvis March的不稳定性。

注意这里利用极角排序用向量点积很方便,因为12与所有向量1i的夹角都小于180度,cos在0到π又是递减的,所以这里直接用cos(极角)代替极角即可,而cos(极角)可以很方便的用向量点积得到:

向量a=(x1,y1),向量b=(x2,y2) a·b=x1x2+y1y2=|a||b|cosθ(θ是a,b夹角)

	P[ltl].cosa=1;
	Point standard_vector={1,0};//排序的基准
	for(int i=0;i<n;i++){//计算每个向量与基准夹角余弦值
		if(i!=ltl){
			Point vector={P[i].x-P[ltl].x,P[i].y-P[ltl].y};//每个向量
			double fenzi=vector.x*standard_vector.x+vector.y*standard_vector.y;
			double fenmu=sqrt(pow(vector.x,2.0)+pow(vector.y,2.0))*sqrt(pow(standard_vector.x,2.0)+pow(standard_vector.y,2.0));
			P[i].cosa=fenzi/fenmu;
		}
	}

然后我们利用两个堆栈来对点集中的点进行操作,先是初始化:
在这里插入图片描述
之后我们会对T逐个元素的扫描,每次扫描后都会让问题的规模线性减少,也就是必然有一个堆栈中的一个元素被剔除,最终栈S中剩下的元素就是逆时针排放好的极点集合。扫描过程:

while(!T.empty()){//注意栈的第一个元素是栈顶元素,而不是第一个插进去的元素
	ToLeft( S[1] , S[0] , T[0]) ? S.push(T.pop()) : S.pop() ;
}

(2)具体代码:

#include <iostream>
#include <vector>
#include <map>
#include <stack>
#include <algorithm>
#include <graphics.h>
#include <time.h>
#include <conio.h>
using namespace std;
#define POINTNUM 10
struct Point
{
    double x;    // x坐标
    double y;    // y坐标
    double z;    // z坐标(默认为0,如果需要三维点则给z赋值)

    Point(double a = 0, double b = 0, double c = 0) { x = a; y = b; z = c; } // 构造函数
};
Point sub(const Point& lhs, const Point& rhs)//向量减法
{
    Point res;

    res.x = lhs.x - rhs.x;
    res.y = lhs.y - rhs.y;
    res.z = lhs.z - rhs.z;

    return res;
}
Point div(const Point& p, double ratio)//向量除法
{
    Point res;
    
    res.x = p.x / ratio;
    res.y = p.y / ratio;
    res.z = p.z / ratio;

    return res;
}
double length(const Point& vec)//向量长度
{
    return (sqrt(pow(vec.x, 2) + pow(vec.y, 2) + pow(vec.z, 2)));
}
Point normalize(const Point& vec)//向量标准化
{
    Point res;
    res = div(vec, length(vec));//向量除以向量长度
    return res;
}
double dotMultiply(const Point& vec1, const Point& vec2)//向量点乘
{
    return(vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z);
}
Point multiply(const Point& vec1, const Point& vec2)//向量叉乘
{
    Point result;

    result.x = vec1.y * vec2.z - vec2.y * vec1.z;
    result.y = vec1.z * vec2.x - vec2.z * vec1.x;
    result.z = vec1.x * vec2.y - vec2.x * vec1.y;

    return result;
}
// 参数: vec1 矢量1  vec2 矢量2
double Cos(const Point& vec1, const Point& vec2)//向量余弦
{
    Point unit_vec1 = normalize(vec1);
    Point unit_vec2 = normalize(vec2);

    return dotMultiply(unit_vec1, unit_vec2);
}
//利用行列式的几何意义,通过判断正负来判断点在直线向量的左侧还是右侧
int Area2(Point a,Point b,Point c){
	return a.x*b.y-a.y*b.x+b.x*c.y-b.y*c.x+c.x*a.y-c.y*a.x;
}
bool ToLeft(Point a,Point b,Point c){
	return Area2(a,b,c)>0;
}
// 参数: points : 平面点集
//
vector<Point> findConvexGraham(const vector<Point>& points)
{
    vector<Point> result;

    // 点的数量必须大于三个
    if (points.size() < 3)
        return result;

    // 寻找最底部的点(LTL)
    int index = 0;
    for (int i = 0; i < points.size(); ++i)
    {
        if (points[i].y < points[index].y ||(points[i].y == points[index].y && points[i].x < points[index].x))
        {
            index = i;
        }
    }
    Point convex_p = points[index];

    // 计算每个点的极角
    map<double, int> cos_map;
    Point x_vec(1.0, 0.0);
    for (int i = 0; i < points.size(); ++i)
    {
        if (i != index)
        {
            double cos_value = Cos(sub(points[i], convex_p), x_vec);
            // 如果有多个点有相同的极角,则取最远的点
            if (cos_map.count(-cos_value) != 0)
            {
                if (length(points[i]) > length(points[cos_map[-cos_value]]))
                    cos_map[-cos_value] = i;
            }
            else
                cos_map[-cos_value] = i;
        }
    }
    
    // 保存结果的栈
    stack<int> result_stack;
    // 存入开始的两个点
    result_stack.push(index);
    result_stack.push(cos_map.begin()->second);

    for (auto iter = (++cos_map.begin()); iter != cos_map.end(); ++iter)
    {
        int first = result_stack.top();
        result_stack.pop();
        int second = result_stack.top();

        Point vec1 = sub(points[first], points[second]);
        Point vec2 = sub(points[iter->second], points[first]);
		//判断当前点是否在上一条极边的左侧还是右侧
        //if (ToLeft(points[second],points[first],points[iter->second]))
		if (multiply(vec1, vec2).z>0)//在左侧则保留上一极点
            result_stack.push(first);
        result_stack.push(iter->second);
    }

    // 将数据从栈中读取
    while (!result_stack.empty())
    {
        result.push_back(points[result_stack.top()]);
        result_stack.pop();
    }

    std::reverse(result.begin(), result.end());

    return result;
}
int main()
{
	vector<Point> points;
	initgraph(600,600);//创建一个600*600的窗口
	srand((unsigned)time(NULL));//产生随机种子
	for(int i=0;i<POINTNUM;i++){//随机生成10个点
		Point point;
		point.x=rand()%600;
		point.y=rand()%600;
		points.push_back(point);
		setlinecolor(RGB(255,0,0));
		setfillcolor(RGB(255,0,0));
		fillcircle(point.x,point.y,3);
	}
	vector<Point> result=findConvexGraham(points);
	//画出结果
	int len=result.size();
	for(int i=0;i<len-1;i++){
		setlinecolor(RGB(0,0,255));
		line(result[i].x,result[i].y,result[i+1].x,result[i+1].y);
	}
	setlinecolor(RGB(0,0,255));
	line(result[len-1].x,result[len-1].y,result[0].x,result[0].y);
	getch();
	closegraph();
	return 0;
}

然而这个实现还是有问题的。。。。:
在这里插入图片描述
不知道为什么,总是有一个点会莫名其妙被算入极点。

(3)算法分析与评价:

while(!T.empty())为O(n),然而时间复杂度应该为O(nlogn),这也侧面说明了预处理(前面对所有点的排序)是一定要做的,这就很容易了,毕竟排序的公认最好算法也是O(nlogn),用哪种排序算法都可以,所以最终的时间复杂度是O(nlogn)。

5.利用归并排序实现凸包算法:

(1)算法说明:

复杂度为O(nlogn)的排序算法并不只有上一种利用额外空间进行排序的算法,所以还可以通过别的复杂度为O(nlogn)的排序算法来得到一些启发。
比如merge sort:
在这里插入图片描述
看了这个图一定能回忆起归并排序分而治之的思想,我们为什么不把点集也进行均匀划分去求每一个小的凸包,进而去逐步合成大的凸包呢?说起来容易做起来不要忽略难点和细节:如何由两个子凸包合成一个凸包呢?
我们可以从Graham Scan中对点集的排序中得到启发,所有的极点一定是关于一个点的极角是有序排列的。这个点应当位于凸包内部,所以会有三种情况:
在这里插入图片描述
怎么找内部的点呢?
这很容易,随便找三个点构成三角形的重心一定在内部,之后去做判断:这个点是否在另一个凸包内部,这又引发了另一个问题:
如何判断一个点在凸包内部还是外部?
我们效仿第一种算法中判断点是否在三角形中的方法做n次ToLeft即可,这n次每次都是嵌入在merge中并不会影响效率。
不同情况如何处理?
(1)如果在另一个凸包内部,那很容易,对两个凸包所有极点与选的那个内部的点的极角(自己选一个基准即可)用二路归并即可,只不过现在的顺序是环形,排完了就会得到极角顺序排列的集合。接下来只需要进行Graham Scan即可。
(2)而对于第二种情况我们必须要用支撑线的原理:
在这里插入图片描述
如果我们在P2中插入一个点X,那么显然ts段会被丢弃,st段会被保留,我们仔细观察可以发现,ts上的所有极边(逆时针),x在它们的右侧,在st上的所有极边的左侧,而s、t所在的极边恰好x在它们的不同侧,s、t称为切点,st则称为支撑线,我们恰好可以通过这个原理淘汰掉ts上的点,这样又得到了极角顺序排列的集合。接下来只需要进行Graham Scan即可。
(3)看第六种算法。

(2)具体代码:

//计算极角和扫描在Graham Scan中已实现,这里用二路归并把所有点集分开再排序即可
#include <iostream>
#include <vector>
#include <map>
#include <stack>
#include <algorithm>
#include <graphics.h>
#include <time.h>
#include <conio.h>
using namespace std;
#define POINTNUM 10
struct Point
{
    double x;    // x坐标
    double y;    // y坐标
    double z;    // z坐标(默认为0,如果需要三维点则给z赋值)

    Point(double a = 0, double b = 0, double c = 0) { x = a; y = b; z = c; } // 构造函数
};
Point sub(const Point& lhs, const Point& rhs)//向量减法
{
    Point res;

    res.x = lhs.x - rhs.x;
    res.y = lhs.y - rhs.y;
    res.z = lhs.z - rhs.z;

    return res;
}
Point div(const Point& p, double ratio)//向量除法
{
    Point res;
    
    res.x = p.x / ratio;
    res.y = p.y / ratio;
    res.z = p.z / ratio;

    return res;
}
double length(const Point& vec)//向量长度
{
    return (sqrt(pow(vec.x, 2) + pow(vec.y, 2) + pow(vec.z, 2)));
}
Point normalize(const Point& vec)//向量标准化
{
    Point res;
    res = div(vec, length(vec));//向量除以向量长度
    return res;
}
double dotMultiply(const Point& vec1, const Point& vec2)//向量点乘
{
    return(vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z);
}
Point multiply(const Point& vec1, const Point& vec2)//向量叉乘
{
    Point result;

    result.x = vec1.y * vec2.z - vec2.y * vec1.z;
    result.y = vec1.z * vec2.x - vec2.z * vec1.x;
    result.z = vec1.x * vec2.y - vec2.x * vec1.y;

    return result;
}
// 参数: vec1 矢量1  vec2 矢量2
double Cos(const Point& vec1, const Point& vec2)//向量余弦
{
    Point unit_vec1 = normalize(vec1);
    Point unit_vec2 = normalize(vec2);

    return dotMultiply(unit_vec1, unit_vec2);
}
//利用行列式的几何意义,通过判断正负来判断点在直线向量的左侧还是右侧
int Area2(Point a,Point b,Point c){
	return a.x*b.y-a.y*b.x+b.x*c.y-b.y*c.x+c.x*a.y-c.y*a.x;
}
bool ToLeft(Point a,Point b,Point c){
	return Area2(a,b,c)>0;
}
bool cmp(Point a, Point b)
{
    if (a.x == b.x)
        return a.y < b.y;
    return a.x < b.x;
}
vector<Point> findConvex(const vector<Point>& points)
{
    vector<Point> result;
    if (points.size() < 3)
        return result;

    vector<Point> tmp_points = points;
    // 首先将所有点按照字典序排序
    sort(tmp_points.begin(), tmp_points.end(), cmp);

    // 上凸包
    vector<Point> upper_hull;
    // 存入第一个和第二个点
    upper_hull.push_back(tmp_points[0]);
    upper_hull.push_back(tmp_points[1]);
    for (int i = 2; i < tmp_points.size(); ++i)
    {
        upper_hull.push_back(tmp_points[i]);
        while (upper_hull.size() > 2 && multiply(sub(upper_hull[upper_hull.size() - 2], upper_hull[upper_hull.size() - 3]), sub(upper_hull[upper_hull.size() - 1], upper_hull[upper_hull.size() - 3])).z >= 0)
        {
            upper_hull.erase(upper_hull.end() - 2);
        }
    }
    // 下凸包
    vector<Point> lower_hull;
    // 存入倒数第一第二个点
    lower_hull.push_back(tmp_points[tmp_points.size() - 1]);
    lower_hull.push_back(tmp_points[tmp_points.size() - 2]);
    for (int i = tmp_points.size() - 3; i >= 0; --i)
    {
        lower_hull.push_back(tmp_points[i]);
        while (lower_hull.size() > 2 && multiply(sub(lower_hull[lower_hull.size() - 2], lower_hull[lower_hull.size() - 3]), sub(lower_hull[lower_hull.size() - 1], lower_hull[lower_hull.size() - 3])).z >= 0)
        {
            lower_hull.erase(lower_hull.end() - 2);
        }
    }
    // 删除重复点
    lower_hull.erase(lower_hull.begin());
    lower_hull.erase(lower_hull.end() - 1);

    // 合并上下凸包
    upper_hull.insert(upper_hull.end(), lower_hull.begin(), lower_hull.end());

    result = upper_hull;

    return result;
}
int main()
{
	vector<Point> points;
	initgraph(600,600);//创建一个600*600的窗口
	srand((unsigned)time(NULL));//产生随机种子
	for(int i=0;i<POINTNUM;i++){//随机生成10个点
		Point point;
		point.x=rand()%600;
		point.y=rand()%600;
		points.push_back(point);
		setlinecolor(RGB(255,0,0));
		setfillcolor(RGB(255,0,0));
		fillcircle(point.x,point.y,3);
	}
	vector<Point> result=findConvex(points);
	//画出结果
	int len=result.size();
	for(int i=0;i<len-1;i++){
		setlinecolor(RGB(0,0,255));
		line(result[i].x,result[i].y,result[i+1].x,result[i+1].y);
	}
	setlinecolor(RGB(0,0,255));
	line(result[len-1].x,result[len-1].y,result[0].x,result[0].y);
	getch();
	closegraph();
	return 0;
}

(3)算法分析与评价:

归并排序的时间复杂度是O(nlogn),后面用的是Graham Scan是O(n),所以最终的时间复杂度是O(nlogn)。

6.分明的分而治之:

(1)算法说明:

其实5中介绍的根据merge sort得到的也是分而治之的思想,只不过你会发现5太绕,我们可能要把两个犬牙交错的子凸包硬生生的合成为一个子凸包,并不形象,这里我们不妨就假设所有的子凸包都是5中的第三种情况,可以用一个明确的分界线把两个子凸包分开:
在这里插入图片描述
一些必要的预处理:
通过对x坐标排序得到两个子凸包的左右x最值的极点。我们不难想象到第一个子凸包左侧的l合并后仍然是最左侧的点,第二个子凸包右侧的r一定也是合并后最右侧的点。
在这里插入图片描述
找到这些点有什么用呢?
我们可以通过内侧的r和l进行对极点的排查,连接内侧的r、l:
在这里插入图片描述
利用支撑线的原理我们取移动l,直到发现到了某一个极点,符合切点的定义(这里的l就是之前的支撑线原理说明中的x,p1就是被插入的凸包),然后再从这个切点不动了,开始移动r(现在x是r了,p2是被插入的凸包),直到找到一个切点,依次类推直到完全闭合:
在这里插入图片描述

(2)具体代码:

//在写

(3)算法分析与评价:

  • 6
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要使用C语言解决凸包问题并实现可视化动态效果,可以使用以下步骤: 1. 首先,需要使用凸包算法来计算给定点集的凸包。常见的凸包算法有Graham扫描算法和Jarvis步进算法。 2. 在计算凸包时,需要使用一个数据结构来存储点的坐标信息。可以使用C语言中的结构体来表示点,如下所示: ``` typedef struct { double x; double y; } Point; ``` 3. 在计算凸包时,需要将点按照一定的顺序进行排序。可以使用C语言中的qsort函数来实现,如下所示: ``` int cmp(const void* a, const void* b) { Point* p1 = (Point*)a; Point* p2 = (Point*)b; if (p1->x < p2->x) { return -1; } else if (p1->x > p2->x) { return 1; } else { return 0; } } qsort(points, n, sizeof(Point), cmp); ``` 4. 在计算凸包时,需要将点按照一定的顺序进行连接。可以使用C语言中的图形库来实现可视化效果,如下所示: ``` void drawLine(Point p1, Point p2) { line(p1.x, p1.y, p2.x, p2.y); delay(100); } for (int i = 0; i < hullSize - 1; i++) { drawLine(hull[i], hull[i+1]); } drawLine(hull[hullSize-1], hull[0]); ``` 这里使用了一个自定义的drawLine函数来绘制线段,并使用了delay函数来实现动态效果。 完整的C语言实现代码如下所示: ``` #include <stdio.h> #include <stdlib.h> #include <graphics.h> #include <math.h> #define MAX_POINTS 1000 typedef struct { double x; double y; } Point; int cmp(const void* a, const void* b) { Point* p1 = (Point*)a; Point* p2 = (Point*)b; if (p1->x < p2->x) { return -1; } else if (p1->x > p2->x) { return 1; } else { return 0; } } double crossProduct(Point p1, Point p2, Point p3) { double x1 = p2.x - p1.x; double y1 = p2.y - p1.y; double x2 = p3.x - p2.x; double y2 = p3.y - p2.y; return x1 * y2 - x2 * y1; } void drawLine(Point p1, Point p2) { line(p1.x, p1.y, p2.x, p2.y); delay(100); } int main() { int gd = DETECT, gm; initgraph(&gd, &gm, ""); Point points[MAX_POINTS]; int n = 0; while (!kbhit() && n < MAX_POINTS) { if (ismouseclick(WM_LBUTTONDOWN)) { clearmouseclick(WM_LBUTTONDOWN); int x = mousex(); int y = mousey(); points[n].x = x; points[n].y = y; n++; putpixel(x, y, WHITE); } } qsort(points, n, sizeof(Point), cmp); Point hull[MAX_POINTS]; int hullSize = 0; for (int i = 0; i < n; i++) { while (hullSize >= 2 && crossProduct(hull[hullSize-2], hull[hullSize-1], points[i]) <= 0) { hullSize--; } hull[hullSize] = points[i]; hullSize++; } int hullStart = 0; for (int i = n - 2; i >= 0; i--) { while (hullSize >= hullStart + 2 && crossProduct(hull[hullSize-2], hull[hullSize-1], points[i]) <= 0) { hullSize--; } hull[hullSize] = points[i]; hullSize++; } for (int i = 0; i < hullSize - 1; i++) { drawLine(hull[i], hull[i+1]); } drawLine(hull[hullSize-1], hull[0]); getch(); closegraph(); return 0; } ``` 在运行程序时,可以使用鼠标点击来添加点,程序会自动计算凸包并进行可视化

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值