基础知识

像一些点、向量、点乘、叉乘、圆等等知识,应该都是最基本的平面几何知识,这里就不再赘述,主要考虑如何实现这些平面几何的运算。

首先是定义一些常量和宏定义:

#define RAD(x) (pi*x/180)
#define DEG(x) (x*180/pi)
const double eps=1e-8;
const double pi=acos(-1);
int dcmp(double x) {return fabs(x)<eps?0:(x<0?-1:1);}

其中dcmp是一个三态函数,用于判断一个浮点数与0的关系,排除了精度误差。

然后是向量的定义(注意虽然在几何中点和向量是本质不同的东西,但是它们的形式都是一样的):

typedef struct point
{double x,y;point(double x=0,double y=0):x(x),y(y) {}friend ostream & operator << (ostream &,point a) {cout<<fixed<<setprecision(3)<<a.x<<' '<<a.y; return cout;}point operator + (point b) {return point(x+b.x,y+b.y);}point operator - (point b) {return point(x-b.x,y-b.y);}point operator * (double p) {return point(x*p,y*p);}point operator / (double p) {return point(x/p,y/p);}double operator * (point b) {return x*b.x+y*b.y;}       //点乘、内积double operator ^ (point b) {return x*b.y-y*b.x;}       //叉乘、外积bool operator < (const point &b) const {return x<b.x || (x==b.x && y<b.y);}
}vec;

其中叉乘的意义需要特别注意一下,如果向量 a×ba \times ba×b 是一个正数,那么由右手螺旋定则可以知道,bbb 在 aaa 的逆时针方向;里面的友元函数是为了方便格式化输出,调试用。

接下来是的定义:

struct circle
{point c; double r;circle(point c,double r):c(c),r(r) {}friend ostream & operator << (ostream &,circle a) {cout<<fixed<<setprecision(3)<<a.c<<'\t'<<a.r;return cout;}
};

接下来是直线的定义:

struct line
{point p; vec v; double ang;line(point p=point(0,0),vec v=vec(0,1)):p(p),v(v) {ang=atan2(v.y,v.x);}bool operator < (const line &l) const {return ang<l.ang;}
};

注意atan2的取值范围是0到 π\piπ,对于直线来说刚好(射线就不行)。

接下来是多边形和**直线集(或者称为多面集)**的定义:

typedef vector<point> polygon;
typedef vector<line> lineset;

注意这里的多边形顶点一定要按照某一时针方向储存!(不够智能?不然鬼知道怎么给这些点排序)

基础运算

向量长度:向量的二范数,也就是这个向量自己点乘自己然后开方。

double length(vec a) {return sqrt(a*a);}

向量夹角:两个向量的夹角,范围为[0,π][0,\pi][0,π] ,可以用余弦定理求得。

double angle(vec a,vec b) {return acos(a*b/length(a)/length(b));}

三角形面积:这个分为两种表示形式,一种是两个向量的形式,一种是三个顶点的形式。

double tri_area(vec a,vec b) {return fabs(a^b)/2;}
double tri_area(point a,point b,point c) {return fabs((b-a)^(c-a))/2;}

向量旋转:将一个向量逆时针旋转某一度数,我们知道旋转可以通过左乘一个矩阵来实现。

vec vec_rotate(vec a,double x) {return vec(a.x*cos(x)-a.y*sin(x),a.x*sin(x)+a.y*cos(x));}

两直线交点:两条直线的交点,要保证必须有交点,即两直线不平行。可以通过参数方程求解。

point line_intersect(line a,line b)
{double t=((b.p-a.p)^b.v)/(a.v^b.v);return a.p+(a.v*t);
}

点到直线距离:一个点到一条直线的距离。用三角形面积公式求解。

double dis_to_line(point a,line b)
{point c=b.p+b.v-a;return fabs((c^b.v)/length(b.v));
}

点在直线的投影:点在直线上的投影点。参数方程结合内积求解。

point line_projection(point a,line b)
{double t=((a*b.v)-(b.p*b.v))/(b.v*b.v);return b.p+b.v*t;
}

