计算几何(2)
2022年4月18日21:31:04
求凸包(围住所有点的周长最小的,面积不一定最小的多边形)的算法。
Graham算法
Andrew算法 O ( n l o g n ) O(nlogn) O(nlogn)
- 将点排序,x 为第一关键字,y 为第二关键字。瓶颈
- 从左到右维护凸多边形的上半部分,再从右至左维护下半部分。(使用栈维护)
- 对于一个顺时针方向的凸多边形,先将最开始的两个点加入栈中,从第三个点开始判断,判断当前点和栈中点组成的直线的延长线的关系,如果待加入点在直线的延长线的左边(逆时针),将栈顶出栈,再进行判断,直到待加入点在直线延长线的右边,然后将点加入栈中,最后再用起点更新栈。
尝试自己手撸一个。
2022年4月19日14:28:13
手撸失败,学习y总代码。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#include<cstdlib>
#include<stack>
#include<map>
#include<queue>
#include<cmath>
#include<set>
#include<cassert>
#define fo(i,a,n) for(int i=(a);i<=(n);i++)
#define fo_(i,a,n) for(int i=(a);i<(n);i++)
#define debug(x) cout<<#x<<":"<<x<<endl
#define all(x) (x).begin(),(x).end()
#define pb push_back
#define endl '\n'
using namespace std;
const int N = 1e5+10;
const int M = 510*510;
const double inf = 1e8;
typedef long long ll;
#define xx first
#define yy second
typedef pair<double,double> PDD;
typedef pair<double,double> Vector;
PDD a[N];
int n,stk[N],top;
bool used[N];
double cross(double x1,double y1,double x2,double y2){//第一个点叉积第二个点
return x1*y2-x2*y1;
}
double dis(Vector A,Vector B){
return sqrt((A.xx-B.xx)*(A.xx-B.xx)+(A.yy-B.yy)*(A.yy-B.yy));
}
int main(){
cin>>n;
fo(i,1,n){
cin>>a[i].xx>>a[i].yy;
}
sort(a+1,a+n+1,[&](PDD a,PDD b){
if(a.xx==b.xx)return a.yy>b.yy;
return a.xx<b.xx;
});
stk[++top]={1};
stk[++top]={2};
used[1]=used[2]=1;
fo(i,3,n){
Vector C = a[i];
while(top>1){// 凸包集合中至少要有一个点。
Vector B = a[stk[top]];
Vector A = a[stk[top-1]];
if(cross(C.xx-A.xx,C.yy-A.yy,B.xx-A.xx,B.yy-A.yy) < 0){
used[top]=false;
top--;
}
}
stk[++top]=i;
used[i]=true;
break;
}
for(int i=n;i>=2;i--){
if(used[i])continue;
Vector C = a[i];
while(top>1){// 凸包集合中至少要有一个点。
Vector B = a[stk[top]];
Vector A = a[stk[top-1]];
if(cross(C.xx-A.xx,C.yy-A.yy,B.xx-A.xx,B.yy-A.yy) < 0){
used[top]=false;
top--;
}
}
stk[++top]=i;
used[i]=true;
break;
}
// 特判第一个点
Vector C = a[1];
while(1){
if(top>1){// 凸包集合中至少要有一个点。
Vector B = a[stk[top]];
Vector A = a[stk[top-1]];
if(cross(C.xx-A.xx,C.yy-A.yy,B.xx-A.xx,B.yy-A.yy) < 0){
used[top]=false;
top--;
}
}
stk[++top]=1;
used[1]=true;
break;
}
// fo(i,1,n){
// if(used[i]){
// cout<<i<<" ";
// }
// }
// cout<<endl;
bool flag=0;
int last,first;
double res=0;
fo(i,1,n){
if(!used[i])continue;
if(!flag){
flag=1;
last = i;
first = i;
continue;
}
res+=dis(a[i],a[last]);
last = i;
}
res+=dis(a[last],a[first]);
printf("%.2lf",res);
return 0;
}
能学习的地方。
stk[++top]={1};
stk[++top]={2};
used[1]=used[2]=1;
fo(i,3,n){
Vector C = a[i];
while(top>1){// 凸包集合中至少要有一个点。
Vector B = a[stk[top]];
Vector A = a[stk[top-1]];
if(cross(C.xx-A.xx,C.yy-A.yy,B.xx-A.xx,B.yy-A.yy) < 0){
used[top]=false;
top--;
}
}
stk[++top]=i;
used[i]=true;
break;
}
----------------------------------------------
y总的写法:
for (int i = 0; i < n; i ++ )
{
while (top >= 2 && area(q[stk[top - 1]], q[stk[top]], q[i]) <= 0)
{
// 凸包边界上的点即使被从栈中删掉,也不能删掉used上的标记
if (area(q[stk[top - 1]], q[stk[top]], q[i]) < 0)
used[stk[top -- ]] = false;
else top -- ;
}
stk[ ++ top] = i;
used[i] = true;
}
学到的地方:当前几个可以直接加入,但是需要用while循环特判的时候,可以参考y总的,把所有代码都放到for循环里边,while再加一个判断。
used[0] = false;
for (int i = n - 1; i >= 0; i -- )
{
if (used[i]) continue;
while (top >= 2 && area(q[stk[top - 1]], q[stk[top]], q[i]) <= 0)
top -- ;
stk[ ++ top] = i;
}
需要用第1个加入的点重新更新整个凸包,非常巧的使用 used[0] = 0;
这样,stk中会出现两次 0 号点,最后stk[top] 一定等于 0 ,刚好便于计算周长。
double res = 0;
for (int i = 2; i <= top; i ++ )
res += get_dist(q[stk[i - 1]], q[stk[i]]);
return res;
计算几何模板
double get_dist(PDD a, PDD b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
double cross(PDD a, PDD b)
{
return a.x * b.y - a.y * b.x;
}
// a->b 向量和 a->c 向量求叉积。
double area(PDD a, PDD b, PDD c)
{
return cross(b - a, c - a);// 先重载两个点相减
}
参考大佬的代码
可以通过acwing和luogu(目测无bug),不需要特判共线的问题
样例:
2
0 0
0 1
输出:
2
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 10010;
int n;
PDD q[N];
int stk[N];
double get_dist(PDD a, PDD b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
PDD operator-(PDD a, PDD b)
{
return {a.x - b.x, a.y - b.y};
}
double cross(PDD a, PDD b)
{
return a.x * b.y - a.y * b.x;
}
double area(PDD a, PDD b, PDD c)
{
return cross(b - a, c - a);
}
double andrew()
{
sort(q, q + n);
int top = 0;
for (int i = 0; i < n; i ++ )
{
while (top >= 2 && area(q[stk[top - 1]], q[stk[top]], q[i]) <= 0)
top -- ;
stk[ ++ top] = i;
}
int k = top;
for (int i = n - 1; i >= 0; i -- )
{
// 不希望删去求上凸包时,最右边的那个点 , 依然可以让求下凸包的时候左边的第一个点再次加入stk中。
while (top > k && area(q[stk[top - 1]], q[stk[top]], q[i]) <= 0)
top -- ;
stk[ ++ top] = i;
}
double res = 0;
for (int i = 2; i <= top; i ++ )
res += get_dist(q[stk[i - 1]], q[stk[i]]);
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%lf%lf", &q[i].x, &q[i].y);
double res = andrew();
printf("%.2lf\n", res);
return 0;
}
信用卡凸包
旋转的bug我找了好久qwq
#include<bits/stdc++.h>
using namespace std;
typedef pair<double,double>PDD;
#define xx first
#define yy second
#define pi acos(-1)
const int N = 1e5+10;
int n,stk[N],top,idx;
double a,b,r;
PDD A[N];// 将一个矩形方块变成四个角。
double dis(PDD a,PDD b){
double dx = a.xx-b.xx;
double dy = a.yy-b.yy;
return sqrt(dx*dx+dy*dy);
}
PDD operator - (PDD a,PDD b){
return {a.xx-b.xx,a.yy-b.yy};
}
double cross(double x1,double y1,double x2,double y2){
return x1*y2-x2*y1;
}
double area(PDD a,PDD b,PDD c){
return cross(b.xx-a.xx,b.yy-a.yy,c.xx-a.xx,c.yy-a.yy);
}
double get(){
sort(A+1,A+idx+1);
double res=0;
for(int i=1;i<=idx;i++){//idx个点
while(top>=2 && area(A[stk[top-1]], A[stk[top]], A[i])<=0){
top--;
}
stk[++top]=i;
}
int k = top;
for(int i=idx;i>=1;i--){
while(top>k && area(A[stk[top-1]], A[stk[top]], A[i]) <=0){
top--;
}
stk[++top]=i;
}
for(int i=2;i<=top;i++){
res+=dis(A[stk[i]],A[stk[i-1]]);
}
return res;
}
PDD rotate(double x,double y,double theta){
double X = x*cos(theta)+y*sin(theta);
double Y = -x*sin(theta)+y*cos(theta);
return {X,Y};
}
int main(){
cin>>n>>a>>b>>r;
/*
错误的写法,但是很难看出bug
原来中点(x,y),先不旋转, 右上角是 (x+b/2-r,y+a/2-r)
所以旋转向量是(b/2-r,a/2-r) , 但是不能用这个向量替代转四次。 因为长方形对角线夹角不一定是45度。
假设旋转 theta 角,
for(int i=1;i<=n;i++){
double x,y,theta;
cin>>x>>y>>theta;
// 转角公式?
PDD t = rotate((b/2)-r,(a/2)-r,-theta);
A[++idx] = {x + t.xx, y + t.yy};
A[++idx] = {x + t.xx, y - t.yy};
A[++idx] = {x - t.xx, y + t.yy};
A[++idx] = {x - t.xx, y - t.yy};
}
*/
int dx[]={1,1,-1,-1};
int dy[]={1,-1,1,-1};
for(int i=1;i<=n;i++){
double x,y,theta;
cin>>x>>y>>theta;
// 转角公式?
for(int j=0;j<4;j++){
// 这个bug好难
PDD t = rotate(dx[j]*((b/2)-r),dy[j]*((a/2)-r),-theta);
A[++idx] = {x + t.xx, y + t.yy};
}
}
// cout<<idx<<endl;
// for(int i=1;i<=idx;i++){
// cout<<i<<" "<<A[i].xx<<" "<<A[i].yy<<endl;
// }
// return 0;
double res= get();
printf("%.2lf",res + 2*pi*r);
return 0;
}