计算几何学习笔记

· · 算法·理论

说的比较简略,不保证全对。

基本运算

二维向量

加减

(x_1,y_1)+(x_2,y_2)=(x_1+y_1,x_2+y_2) (x_1,y_1)-(x_2,y_2)=(x_1-y_1,x_2-y_2)

对于平面上的两个点 A(x_1,y_1),B(x_2,y_2),则向量 \overrightarrow{AB}=B-A

数乘

k(x,y)=(kx,ky)

模长

|(x,y)|=\sqrt{x^2+y^2}

点乘

结果是数值:

(x_1,y_1)\cdot(x_2,y_2)=x_1x_2+y_1y_2

设向量 \vec a,\vec b 的夹角为 \theta,则:

\theta=0,则 \vec a\cdot\vec b=|a||b|

0<\theta<90^\circ,则 \vec a\cdot \vec b>0

\theta=90^\circ,则 \vec a\cdot\vec b=0

90^\circ<\theta<180^\circ,则 \vec a\cdot\vec b<0

\theta=180^\circ,则 \vec a\cdot\vec b=-|a||b|

叉乘

结果是向量,其模长为:

|(x_1,y_1)\times(x_2,y_2)|=x_1y_2-x_2y_1

这个结果的绝对值等于 \vec a\vec b 围成的平行四边形的面积。

\vec a\times\vec b<0,则 \vec a\vec b 的逆时针方向。

\vec a\times\vec b=0,则 \vec a\vec b 共线。

\vec a\times \vec b>0,则 \vec a\vec b 的顺时针方向。

下文叉乘代表结果的模长。

旋转

### 两点距离 $AB=|\overrightarrow{AB}|

线

用两个点 A,B 表示有向线段 AB

点在直线哪一侧

\overrightarrow{AB}\times \overrightarrow{AC}>0,则 CAB 左侧。

\overrightarrow{AB}\times \overrightarrow{AC}=0,则 CAB 上。

\overrightarrow{AB}\times \overrightarrow{AC}<0,则 CAB 右侧。

点是否在线段上