点关于直线的对称:一个点关于一条直线的对称点。算出投影后延长即可。

point line_symmetry(point a,line b)
{point c=line_projection(a,b);return a+((c-a)*2);
}

多边形周长:一个多边形的周长,计算每边边长求和即可。

double polygon_perimeter(polygon a)
{double ans=0;REP(i,0,a.size()-2) ans+=length(a[i]-a[i+1]);return ans+length(a[a.size()-1]-a[0]);
}

多边形面积:一个多边形的面积,拆分成三角形求解。(下面代码对任意多边形(凸、凹)适用)

double polygon_area(polygon a)
{double ans=0;REP(i,1,a.size()-2) ans+=((a[i]-a[0])^(a[i+1]-a[0]));return fabs(ans/2);
}

三角形外接圆:三角形的外接圆,圆心坐标可以用两点之间距离公式推导出来。

circle circumscribed_circle(point p1,point p2,point p3)
{double bx=p2.x-p1.x,by=p2.y-p1.y;double cx=p3.x-p1.x,cy=p3.y-p1.y;double d=(bx*cy-by*cx)*2;double ax=(cy*(bx*bx+by*by)-by*(cx*cx+cy*cy))/d+p1.x;double ay=(bx*(cx*cx+cy*cy)-cx*(bx*bx+by*by))/d+p1.y;point p(ax,ay);return circle(p,length(p1-p));
}

三角形内切圆:三角形的内切圆。

circle inscribed_circle(point p1,point p2,point p3)
{double a=length(p2-p3);double b=length(p3-p1);double c=length(p1-p2);point p=(p1*a+p2*b+p3*c)/(a+b+c);return circle(p,dis_to_line(p,line(p1,p2-p1)));
}

凸包

在实向量空间中,对于给定的集合 SSS,所有包含 SSS 的凸集的交集定义为这个集合的凸包。详细内容可以参看《凸优化》中凸集相关部分的描述。这里只给出几条性质(好像没什么用):

  • 凸包内任意两点的凸组合仍然在凸包内;
  • 一个集合是凸的当且仅当它的凸包是它本身;

对于二维平面点集,求解凸包的常用方法是Graham扫描法(复杂度 nlog⁡nn\log nnlogn )。具体做法是首先按照横坐标对点集排序,然后用栈维护一个下凸序列,左右扫两次就可以了。代码如下:

polygon convex_hull(polygon a)
{sort(a.begin(),a.end());int m=0,n=a.size();point *q=new point[n+5];REP(i,0,n-1){while(m>1 && ((q[m-1]-q[m-2])^(a[i]-q[m-2]))<=0) m--;q[m++]=a[i];}int k=m;REP_(i,n-2,0){while(m>k && ((q[m-1]-q[m-2])^(a[i]-q[m-2]))<=0) m--;q[m++]=a[i];}if(n>1) m--;polygon ans;REP(i,0,m-1) ans.push_back(q[i]);return ans;
}

旋转卡壳

这是一种思维,本质上是挖掘了凸多边形点边距的规律(先增后减,极值单方向变化)。

  • 旋转卡壳有16种读音,但我认为最正确的一种应该是——旋(xuaˊn)转(zhuaˇn)卡(qiaˇ)壳(keˊ)旋(xu\acute{a}n)转(zhu\check{a}n)卡(qi\check{a})壳(k\acute{e})旋(xuaˊn)转(zhuaˇn)卡(qiaˇ)壳(keˊ) 。

旋转卡壳可以用来解决类似最远点对最远点边距最小面积矩形覆盖等等(先取凸包),下图是旋转卡壳求最远点边距的示意图:

当单方向枚举边时,对应最优解的点的变化也是单方向的,并且枚举完所有边之后,最优解的点也刚好旋转了一圈。可以看出这样的算法复杂度是O(n)的。

