算法——蛮力法之最近对问题和凸包问题
上次的博客写到一半宿舍停电了。。。。然而今天想起来补充完的时候发现博客园并没有自动保存哦,微笑。
最近对问题
首先来看最近对问题,最近对问题描述的就是在包含n个端的集合中找到距离最近的两个点,当然问题也可以定义在多维空间中,但是这里只是跟随书上的思路实现了二维情况下的最近对问题。假设所有讨论的点是以标准的笛卡尔坐标形式(x,y)给出的,那么在两个点Pi=(Xi,Yi)和Pj=(Xj,Yj)之间的距离是标准的欧几里得距离:
d(Pi,Pj)=sqrt( (X1-X2)2+(Y1-Y2)2 )
蛮力法的思路就是计算出所有的点之间的距离,然后找出距离最小的那一对,在这里增加效率的一种方式是只计算点下标 i<j 的那些对点之间的距离,这样就避免了重复计算同一对点间距离。下面是蛮力法解决最近对问题的算法:
使用蛮力法求平面中距离最近的两点 BruteForceClosetPoints(P) //输入:一个n(n≥2)的点的列表P,Pi=(Xi,Yi) //输出:距离最近的两个点的下标index1和index2 dmin <— ∞ for i <— 1 to n-1 do for j <— i+1 to n do d <— sqrt( (Xi-Xi)2+(Yj-Yj)2 ) if d<dmin dmin=d; index1=i; index2=j; return index1,index2 |
该算法的关键步骤是基本操作虽然是计算两个点之间的欧几里得距离,但是求平方根并不是像加法乘法那么简单。上面算法中,开平方函数导数恒大于0,它是严格递增的,因此我们可以直接只计算(Xi-Xi)2+(Yj-Yj)2,比较d2的大小关系,这样基本操作就变成了求平方。平方操作的执行次数是:
n(n-1) ∈ Θ(n2)
因此,蛮力法解决最近对问题的平均时间复杂度是Θ(n2)
下面是该算法的c++代码实现部分,在实现这个算法时,我碰到了三个问题:
一是:怎么表示一个点集,因为最终返回的下标是集合中点的下标,要用的数据结构就是一维数组,但是点的xy坐标又要怎么表示呢,这里我在头文件中创建了struct类型的点结构,该结构拥有的成员变量就是x代表的横坐标和y代表的纵坐标,这样就可以直接创建该结构的一位数组进行计算了。
二是:BruteForceClosetPoints(P)函数返回的是两个最近对下标的值,但是用return只能返回一个值,因此这里在BruteForceClosetPoints函数的参数表中增加了引用类型的变量 &index1,&index2。
三是:在计算点间距离前,需要将最大值付给存储当前最小距离的变量dmin,但是我不记得怎么表示float类型的最大值,搜索之后发现c++的<float.h>头文件中定义了FLT_MAX这个常量表示float类型的最大值(其中也记录了double类型的最值,而int类型是记录在<limits.h>头文件中)。
另外在实现时为更符合思路,我没有使用数组的0号空间,而是直接从1号空间开始使用。
下面是源代码部分:
//头文件部分 #include<float.h>//该头文件中存储了float类型和double类型的最值 typedef struct Point{ float x; float y; }; void BruteForceClosetPoints(Point P[], int n, int &index1, int &index2); //源文件部分 #include <iostream> #include "最近对问题头文件.h" using namespace std; int main(){ Point P[4]; for (int i = 1; i < 4; i++){ cin >> P[i].x >> P[i].y; } getchar(); /* cout << "您输入的点的坐标为:" << endl; for (int i =1; i <4; i++){ cout<< "("<<P[i].x <<","<< P[i].y<<") "; } */ int index1, index2; BruteForceClosetPoints(P, 4, index1, index2); cout << "最近对的两个点的下标为:" << index1 << " " << index2<< endl; getchar(); return 1; } void BruteForceClosetPoints(Point P[], int n, int &index1, int &index2){//由于要同时返回两个点的下标,因此这里将形参设置为引用变量 float dmin = FLT_MAX;//常量FLT_MAX 来源于float.h头文件 int i = 1,j=0; float d; for (; i < n-1; i++){ for (j = i + 1; j < n; j++){ d = (P[i].x - P[j].x)*(P[i].x - P[j].x) + (P[i].y - P[j].y)*(P[i].y - P[j].y); if (d < dmin){ dmin = d; index1 = i; index2 = j; } } } }
凸包问题
凸包问题是为一个有n个点的集合构造凸包的问题。凸包的定义是任意包含n>2个点(不共线的点)的集合s的凸包是以s中的某些点为顶点的凸多边形(如果所有的点都位于一条直线上,多边形退化为一条线段,但他的两个端点仍然包含在s中)。对于凸多边形,举例子来说,三角形,长方形,直线都是,而五角星的形状就是典型的非凸多边形了。
用蛮力法解决凸包问题的思路之一是通过找到凸包的边界(两个极点组成的线段)来确定构成凸包的极点。
找到凸包边界的算法是:对于一个n个点集合中的两个点Pi和Pj,当且仅当该集合中的其他点都位于穿过这两点的直线的同一边是,他们的连线是该集合凸包边界的一部分。
在实现上述算法的时候用到的解析几何的基本知识是,当坐标平面上的两个点(X1,Y1)、(X2,Y2)组成的直线方程是:ax+by=c(其中a=Y2-Y1,b=X1-X2,c=X1Y2-X2Y1)
这样,这条直线将坐标平面分成分成两个半平面,其中一个平面的点都满足ax+by>c,另一个半平面的点都满足ax+by<c,在直线上的点都满足ax+by=c。因此,可以通过将点带入ax+by-c这个解析式中判断解析式值正负的方法来判断某些点是否位于这条直线的同一边。
蛮力法解决凸包问题: BruteForceConvexHull(P) //输入:一个n个(n≥2)的点的列表P,Pi=(Xi,Yi) //输出:能够组成凸包的点的列表Qi=(Xi,Yi) for i <— 1 to n-1 do for j <— i+1 to n do sign1 <— 0;sign2 <— 0; a = yj - yi;b = xi - xj;c = xi*yj - yi*xj; for k <— 1 to n do if k=i||k=j continue if axk+byk-c≥0 sign1++; if axk+byk-c≤0 sign2--; if sign2=2-n || sign1=n-2 record the pole return OK |
上面的算法是我根据书上的描述自己写的,在实现该算法的时候,遇到的问题就是不知道该怎么判断除组成直线的两个点外其余n-2个点都在该直线的同一侧,后来网上搜索后发现有人设置了旗帜变量,于是我在他的基础上做了改进,设置两个旗帜变量,这样可以处理当n-2个点中有点存在于该直线上这种情况。
该算法的基本操作就是最内层循环的if条件判断,该语句执行的次数是:n(n-1)+n(n-2)+.....+n=n3/2 ∈ Θ(n3)
因此,概算法的平均时间复杂度是:n3
下面是该算法的c++实现代码,在实现的过程中,依旧用struct结构来定义了点的数据结构,但是当判断出一个点是极点后要将其坐标保存到新的数组中操作有点麻烦,于是我在struct结构中新增了singal属性,用来存储该点是否能够构成凸包边界的标志,这样,在整个判断结束后,只要重新遍历一次原始列表P,将该列表中signal属性被重新赋值的坐标输出就是构成凸包的极点了。
//头文件内容 #define NUM 7 //P列表点的个数 typedef struct Point{ float x; float y; int signal=0; }Point; int BruteForceConvexHull(Point P[], int n); //源文件内容 #include <iostream> #include "最近对问题头文件.h" using namespace std; int main(){ Point P[NUM]; for (int i = 1; i < NUM; i++){ cin >> P[i].x >> P[i].y; } getchar(); BruteForceConvexHull(P, NUM-1); cout << "属于凸包集合的点坐标为:" << endl; for (int i = 1; i <NUM; i++){ if (P[i].signal == 1){ cout << " (" << P[i].x << "," << P[i].y << ") "<<endl; } } getchar(); return 1; } int BruteForceConvexHull(Point P[], int n){ int i = 1, j = 1, k = 1; float a, b, c; int sign1 = 0, sign2 = 0; for (; i <= n - 1; i++){ for (j = i + 1; j <= n; j++){ a = P[j].y - P[i].y; b = P[i].x - P[j].x; c = P[i].x*P[j].y - P[i].y*P[j].x; sign1 = 0; sign2 = 0; for (k = 1; k <= n; k++){ if (k == i || k == j){ continue; } if ((a*P[k].x + b*P[k].y - c) <= 0){//此处必须是两个if语句,避免当判断的点在线上时只判断了其中一个并且最好用两个旗帜变量来记录 sign1--; } if ((a*P[k].x + b*P[k].y - c) >= 0) { sign2++; } } if ((sign1 == 2 - n) || (sign2 == n - 2)){ //cout << sign1 << " " << sign2 << " 点 " << i << " " << j << " 是极点" << endl; P[i].signal = 1; P[j].signal = 1; } } } return 1; }