### 两线段是否相交 先判断是否有一个线段端点在另一个线段上。 $AB$ 和 $CD$ 相交的条件为 $A,B$ 在直线 $CD$ 的不同侧且 $CD$ 在 $AB$ 的不同侧。 ### 两直线交点 要求直线 $AB$ 和直线 $CD$ 的交点。 直线 $AB$ 上的点可表示为 $A+k\overrightarrow{AB}$。 则 $A+k_1\overrightarrow{AB}=C+k_2\overrightarrow{CD}$。 两边同时叉乘 $\overrightarrow{CD}$ 得: $$A\times \overrightarrow{CD}+k_1\overrightarrow{AB}\times \overrightarrow{CD}=C\times \overrightarrow{CD}$$ 解得 $k_1=\frac{\overrightarrow{AC}\times\overrightarrow{CD}}{\overrightarrow{AB}\times \overrightarrow{CD}}$。 交点即为 $A+k_1\overrightarrow{AB}$。 ## 图形 ### 三角形面积 面积等于对应梯形面积的一半,而梯形面积可以叉乘求出。 $$S_{\triangle ABC}=|\frac{\overrightarrow{AB}\times \overrightarrow{AC}}2|$$ ### 凸多边形面积 把多边形剖成三角形,面积相加。 ### 判断点是否在凸多边形内部 将这个点与凸多边形的边构成的三角形面积相加,判断是否等于凸多边形面积,单次 $O(n)$。 一个更快的做法是将所有顶点按极角排序,二分出来要查询的点位于哪个三角形,复杂度 $O(\log n)$。 ## 三维向量 ### 加减 $$(x_1,y_1,z_1)+(x_2,y_2,z_2)=(x_1+x_2,y_1+y_2,z_1+z_2)$$ $$(x_1,y_1,z_1)-(x_2,y_2,z_2)=(x_1-x_2,y_1-y_2,z_1-z_2)$$ ### 点乘 $$(x_1,y_1,z_1)\cdot(x_2,y_2,z_2)=x_1x_2+y_1y_2+z_1z_2$$ 几何意义同二维向量。 ### 叉乘 叉乘结果是一个向量。 $$(x_1,y_1,z_1)\times (x_2,y_2,z_2)=(y_1z_2-y_2z_1,z_1x_2-z_2x_1,x_1y_2-x_2y_1)$$ 模长为对应平行四边形面积,方向用右手定则判断。 ### 平面 可以用三个不共线的点确定一个平面。 #### 法向量 设 $A,B,C$ 为平面上逆时针排列的三个点,则 $A,B,C$ 所在平面的法向量为:$\overrightarrow{AB}\times \overrightarrow{AC}$,方向垂直于这个平面向上。 #### 判断一个点在平面的哪一侧 要判断点 $D$ 在 $A,B,C$ 所在平面的哪一侧。 先求出平面的法向量 $\overrightarrow{AP}$,用点乘算出 $AP$ 和 $AD$ 的夹角是锐角还是钝角,若锐角,则在平面上方。 ### 判断一个点是否在三个点确定的圆内 要判断点 $D$ 是否在经过 $A,B,C$ 的圆内,先把所有点 $(x,y)$ 变换为三维向量 $(x,y,x^2+y^2)$。 若 $D$ 在 $A,B,C$ 所在平面的下方,在 $D$ 在 $A,B,C$ 确定的圆内部。 # 极角排序 $(x,y)$ 的极角可以使用函数 ```atan2(y,x)``` 求出。 按极角排序相当于个所有点按逆时针顺序排序。 [[CQOI2017] 小Q的草稿](https://www.luogu.com.cn/problem/P3699) 以每个点为中心,统计这个点的贡献。 极角排序后,类似扫描线,每次加入一条线段,删除一条线段或查询一个点,set 维护即可。 [Bob and stages](https://www.luogu.com.cn/problem/CF852H) 枚举起点,设 $f_{k,x}$ 表示选到第 $k$ 个点,最后一个点是 $x$ 的最大面积。 但是要求凸性,所以需要记倒数第二个点是什么。 可以提前把所有两两之间的连线按极角排序,从小到大转移,这样,凸包的限制自动满足。 然后就是判断一个三角形内是否有其他点。 可以把所有点按极角排序,枚举三角形的一条边,另一条逆时针扫一圈。 # 坐标旋转 有些题,为了更方便做,可以把所有点绕原点旋转一个角度,或平移一段距离。 [[SDOI2018] 物理实验](https://www.luogu.com.cn/problem/P4605) 把直线导轨转到与 $y$ 轴重合的位置,把所有端点投到 $y$ 轴上离散化,扫描线维护一下,这样每一段区间能照到的线段是唯一的,双指针维护即可。 # 二维凸包 先找到最靠下的点,若有多个取最靠左的。 这个点一定在凸包上,以这个点为中心极角排序,若极角相同按照到这个点的距离排序。 然后用栈维护当前凸包。加入一个点,从栈顶弹出不合法的点,再把当前的点加进去。 最后,栈里面的点就是凸包顶点。 ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; const double eps=1e-12,pi=acos(-1); struct node{ double x,y; friend node operator-(node a,node b){ return {a.x-b.x,a.y-b.y}; } }g[N]; double jj(node x){ return atan2(x.y,x.x); } double dis(node a,node b){ return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } double xl(node x,node y){ auto tmp=jj(y-x); if(tmp<0)tmp+=2*pi; return tmp; } int n,q[N],tl; signed main(){ cin>>n; int wz=0; for(int i=1;i<=n;i++){ cin>>g[i].x>>g[i].y; if(!wz||g[i].y<g[wz].y||(g[i].y==g[wz].y&&g[i].x<g[wz].x))wz=i; } swap(g[wz],g[1]); sort(g+2,g+n+1,[&](node a,node b){ return jj(a-g[1])<jj(b-g[1]); }); q[++tl]=1; for(int i=2;i<=n;i++){ while(tl>1&&xl(g[q[tl]],g[i])<xl(g[q[tl-1]],g[q[tl]])-eps)tl--; q[++tl]=i; } double res=0; for(int i=2;i<=tl;i++)res+=dis(g[q[i]],g[q[i-1]]); res+=dis(g[q[1]],g[q[tl]]); printf("%.2lf",res+eps); } ``` 另一种做法是以 $x$ 为第一关键字,$y$ 为第二关键字排序。 然后从左往右跑一边上面类似的做法。 这样会跑出来一个下凸壳,倒着再跑一遍即可得到完整凸包。 此做法精度误差更小。 ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int N=2e5+5; const double eps=1e-12,pi=acos(-1); struct node{ double x,y; friend node operator-(node a,node b){ return {a.x-b.x,a.y-b.y}; } }g[N]; double cro(node a,node b){ return a.x*b.y-a.y*b.x; } double dis(node a,node b){ return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } int n,q[N],tl; signed main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>g[i].x>>g[i].y; } sort(g+1,g+n+1,[&](node a,node b){ if(a.x==b.x)return a.y<b.y; return a.x<b.x; }); for(int i=1;i<=n;i++){ while(tl>1&&cro(g[q[tl-1]]-g[q[tl]],g[i]-g[q[tl]])>eps)tl--; q[++tl]=i; } for(int i=n;i>=1;i--){ while(tl>1&&cro(g[q[tl-1]]-g[q[tl]],g[i]-g[q[tl]])>eps)tl--; q[++tl]=i; } double res=0; for(int i=2;i<=tl;i++)res+=dis(g[q[i]],g[q[i-1]]); printf("%.2lf",res+eps); } ``` [[ICPC 2021 Nanjing R] Paimon Polygon](https://www.luogu.com.cn/problem/P9845) 可以发现,划分出来的两个凸包要么包含,要么相离。 若包含,则大凸包就是整个点集的凸包,直接求出来整个点集的凸包,判断剩余的点集是否合法即可。 若相离,则极角排序后,两个凸包包含的点对于两个区间。 对于每个点,判断这个点能否作为左端点,右端点和中间点,均摊下来可以 $O(n)$ 统计答案。 # 闵可夫斯基和 对于两个凸包 $\{(x,y)|(x,y)\in A\}$ 和 $\{(x,y)|(x,y)\in B\}$,闵可夫斯基和的结果为 $\{(x_1+x_2,y_1+y_2)|(x_1,y_1)\in A,(x_2,y_2)\in B\}$ 的凸包。 具体求法是把凸包的边变成向量,按极角排序再合并起来。 归并排序可以做到 $O(n)$。 [[JSOI2018] 战争](https://www.luogu.com.cn/problem/P4557) 问题等价于求出点集 $A$ 和 $-B$ 的闵可夫斯基和,然后多次询问一个点是否在结果凸包内。 用上文提到的方法二分查询,总复杂度 $O(n\log n)$。 # 半平面交 给定若干条有向直线,每条直线表示保留这条直线左侧的半平面。求所有半平面的交。 把直线按极角排序,增量构造。 维护双端队列,表示当前有用的直线。 若队尾的两条直线的交点位于当前处理的直线的右侧,则最后一条直线就没用了。 由于最后会成为一个凸包,所以要给队头做同样的判断。 最后还要用队头更新队尾。 ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int N=1e4+5; const double eps=1e-12; struct node{ double x,y; friend node operator-(node a,node b){ return {a.x-b.x,a.y-b.y}; } friend node operator*(double a,node b){ return {a*b.x,a*b.y}; } friend node operator+(node a,node b){ return {a.x+b.x,a.y+b.y}; } }; double jj(node x){ return atan2(x.y,x.x); } bool chk(double x){ return abs(x)<=eps; } double cro(node a,node b){ return a.x*b.y-a.y*b.x; } struct vec{ node a,b; friend bool operator<(vec a,vec b){ if(chk(jj(a.b-a.a)-jj(b.b-b.a)))return cro(a.a-b.a,b.b-b.a)<-eps; return jj(a.b-a.a)<jj(b.b-b.a); } }; node jd(vec a,vec b){ double k=cro(a.a-b.a,b.b-b.a)/cro(b.b-b.a,a.b-a.a); return a.a+k*(a.b-a.a); } bool chk(vec a,vec b,vec c){ auto x=jd(a,b); return cro(x-c.a,c.b-c.a)>-eps; } double S(node a,node b,node c){ return cro(b-a,c-a)/2; } vector<vec>q; vec qs[N]; node g[N]; int n,hd,tl; signed main(){ cin>>n; for(int i=1;i<=n;i++){ vector<node>jl; int m; cin>>m; while(m--){ double x,y; cin>>x>>y; jl.push_back({x,y}); } for(int i=1;i<jl.size();i++)q.push_back({jl[i-1],jl[i]}); q.push_back({jl.back(),jl[0]}); } sort(q.begin(),q.end()); vector<vec>tmp; double lst=1e9; for(auto c:q){ if(chk(jj(c.b-c.a)-lst))continue; lst=jj(c.b-c.a); tmp.push_back(c); } q=tmp; hd=1;tl=0; for(auto c:q){ while(hd<tl&&chk(qs[tl-1],qs[tl],c))tl--; while(hd<tl&&chk(qs[hd+1],qs[hd],c))hd++; qs[++tl]=c; } while(hd<tl&&chk(qs[tl-1],qs[tl],qs[hd]))tl--; int idx=0; for(int i=hd+1;i<=tl;i++)g[++idx]=jd(qs[i],qs[i-1]); g[++idx]=jd(qs[hd],qs[tl]); double res=0; for(int i=3;i<=idx;i++)res+=S(g[1],g[i-1],g[i]); printf("%.3lf",res+eps); } ``` [[SCOI2015] 小凸想跑步](https://www.luogu.com.cn/problem/P4250) [[ZJOI2019] 浙江省选](https://www.luogu.com.cn/problem/P5328) # 反演 > 反演变换适用于题目中存在多个圆/直线之间的相切关系的情况。利用反演变换的性质,在反演空间求解问题,可以大幅简化计算。 对于一个反演半径 $r$ 和反演中心 $O$,$P$ 的反演点 $P'$ 满足 $O,P,P'$ 共线,且 $OP\times OP'=r^2$。 有两个比较有用的性质: - 过点 $O$ 的圆反演图形为一条直线。 - 在原图相切的图形反演后也相切。 [Drawing Circles is Fun](https://www.luogu.com.cn/problem/CF372E) 以 $O$ 为反演中心,任意长度为反演半径进行反演变换。 原图中两个经过 $O$ 的相切的圆反演后就是两条平行线。 于是原图的限制变成了对边平行,也就是任意两条线段作为对角线都能得到平行四边形。于是这个问题就变成了容易的问题。 # 半平面数据结构 给定 $n$ 条直线,$q$ 次询问,每次询问一个点下方有几条直线。 把直线每 $T$ 个分成一块,分开处理。 对于一组直线,枚举直线两两之间的交点。 扫描线的过程中维护直线的相对顺序,遇到交点就调整,二分查询。 总复杂度 $O((T^2+q\log T)\frac nT)$。 取 $T=\sqrt q$,总复杂度 $O(n\sqrt q)$。 [【UNR #4】己酸集合](https://uoj.ac/problem/553) # 剖分 ## 梯形剖分 多边形不是凸的了。 从前往后扫描线,处理出来每条边的方向。 剖出来的竖直的先必须满足向左的边在上,向右的边在下,这样就能避免剖到多边形外面。 [Spider](https://www.luogu.com.cn/problem/CF223D) ## Delaunay 三角剖分 把平面上若干个点剖成三角形。 先把所有点随机扰动,使得不存在四点共圆或三点共线。 把所有点按 $x$ 排序,分治构造。 假设现在已经构造出了左半边和右半边。 先找到最靠下的连接左右两边的边。具体实现可以求出来这个点集的下凸壳,找到连接左右两半的那条。 假设找到的点为 $A,B$。 找到右侧与 $B$ 相连的点 $C$,使得: - $C$ 在 $\overrightarrow{BA}$ 的顺时针方向。 - 不存在与 $B$ 相连的其他点 $D$,使得 $D$ 在 $A,B,C$ 构成的圆内部。 左半边的点同理。 通过打擂台的方式可以找到唯一的一个点。 假设这个点位于右半边,为 $C$。 连接 $AC$,删掉与 $B$ 相连的与 $AC$ 有交的边。 以 $AC$ 为新的最底部的边重复这个过程。 总复杂度 $O(n\log n)$,我实现的似乎并不是这个复杂度,但在 $10^5$ 的数据范围下能跑。 [平面欧几里得最小生成树](https://www.luogu.com.cn/problem/P6362) 先跑出来三角剖分,只保留三角剖分的边跑最小生成树即可。 ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; const double eps=1e-9; struct vec{ double x,y,z; friend vec operator-(vec a,vec b){ return {a.x-b.x,a.y-b.y,a.z-b.z}; } }; vec cro(vec a,vec b){ return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x}; } double dot(vec a,vec b){ return a.x*b.x+a.y*b.y+a.z*b.z; } bool chk(vec a,vec b,vec c,vec x){ return dot(cro(b-a,c-a),x-a)<=0; } struct node{ double x,y; void rnd(){ x+=((int)rand()-RAND_MAX/2)*1.0/RAND_MAX*eps; y+=((int)rand()-RAND_MAX/2)*1.0/RAND_MAX*eps; } friend bool operator<(node a,node b){ return a.x<b.x; } friend node operator-(node a,node b){ return {a.x-b.x,a.y-b.y}; } }p[N]; vec to(node x){ return {x.x,x.y,x.x*x.x+x.y*x.y}; } bool in(node a,node b,node c,node x){ return chk(to(a),to(b),to(c),to(x)); } double mc(node x){ return x.x*x.x+x.y*x.y; } double Dis(node a,node b){ return mc(b-a); } double cro(node a,node b){ return a.x*b.y-a.y*b.x; } bool in(node a,node b,node x){ if(cro(a-x,b-x))return 0; if(Dis(a,x)>Dis(a,b))return 0; if(Dis(b,x)>Dis(a,b))return 0; return 1; } bool xj(node a,node b,node c,node d){ if(in(c,d,a)||in(c,d,b)||in(a,b,c)||in(a,b,d))return 0; return ((cro(a-c,d-c)>0)^(cro(b-c,d-c)>0))&((cro(c-a,b-a)>0)^(cro(d-a,b-a)>0)); } unordered_set<int>e[N]; int n,fa[N],qs[N],tl; void solve(int l,int r){ if(r-l+1<=3){ for(int i=l;i<=r;i++){ for(int j=l;j<=r;j++){ if(i==j)continue; e[i].insert(j); } } return; } int mid=l+r>>1; solve(l,mid);solve(mid+1,r); int a=0,b=0,s1=0,s2=0; tl=0; for(int i=l;i<=r;i++){ while(tl>1&&cro(p[qs[tl-1]]-p[qs[tl]],p[i]-p[qs[tl]])>=0)tl--; qs[++tl]=i; } for(int i=1;i<tl;i++){ if(qs[i]<=mid&&qs[i+1]>mid){ a=qs[i];b=qs[i+1]; break; } } auto fd1=[&](){ int x=0; for(auto c:e[a]){ if(cro(p[a]-p[b],p[c]-p[b])>=0)continue; if(!x){ x=c; continue; } // cout<<c<<" "<<x<<" "<<in(p[a],p[b],p[x],p[c])<<" "<<in(p[a],p[b],p[c],p[x])<<endl; if(in(p[a],p[b],p[x],p[c]))x=c; } return x; }; auto fd2=[&](){ int x=0; for(auto c:e[b]){ if(cro(p[b]-p[a],p[c]-p[a])<=0)continue; if(!x){ x=c; continue; } if(in(p[a],p[b],p[x],p[c]))x=c; } return x; }; while(1){ e[a].insert(b);e[b].insert(a); s1=fd1();s2=fd2(); if(!s1&&!s2)break; if(!s2||(s1&&in(p[a],p[b],p[s2],p[s1]))){ vector<int>jl; for(auto c:e[a]){ if(xj(p[a],p[c],p[b],p[s1]))jl.push_back(c); } for(auto c:jl){ e[a].erase(e[a].find(c)); e[c].erase(e[c].find(a)); } a=s1; s1=0; }else{ vector<int>jl; for(auto c:e[b]){ if(xj(p[b],p[c],p[a],p[s2]))jl.push_back(c); } for(auto c:jl){ e[b].erase(e[b].find(c)); e[c].erase(e[c].find(b)); } b=s2; s2=0; } } } struct edge{ int x,y;double z; friend bool operator<(edge a,edge b){ return a.z<b.z; } }; vector<edge>q; int fd(int x){ if(x==fa[x])return x; return fa[x]=fd(fa[x]); } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++)cin>>p[i].x>>p[i].y,p[i].rnd(); sort(p+1,p+n+1); solve(1,n); for(int i=1;i<=n;i++){ for(auto c:e[i])q.push_back({i,c,sqrt(Dis(p[i],p[c]))}); } for(int i=1;i<=n;i++)fa[i]=i; double res=0; sort(q.begin(),q.end()); for(auto[x,y,z]:q){ x=fd(x);y=fd(y); if(x==y)continue; fa[x]=y; res+=z; } printf("%.6lf",res); } ``` # 三维凸包 增量构造。 先随机扰动,这样凸包上的每个面都是三角形。 直接存下来所有三角形。 每次加入一个点,把这个点能看到的面删掉,再把边界处的边和这个点连起来,构成新的凸包。 可以直接枚举所有面,只有边界上的边会有恰好一个邻面被删了。 总复杂度 $O(n^2)$,代码很好写。 [【模板】三维凸包](https://www.luogu.com.cn/problem/P4724) ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int N=2005; const double eps=1e-8; struct node{ double x,y,z; void rnd(){ x+=(rand()-RAND_MAX/2)*1.0/RAND_MAX*eps; y+=(rand()-RAND_MAX/2)*1.0/RAND_MAX*eps; z+=(rand()-RAND_MAX/2)*1.0/RAND_MAX*eps; } friend node operator-(node a,node b){ return {a.x-b.x,a.y-b.y,a.z-b.z}; } }p[N]; double mc(node x){ return sqrt(x.x*x.x+x.y*x.y+x.z*x.z); } double dot(node a,node b){ return a.x*b.x+a.y*b.y+a.z*b.z; } node cro(node a,node b){ return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x}; } struct face{ int a,b,c; }; vector<face>q; bool chk(node a,node b,node c,node d){ return dot(cro(b-a,c-a),d-a)>0; } int n; set<pair<int,int> >jl; void add(int x,int y){ if(jl.count({y,x}))jl.erase(jl.find({y,x})); else jl.insert({x,y}); } signed main(){ cin>>n; for(int i=1;i<=n;i++)cin>>p[i].x>>p[i].y>>p[i].z,p[i].rnd(); q.push_back({1,2,3}); q.push_back({3,2,1}); for(int i=4;i<=n;i++){ vector<face>tmp; jl.clear(); for(auto [a,b,c]:q){ if(chk(p[a],p[b],p[c],p[i])){ add(a,b);add(b,c);add(c,a); }else tmp.push_back({a,b,c}); } for(auto [a,b]:jl)tmp.push_back({a,b,i}); q=tmp; } double res=0; for(auto [a,b,c]:q)res+=mc(cro(p[b]-p[a],p[c]-p[a]))/2; printf("%.3lf",res+eps); } ```