其它的算法类似:最远点对旋转时可以用下一个点的变化方向来判断该谁旋转。比如说当前处理到p[i]和p[j],其中p是凸包,那么就根据(p[i+1]-p[i])和(p[j+1]-p[j])的夹角来判断就行了(把图画出来就很好理解)。

求点集的最远点对代码如下:

double max_point_dis(polygon a)
{polygon p=convex_hull(a);int n=p.size(),i=0,j=0,si,sj;if(n==2) return length(p[0]-p[1]);REP(k,0,n-1){if(!(p[i]<p[k])) i=k;if(p[j]<p[k]) j=k;}double ans=0; si=i,sj=j;while(i!=sj || j!=si){ans=max(ans,length(p[i]-p[j]));(((p[(i+1)%n]-p[i])^(p[(j+1)%n]-p[j]))<0)?(i=(i+1)%n):(j=(j+1)%n);}return ans;
}

对于最小面积或周长矩形覆盖问题其实是一样的,容易想到(其实就是感觉)最优解一定会有一条边和凸包的某条边重合,于是只要枚举这些边,其实整个矩形就已经定下来了。这里随着边的变化有三个点跟着同方向移动,枚举边求相应最小值即可。

半平面交

给定多个半平面(即直线的某一侧无限大区域),求所有半平面的交。基本思路是将所有直线按照极角排序,然后维护一个双端队列(因为同一个极角对应了两个直线方向,所以双端都要维护),加入新的半平面时通过比较这条直线和上一个交点的关系去维护队列。

我们规定直线的左侧是合法的半平面,最后输出所有半平面交出来的凸多边形(一定是凸的,因为半平面本身就是一个凸集,而有限个凸集的交一定是凸集),代码如下:

bool on_left(point p,line L) {return (L.v^(p-L.p))>0;}
polygon half_plane_intersect(lineset L)
{sort(L.begin(),L.end());int l=0,r=0,n=L.size();point *p=new point[n];line *q=new line[n];q[0]=L[0];REP(i,1,n-1){while(l<r && !on_left(p[r-1],L[i])) r--;while(l<r && !on_left(p[l],L[i])) l++;q[++r]=L[i];if(fabs(q[r].v^q[r-1].v)<eps){r--;if(on_left(L[i].p,q[r])) q[r]=L[i];}if(l<r) p[r-1]=line_intersect(q[r-1],q[r]);}while(l<r && !on_left(p[r-1],q[l])) r--;if(r-l<=1) return (polygon)0;p[r]=line_intersect(q[r],q[l]);polygon ans;REP(i,l,r) ans.push_back(p[i]);return ans;
}

如果题目不保证最后交出来的一定是一个有限的凸多边形,那么我们可以添加一个坐标很大的矩形框,最后再把它删除就可以了。

最小圆覆盖

找到一个最小半径的圆,覆盖平面上所有给定的点。

做法是使用随机增量法,大致思路是在添加一个新的点时更新当前圆的圆心和半径,如果新增的点在当前圆内,则什么也不做;如果新增的点在当前圆外,则以新增的点为圆心,双重枚举之前的所有点,找到一个最小的圆。由于最小圆覆盖的圆一定经过某两个点或者某三个点,所以算法正确性得到保证,另外可以证明在随机化的条件下,时间复杂度的期望是O(n) 。

代码:

circle min_circle(polygon p)
{random_shuffle(p.begin(),p.end());int n=p.size();point c=p[0];double r=0;REP(i,0,n-1){if(dcmp(length(c-p[i])-r)<=0) continue;c=p[i],r=0;REP(j,0,i-1){if(dcmp(length(c-p[j])-r)<=0) continue;c=(p[i]+p[j])/2; r=length(c-p[i]);REP(k,0,j-1){if(dcmp(length(c-p[k])-r)<=0) continue;circle x=circumscribed_circle(p[i],p[j],p[k]);c=x.c,r=x.r;}}}return circle(c,r);
}

扫描线

首先介绍离散化的思想,一般来说有些数值太大无法直接使用数组下标表示,我们就从小到大给每个数分配一个id,然后用这个id表示这个数。每次可以使用二分查找在logn的时间内查询某个数的id。代码实现如下:

