Graham算法解决凸包问题
模板题,题目来自洛谷如下。
题目简要描述
给定一些点,求凸包的周长。
输入格式
输入数据的第一行是一个整数。表示农夫约翰想要围住的放牧点的数目n。
第 2 2 2 到第 ( n + 1 ) (n + 1) (n+1) 行,每行两个实数,第 ( i + 1 ) (i + 1) (i+1) 行的实数 xi, yi 分别代表第 i i i 个放牧点的横纵坐标
输出格式
输出输出一行一个四舍五入保留两位小数的实数,代表围栏的长度。
输入输出样例
//输入
4
4 8
4 12
5 9.3
7 8
//输出
12.00
Graham算法
该算法的大致思路为:
通过维持一个关于候选点的栈S来解决凸包问题。输入集合Q中的每个点都被压入栈一次,非凸包顶点的点最终被弹出栈。当算法终止时,栈中仅包含凸包顶点,且以逆时针的顺序出现在边界上。而该算法主要由排序和扫描组成。(摘自《算法导论》)
主要过程:
首先是排序过程,易得纵坐标最小的值必定是凸包中的顶点,因此选择一个y值最小(如有相同选x最小)的点,记作P0。接着其余的点按极角的大小逆时针排序,编号P1至Pm。
接着是扫描过程,按上述排序过程遍历每个点,进行判断,如若该点更合适,则上一个点出栈,该点入栈,否则直接入栈。
图片说明:
文字描述比较绕,下面给出图片描述。(图源来自《算法导论》)
代码实现:
#include<bits/stdc++.h>
#define maxn 8000005
using namespace std;
int n;
struct dot{
double x;
double y;
}p[maxn], s[maxn]; // p用以存所有点,s用以存凸包顶点
double cp(dot a1, dot a2, dot b1, dot b2){ // 叉乘,a1,a2,b1,b2为四个点
return (a2.x - a1.x) * (b2.y - b1.y) - (b2.x - b1.x) * (a2.y - a1.y);
}
double dis(dot p1, dot p2){ //求两点间距
return sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}
bool cmp(dot p1, dot p2){ // 排序函数,按极角大小排序
double tmp = cp(p[0], p1, p[0], p2);
if(tmp > 0)
return 1;
if(tmp == 0 && dis(p[0], p1) < dis(p[0], p2))
return 1;
return 0;
}
int main(){
scanf("%d", &n);
double tmp;
for(int i = 0; i < n; i++){
scanf("%lf%lf", &p[i].x, &p[i].y);
if(i > 0 && (p[i].y < p[0].y || (p[i].y == p[0].y && p[i].x < p[0].x))){ // p[0].y保证最小
tmp = p[i].x, p[i].x = p[0].x, p[0].x = tmp;
tmp = p[i].y, p[i].y = p[0].y, p[0].y = tmp;
}
}
sort(p + 1, p + n, cmp); //从第二项开始排
s[0] = p[0]; //p[0]一定在凸包中
int cnt = 0;
for(int i = 1; i < n; i++){
// 判断此时栈顶的点是否需要剔除,这步结合上图可理解的更透彻
while(cnt > 0 && cp(s[cnt - 1], s[cnt], s[cnt], p[i]) <= 0)
cnt -= 1;
cnt += 1;
s[cnt] = p[i]; //p[i]入栈
}
s[cnt + 1] = p[0]; //需要求周长, 第一个点再入栈
double C = 0;
for(int i = 0; i <= cnt; i++)
C += dis(s[i], s[i + 1]);
printf("%.2lf", C);
return 0;
}