sort(a+1,a+1+n);
len=unique(a+1,a+n+1)-a-1;

查询id:

int ID(int x) {return lower_bound(a+1,a+len+1,x)-a;}

扫描线,顾名思义就是一条线在图形上扫描,但是肯定是按照一定的顺序和关键点去扫描的,比较经典的应用是计算平面内多个矩形的覆盖面积。大致思路如下:

将所有横坐标离散化,纵坐标当做扫描线的关键点(也即每次递增扫下一个纵坐标),用线段树维护相邻两个横坐标之间的线段出现的次数,每经过一个矩形的下边沿,相应所有线段出现次数加一,反之经过一个上边沿,出现次数减一;对于相邻两个纵坐标统计有效边沿长度和,累计答案。

这里的线段树的使用有一些技巧,因为我们需要的是某一个区间中所有出现次数大于0的结点的长度和,所以lazy标签可以不用pushdown,然后如果当前区间的lazy大于0,其长度和可以直接计算;否则,其长度和为其左右儿子的长度和之和。

下面是一道经典例题代码:(HDU1542 Atlantis)

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
typedef long long LL;const int maxn=1005;
int lazy[maxn],n,T,len;
double id[maxn],s[maxn];
struct edge
{double l,r,y;int flag;bool operator < (const edge &a) const {return y<a.y;}
}e[maxn];
int ID(double x) {return lower_bound(id+1,id+len+1,x)-id;}#define chl k<<1
#define chr k<<1|1
#define mid ((l+r)>>1)void push_up(int k,int l,int r)
{if(lazy[k]>0) s[k]=id[r+1]-id[l];else s[k]=s[chl]+s[chr];
}void update(int k,int l,int r,int ll,int rr,int flag)
{if(r<ll || l>rr) return;if(l>=ll && r<=rr){lazy[k]+=flag;push_up(k,l,r);return;}update(chl,l,mid,ll,rr,flag);update(chr,mid+1,r,ll,rr,flag);push_up(k,l,r);
}int main()
{//freopen("input.txt","r",stdin);while(~scanf("%d",&n)){if(!n) break;REP(i,1,n){double x,y,xx,yy;scanf("%lf%lf%lf%lf",&x,&y,&xx,&yy);id[2*i-1]=x; id[2*i]=xx;e[2*i-1]=(edge){x,xx,yy,-1};e[2*i]=(edge){x,xx,y,1};}n*=2;sort(e+1,e+n+1);sort(id+1,id+n+1);mem(s,0); mem(lazy,0);len=unique(id+1,id+n+1)-id-1;update(1,1,n-1,ID(e[1].l),ID(e[1].r)-1,e[1].flag);double ans=0;REP(i,2,n){ans+=(e[i].y-e[i-1].y)*s[1];update(1,1,n-1,ID(e[i].l),ID(e[i].r)-1,e[i].flag);}printf("Test case #%d\nTotal explored area: %.2f\n\n",++T,ans);}return 0;
}

ACM算法总结 平面几何相关推荐

  1. 繁凡的ACM算法全家桶(全新的模板整合计划)

    整理的算法模板合集: ACM模板 点我看算法全家桶系列!!! 实际上是一个全新的模板整合计划 ACM算法全家桶的所有内容的 Portable Document Format 版本全部 开 源 啦 !可 ...

  2. 如何出(改编)一道ACM算法题?

    本文背景 本人算法能力一般,但是为省赛和区域赛网络赛出过几道题,总结了一些自己的经验,希望与大家分享.本文不涉及具体的算法题,都是一些理论性的想法和一些建议. 题源 如果你能在没有题源的情况下自己想出 ...

  3. 【C语言、C++基础编程题】【基础类题集】【平顶山学院ACM算法攻关部】

    题源:平顶山学院ACM算法攻关部 目录 前言 1000:A+B Problem 1001:编写一个程序,输出指定信息 1002:求三个数最大值 1003:字符串加密 1004:计算圆柱体的面积体积等 ...

  4. ACM算法之基础算法

    ACM算法基础篇 基础算法有:枚举,递推,贪心,,分治,递归,构造,模拟,排序和检索 1.枚举:也叫穷举,是从问题所有可能解中一一枚举个元素,用题目给的检验条件判定哪些是无用的,那些是有用的,符合的解 ...

  5. acm算法有用吗?写给自己。

    acm算法有用吗?写给自己,也希望能帮助那些大一迷茫的人. 我是一个普通二本院校即将进入大三的acmer,一般问这个问题的人都是一个acmer的失败者,因为成功的人都去忙着学习该学习的算法了,不会问这 ...

  6. ACM算法训练【逆序对的数量】

    ACM算法训练[逆序对的数量] 题目说明 数据范围 样例 分析与代码 题目说明 数据范围 样例 分析与代码 ①归并排序基本思想: ②在归并的过程中,逆序对出现的三种情况: a.全部出现在左边的区间 b ...

  7. ACM算法训练【模拟队列】

    ACM算法训练[模拟队列] 1.题目 2.样例 3.代码 1.题目 2.样例 输入样例: 10 push 6 empty query pop empty push 3 push 4 pop query ...

  8. 2013 五月份以前的一年半ACM算法生涯

    我是大学入学后才开始接触ACM算法的.在2012入学第一学期,我开始学习了第一门程序设计语言,也就是经典的C语言,而我的ACM情缘也是从那时候开始的.那时候的我根本就不知道的什么是ACM,对它也没什么 ...

  9. 【组队学习】【36期】交大ACM算法课(C++)

    交大ACM算法课(C++) 航路开辟者:伯禹教育 领航员:吕豪杰.叶前坤.孙博腾.何锋丽.张腾飞.韩可航.韩江雪 航海士:赵子一.于沼懿.陈伟峰.孙昌慧.李小山.汪超.李牧轩.江玲新.孙超.吴昌广.光 ...

最新文章

  1. sql注入原理及解决方案
  2. 《Excel与VBA开发》一书上市时间
  3. 用python爬虫抓站的一些技巧总结
  4. 便捷的flex弹性布局
  5. 【第一届“文翁杯”现场竞技赛】(校内“欢乐”赛)T1—洗刷刷(dp)
  6. BizTalk开发系列(十二) Schema设计之Group与Order
  7. IntelliJ IDEA 14.x 创建工作空间与多个Java Web项目
  8. Python全栈开发之11、进程和线程
  9. windows2008下运行环境部署(iis,ftp,oracle)
  10. 华为p50 pro 鸿蒙,华为P50Pro真豪横!鸿蒙系统+真全屏5摄,网友:太香了!
  11. Serializer对象
  12. 颜色透明度16进制对照表
  13. 计算机功能转动怎么设定,win7如何使用屏幕旋转功能 电脑使用屏幕旋转功能操作方法...
  14. 人工智能算法(一)进化算法
  15. win10系统C盘出现感叹号及加密图标解除
  16. 对话加拿大工程院于非院士:寻找 AI 领域的「香农定理」
  17. 通信协议学习-485通信(1)
  18. 人工智能方面有什么创业项目_人工智能创业有哪些项目,其商机前景介绍
  19. LL库下STM32使用安信可VB离线语音识别
  20. kali-Firefox安装flash插件

热门文章

  1. 网线、集线器、交换机、路由器等
  2. Ubuntu20.04安装Pytorch:Anaconda3+Pycharm+Pytorch+CUDA
  3. ubuntu服务器版无线网卡,Ubuntu无线网卡驱动安装方法
  4. Int和Integer的区别
  5. hp1010警告灯亮
  6. SiTime MEMS硅晶振五大优势
  7. 终于有一款多功能的串口网络中控播放器
  8. 高效学习--BOST学习技巧组合
  9. 使用马哈鱼SQLFLow 1 分钟获取复杂 SQL 语句的血缘关系
  10. 雅虎美女CEO梅耶尔