计算几何算法汇总

command_block

2019-08-04 16:01:26

Personal

# 0.前面的话 首先,笔者是一个初三蒟蒻,数学(几何)还特别菜。 所以如果什么地方讲错了或者比较naive,请轻喷。 解析法由于斜率不存在等等麻烦的情况,容易写错,所以本文主要讨论**向量法**。 (如果您是初中巨佬,还不熟知向量的应用,建议自学一下高中数学的相关内容) 向量法由于三角函数等等原因,跑的比较慢,所以计算几何的出题人一般不会卡常数。 我们实现函数的时候要以**代码补偿**为主,不要过度追求常数而把自己绕晕。 本文中所有的证明均为**方便记忆**之用,可能并不严格或者漏了情况。 这篇文章内容多了之后说不定会分块。 # 1.简单函数 - ### 约定/宏 ```cpp #define db double const db eps=1e-?; //根据需要调整,一般在 1e-6 ~ 1e-12 之间 ``` - ### 符号函数`sign` 众所周知,浮点数运算是有**误差**的,那么就需要一个符号函数,忽略较小的误差。 ```cpp int sign(double x) {return x>eps ? 1 : (x<-eps ? -1 : 0);} ``` - ## 点(向量)结构体`Point` 有加减和数乘几种运算。 len是求模长。 ```cpp struct Point { db x,y; inline void print() {printf("(%.3lf,%.3lf)\n",x,y);} Point operator + (const Point &B) const {return (Point){x+B.x,y+B.y};} Point operator - (const Point &B) const {return (Point){x-B.x,y-B.y};} Point operator * (const double &B) const {return (Point){x*B,y*B};} Point operator / (const double &B) const {return (Point){x/B,y/B};} double len() {return sqr(x*x+y*y);} }; ``` - ## 叉积,内积 封装在结构体里面,`|`运算是内积,`^`运算是叉积(小心优先级!) ```cpp double operator | (const Point &B) const {return x*B.x+y*B.y;} double operator ^ (const Point &B) const {return x*B.y-y*B.x;} ``` 向量内积: $\vec a·\vec b=|a||b|cosθ=a.x*b.x+a.y*b.y$ 根据 $|a||b|cosθ$ 这个式子,向量内积的数值就等于 $a$ 在 $b$ 上的投影与 $b$ 的模长积。 由于包含投影,可以用来判断垂直 : 内积为$0$即垂直。 假如两个向量的方向较为相同,即夹角小于 $90$ 度,那么点积的结果 $>0$ ; 假如两个向量的方向较为不同,即夹角大于 $90$ 度,那么点积的结果 $<0$ ; 两个向量同时旋转相同角度,其内积结果不变(显然)。 向量叉积: $\vec a\times \vec b=a.x*b.y-a.y*b.x$ 几何意义是 $\vec a,\vec b$ 两个向量张成的平行四边形(有向)面积,假如**逆时针则符号为正**,否则符号为负。 取绝对值除以2就是这两个向量围成的三角形面积。 ![](https://cdn.luogu.com.cn/upload/pic/69041.png) 可以用来算面积,判断方向等等。 ~~更多内容请见必修四~~ - ## 旋转 把某个点绕原点逆时针旋转一个弧度,可以使用复数相乘,幅角相加来推导。 ```cpp const double Pi=acos(-1); Point rotate(const Point &A,double len) { double s1=sin(len),s2=cos(len); return (Point){A.x*s2-A.y*s1,A.x*s1+A.y*s2}; } ``` - ## 线`Line` 可以表示直线,射线和线段。 ```cpp db dis(Point A,Point B) {return sqrt((A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y));} struct Line { Point a,b; double len(){return dis(a,b);} }; ``` - ## 点在直线/线段上 ```cpp //判断点是否在直线上 //之所以写l0是因为直线有0个端点 bool onl0(Point A,Line L) {return sign((A-L.a)^(A-L.b))==0;} //先判断是否在直线上,然后在判断是否在四至矩形内 //写l2是因为线段有2个端点 bool onl2(Point A,Line L) { return sign((A-L.a)^(A-L.b))==0&& sign(A.x-min(L.a.x,L.b.x))>=0&& sign(A.y-min(L.a.y,L.b.y))>=0&& sign(A.x-max(L.a.x,L.b.x))<=0&& sign(A.y-max(L.a.y,L.b.y))<=0; } ``` `sign((A-L.a)^(A-L.b))==0` 意思就是点 $A$ 是否在 $L$ 所在的直线上,具体原理 : 众所周知,点 $C-D$ 会形成向量 $\overrightarrow{DC}$ 那么这个式子的意思就是:判断 $\overrightarrow{AL_a},\overrightarrow{AL_b}$ 这两个向量叉积是否为$0$。 也就相当于判断 $a,L_a,L_b$ 三点围成的三角形面积。不难发现 : 三点共线 $\Longleftrightarrow$ 面积为$0$。 如图: ![](https://cdn.luogu.com.cn/upload/pic/69024.png)![](https://cdn.luogu.com.cn/upload/pic/69025.png) 此后的四个条件是判断$A$是否在如右图矩阵中,注意可以视情况不取等号(一般要取)。 - ## 求两直线交点(常用) 调用该函数前请先排除两直线**重合**,**平行**等情况,否则会出现`nan`。 ```cpp Point inter(Line L1,Line L2) { double ls=(L1.b-L1.a)^(L2.a-L1.a); double rs=(L1.b-L1.a)^(L2.b-L1.a); return L1.a+(L1.b-L1.a)*ls/(ls-rs); } ``` 这东西的原理如下图: ![](https://cdn.luogu.com.cn/upload/pic/69037.png) 下称 `L1.a→A1, L1.b→B1, L2.a→A2, L2.b→B2`。 算出 $\overrightarrow{A_1B_1}$ 和 $\overrightarrow{A_1A_2}$ 的叉积,由于是逆时针转,得到的数值是 $S\triangle A_1B_1A_2$ 的 $2$ 倍,称作 $ls$。 算出 $\overrightarrow{A_1B_1}$ 和 $\overrightarrow{A_1B_2}$ 的叉积,由于是顺时针转,得到的数值是 $S\triangle A_1B_1B_2$ 的 $-2$ 倍,称作 $rs$。 然后观察这两个三角形,假如把红线当做底,那么他们是等高的,底边比就相当于面积比。 我们把 $\overrightarrow{A_2B_2}$ 这个向量放缩到 $\Large\frac{ls}{ls-rs}$ 倍(注意符号),那么加上 $A_2$ 刚好够到那个点。 以上只是一种情况,经过严谨的讨论其他的情况下也都是正确的(使用向量一般不需要特判)。 记忆方法 : 从 $A1$ 开始弄三个向量,叉左边右边,把中间横着的缩放。 - ## 点到线段/直线的最短距离 ```cpp //计算点到直线距离,利用叉积+面积法 double disl0(Point A,Line L) {return abs((L.a-A)^(L.b-A))/dis(L.a,L.b);} //计算点到线段距离,利用点积判方向 double disl2(Point A,Line L) { if(sign(A-L.a|L.b-L.a)<0||sign(A-L.b|L.a-L.b)<0) return min(dis(A,L.a),dis(A,L.b)); return disl0(A,L); } ``` 如果是直线,求出垂线段长度即可。 方法 : 求出直线上两点与该点围成的三角形的面积,然后除以三角形的底。 ![](https://cdn.luogu.com.cn/upload/pic/69043.png) 线段的话,如果垂线段不落在线段上,那就不算数。 我们就要判断一个点有没有资格使用垂线段。 ![](https://cdn.luogu.com.cn/upload/pic/69050.png) 由上图可知,只有蓝色区域内的点有资格使用垂线段,外面的点直接对到两端点距离取min就好了 要判断深蓝色的两个夹角,( 有一个大于90度$\Longleftrightarrow$不在蓝色区域内 ),判断方法是点积。 - ## 完整模板Code 有效部分约长1.7K. ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define db double using namespace std; const db Pi=acos(-1),eps=1e-8; int sign(db x) {return x<eps ? (-eps<x ? 0 : -1 ) : 1;} struct Point{ db x,y; void print() {printf("(%.3lf,%.3lf)\n",x,y);} Point operator + (const Point &B) const {return (Point){x+B.x,y+B.y};} Point operator - (const Point &B) const {return (Point){x-B.x,y-B.y};} Point operator * (const db &B) const {return (Point){x*B,y*B};} Point operator / (const db &B) const {return (Point){x/B,y/B};} db operator | (const Point &B) const {return x*B.x+y*B.y;} db operator ^ (const Point &B) const {return x*B.y-y*B.x;} }; Point rotate(const Point &A,db len){ db lx=cos(len),ly=sin(len); return (Point){A.x*lx-A.y*ly,A.y*lx+A.x*ly}; } db dis(Point A,Point B) {return sqrt((A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y));} struct Line{ Point a,b; db len(){return dis(a,b);} }; //判断点是否在直线上 //之所以写l0是因为直线有0个端点 bool onl0(Point A,Line L) {return sign((L.a-A)^(L.b-A))==0;} //先判断是否在直线上,然后在判断是否在四至矩形内 //写l2是因为线段有2个端点 bool onl2(Point A,Line L) { return onl0(A,L) &&sign(A.x-min(L.a.x,L.b.x))>=0 &&sign(A.y-min(L.a.y,L.b.y))>=0 &&sign(A.y-max(L.a.y,L.b.y))<=0 &&sign(A.y-max(L.a.y,L.b.y))<=0; } //算出左右面积,然后根据比例决定 Point inter(Line L1,Line L2) { db ls=(L1.b-L1.a)^(L2.a-L1.a), rs=(L1.b-L1.a)^(L2.b-L1.a); return (L2.b-L2.a)*(ls/(ls-rs))+L2.a; } //计算点到直线距离,利用叉积+面积法 db disl0(Point A,Line L) {return abs((L.a-A)^(L.b-A))/L.len();} //计算点到线段距离,利用点积判方向 db disl2(Point A,Line L) { if (sign((A-L.a)|(L.b-L.a))<=0||sign((A-L.b)|(L.a-L.b))<=0) return min(dis(A,L.a),dis(A,L.b)); return disl0(A,L); } void test1() { Point a=(Point){1,2},b=(Point){4,5}; printf("%.3lf ",dis(a,b)); a=rotate(a,Pi/4);b=rotate(b,Pi/2); printf("%.3lf\n",dis(a,b)); //4.243 4.686 puts(""); } void test2() { Point a=(Point){1,2},b=(Point){4,5},c=(Point){3,3},d=(Point){1.5,2.5}; Line l=(Line){(Point){0,1},(Point){3,4}}; printf("%d %d %d %d %d\n",onl2(a,l),onl2(b,l),onl2(c,l),onl0(b,l),onl2(d,l)); //1 0 0 1 1 puts(""); } void test3() { Line l1=(Line){(Point){0,1},(Point){3,4}}; Line l2=(Line){(Point){-2,1},(Point){-1,3}}; inter(l1,l2).print(); //(-4.000,-3.000) l1.b=l1.b*2; inter(l1,l2).print(); //(-4.800,-4.600) puts(""); } void test4() { Point a=(Point){0,2},b={-2,2},c={4,5}; Line l=(Line){(Point){0,1},(Point){3,4}}; printf("%.3lf %.3lf %.3lf\n",disl0(a,l),disl0(b,l),disl0(c,l)); //0.707 2.121 0.000 printf("%.3lf %.3lf %.3lf\n",disl2(a,l),disl2(b,l),disl2(c,l)); //0.707 2.236 1.414 puts(""); } int main() { test1(); test2(); test3(); test4(); return 0; } ``` 默写纸: ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define db double using namespace std; const db Pi=acos(-1),eps=1e-8; int sign(double x) {} struct Point { db x,y; inline void print() {printf("(%.3lf,%.3lf)\n",x,y);} }; Point rotate(const Point &A,double len) { } double dis(Point A,Point B) {} struct Line { }; bool onl0(Point A,Line L) {} bool onl2(Point A,Line L) { } Point inter(Line L1,Line L2) { } double disl0(Point A,Line L) {} double disl2(Point A,Line L) { } void test1() { Point a=(Point){1,2},b=(Point){4,5}; printf("%.3lf ",dis(a,b)); a=rotate(a,Pi/4);b=rotate(b,Pi/2); printf("%.3lf\n",dis(a,b)); //4.243 4.686 puts(""); } void test2() { Point a=(Point){1,2},b=(Point){4,5},c=(Point){3,3},d=(Point){1.5,2.5}; Line l=(Line){(Point){0,1},(Point){3,4}}; printf("%d %d %d %d %d\n",onl2(a,l),onl2(b,l),onl2(c,l),onl0(b,l),onl2(d,l)); //1 0 0 1 1 puts(""); } void test3() { Line l1=(Line){(Point){0,1},(Point){3,4}}; Line l2=(Line){(Point){-2,1},(Point){-1,3}}; inter(l1,l2).print(); //(-4.000,-3.000) l1.b=l1.b*2; inter(l1,l2).print(); //(-4.800,-4.600) puts(""); } void test4() { Point a=(Point){0,2},b={-2,2},c={4,5}; Line l=(Line){(Point){0,1},(Point){3,4}}; printf("%.3lf %.3lf %.3lf\n",disl0(a,l),disl0(b,l),disl0(c,l)); //0.707 2.121 0.000 printf("%.3lf %.3lf %.3lf\n",disl2(a,l),disl2(b,l),disl2(c,l)); //0.707 2.236 1.414 puts(""); } int main() { test1(); test2(); test3(); test4(); return 0; } ``` # 2.简单练习 - [P1153 点和线](https://www.luogu.com.cn/problem/P1153) 看到最多 $10$ 个点,很明显这是一道爆搜题。 具体的思路就是暴力dfs,每加入一个点就判断有没有和前面已经练出来的线段相交。 注意:暴力dfs的话每种合法方案会被统计 $2$ 次(顺逆时针各一次),最后要除掉。 由于常数大,需要预处理所有 $O(n^2)$ 条线段的两两相交情况。 [评测记录](https://www.luogu.com.cn/record/22155996) - [P3744 李彬的几何](https://www.luogu.com.cn/problem/P3744) **题意** :按照顺时针顺序给出一个凸多边形,让你移动它的顶点使得它把它变成一个非凸多边形。 指定一个实数 $D$ ,然后将每个顶点最多移动 $D$ 个单位距离,问 $D$ 最小是多少。 这是一道基础的贪心题,显然要把凸包变凹,等价于制造三点共线,因为共线之后再往里移动一点点就不是个凸包了。 可以证明,选择凸包上相邻的三个点并令其共线是较优的,我们算出一个点到两边点连成直线的距离,然后除以 $2$ (因为三个点可以同时移动)取$\min$即可。 [评测记录](https://www.luogu.com.cn/record/22195192) - [P1354 房间最短路问题](https://www.luogu.com.cn/problem/P1354) 容易证明,最优路径的拐点肯定都是在缺口边缘。 由于数据范围极小,根本不用在意复杂度。 我们把缺口之间(包括起点终点)两两连边,然后跑folyd即可。 问题在于如何判断某两个点之间能否直线到达。 把有缺口的墙拆成 $3n$ 条线段,然后暴力判相交即可。 注意把线段在端点处削短一点,否则可能有在端点处算作相交的情况。 (可以不造四个墙壁[滑稽]) [评测记录](https://www.luogu.com.cn/record/22214359) - [P3217 [HNOI2011]数矩形](https://www.luogu.com.cn/problem/P3217) 题意:找4个点形成**矩形**并且面积最大,问面积。点数$\leq 1500$。 首先,四个点形成矩形的充要条件是 : 对角线相等且互相平分。 那么我们枚举每两个点,产生的线段按照中点和长度分类,可以通过排序实现。 被分到同一类的对角线,任选两条都可以弄成一个矩形,但是如果暴力枚举的话可能会超时。 把所有候选对角线放在一起大概是这个样子: ![](https://cdn.luogu.com.cn/upload/pic/69059.png) 我们发现,对于一条候选对角线,肯定是选择端点尽量远的另一条。 这个东西是单峰的,满足某种决策单调性,所以两个指针一起转这个圈圈就好了。 总复杂度 $O(n^2logn)$ ,瓶颈在排序分类。 [todo] ------------ # 3.算法进阶 - ## 凸包 模板题:[P2742 【模板】二维凸包](https://www.luogu.com.cn/problem/P2742) 这个东西很重要,而且你会在很多貌似和计算几何没有关系的地方见到它。 **定义** : 包住平面上某个点集的**周长最小**的简单多边形,可以证明一定是凸多边形。 **感性理解:橡皮筋套钉子**。 凸包的求解方法: 首先先按照水平序排序 ( $x$ 相同比 $y$ ,否则比 $x$ )。 可以利用这个凸性来构造,维护一个单调栈,从左往右扫,如果遇见一个点和栈顶的两个点成顺时针夹角,那么就弹掉一个点,最后把新来的点加上。 这样就能保证我们求出了一个斜率递增(或者说,逆时针转)的下凸壳。 反过来再扫一遍则得到上凸壳(同样是逆时针转)。 (注意有些题目有重点,必须去掉) [评测记录](https://www.luogu.com.cn/record/22207317) ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define eps 1e-7 using namespace std; int sign(double x) {return x>eps ? 1 : (x<-eps ? -1 : 0);} struct Point { double x,y; Point operator - (const Point &B) const {return (Point){x-B.x,y-B.y};} Point operator * (const double &B) const {return (Point){x*B,y*B};} double operator ^ (const Point &B) const {return x*B.y-y*B.x;} }p[10500]; bool cmp(Point A,Point B) {return sign(A.x-B.x)==0 ? A.y<B.y : A.x<B.x;} double dis(Point A,Point B) {return sqrt((A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y));} bool anticlock(Point A,Point B,Point C) {return sign((A-B)^(C-B))>=0;} //不写等于号可以抛弃共线的点,在这道题里面没有影响 int n,stk[10500],tot; double ans; int main() { scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lf%lf",&p[i].x,&p[i].y); sort(p+1,p+n+1,cmp); stk[1]=1;stk[tot=2]=2; for (int i=3;i<=n;i++){ while(tot>=2&&!anticlock(p[i],p[stk[tot]],p[stk[tot-1]])) tot--; stk[++tot]=i; } for (int i=1;i<tot;i++) ans+=dis(p[stk[i]],p[stk[i+1]]); stk[1]=n;stk[tot=2]=n-1; for (int i=n-2;i;i--){ while(tot>=2&&!anticlock(p[i],p[stk[tot]],p[stk[tot-1]])) tot--; stk[++tot]=i; } for (int i=1;i<tot;i++) ans+=dis(p[stk[i]],p[stk[i+1]]); printf("%.2lf",ans); return 0; } ``` - ## 旋转卡壳 经典问题 : **平面最远点对** 题目 : [P1452 Beauty Contest](https://www.luogu.com.cn/problem/P1452) 们明显,平面最远点对一定都在凸包上,所以先求出凸包。 定义凸包上的**对踵点对** : 用两条平行直线卡着凸包转,着两条直线一定会卡住至少两个点,这两个点称为对踵点对。 ![](https://jvruo.com/usr/uploads/2018/02/2986768821.gif) <https://jvruo.com/usr/uploads/2018/02/2986768821.gif> (图可能会挂,给个链接) 首先证明一个引理(**重要**) : 只用考虑斜率恰好与凸包某条边相同的直线。 容易发现,斜率恰好与凸包某条边相同的直线卡住凸包,必然在某一侧同时卡住两个点。 (为了避免卡住多个点的情况,求凸包时不应保留多个共线的点) 否则必然是一边卡住一个点。 那我们可以把两条直线逆时针转一下,转到卡住多一个点为止,那么原来的两个点仍然在直线上,直线的斜率变化得与凸包某条边相同。 对踵点对的个数为 $O(n)$ ,证明如下: 由上面的引理,我们只用考虑斜率恰好与凸包某条边相同的直线,这样的直线有 $n$ 条。 那么直线在对面只会卡住 $1$ 或 $2$ 个点,产生 $O(1)$ 个对踵点对。 所以对踵点对总量为 $O(n)*O(1)=O(n)$ ,证毕。 容易发现,平面最远点对∈对踵点对,采用反证法容易证明。 那我们求出所有的对踵点对,然后对距离取$\max$即可。 问题在于如何求对踵点对? (以下内容可以结合[DP的决策单调性优化总结 **/** 常用优化手段 **/** 单峰性-单指针跳跃-1D](https://www.luogu.com.cn/blog/command-block/dp-di-jue-ce-dan-diao-xing-you-hua-zong-jie)来理解) 我们逆时针枚举边,然后看看对面有什么被卡住了。 我们发现,由于凸包的凸性,对踵点对一定是单调逆时针变化的。 假设我们已经求出了上一次的对踵点对,我们把边逆时针转动时,另一边的对踵点也会逆时针转动。 而且,由于凸包的凸性,逆时针方向的点到该直线的距离一定是个单峰函数,而且峰顶就是对踵点对。 转动点的时候,我们可以计算这个点到该边的距离,如果小于逆时针方向的下一个点,就跳到下一个点,如果比相邻的两个点都牛逼,那么这就是对踵点。 图解: ![](https://cdn.luogu.com.cn/upload/pic/70336.png) 可能同时卡住对面的两个点,这时,需要特判产生4双对踵点对。 我们考虑一个现象 : 假如把所有的点轻微扰动,可以认为卡住对面的两个点这种情况不存在。 而轻微扰动对大多数东西的影响不大,所以我们在大多数题目里面,可以不考虑卡住对面的两个点这种情况。 比如这道题叫我们求的是最远点对,那么可以不特判。 还要注意凸包只有两个点的情况。 ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define eps 1e-7 #define MaxN 50500 using namespace std; int sign(double x) {return x>eps ? 1 : (x<-eps ? -1 : 0);} struct Point { double x,y; Point operator - (const Point &B) const {return (Point){x-B.x,y-B.y};} Point operator * (const double &B) const {return (Point){x*B,y*B};} double operator ^ (const Point &B) const {return x*B.y-y*B.x;} }p[MaxN],t[MaxN];int tot; bool cmp(Point A,Point B) {return sign(A.x-B.x)==0 ? A.y<B.y : A.x<B.x;} double dis(Point A,Point B) {return (A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y);} bool anticlock(Point A,Point B,Point C) {return sign((A-B)^(C-B))>0;} double tris(Point A,Point B,Point C) {return abs((B-A)^(C-A));} int n,stk[MaxN],top; void hull() { sort(p+1,p+n+1,cmp); stk[1]=1;stk[top=2]=2; for (int i=3;i<=n;i++){ while(top>=2&&!anticlock(p[i],p[stk[top]],p[stk[top-1]])) top--; stk[++top]=i; } for (int i=1;i<=top;i++)t[++tot]=p[stk[i]]; stk[1]=n;stk[top=2]=n-1; for (int i=n-2;i;i--){ while(top>=2&&!anticlock(p[i],p[stk[top]],p[stk[top-1]])) top--; stk[++top]=i; } for (int i=2;i<top;i++)t[++tot]=p[stk[i]]; } double ans; void rothull() { if (tot==2){ ans=dis(t[1],t[2]); return ; }t[0]=t[tot]; int pos=2; for (int i=1;i<=tot;i++){ while(tris(t[pos%tot+1],t[i],t[i-1])>eps+tris(t[pos],t[i],t[i-1])) pos=pos%tot+1; ans=max(ans,dis(t[pos],t[i])); ans=max(ans,dis(t[pos],t[i-1])); } } int main() { scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lf%lf",&p[i].x,&p[i].y); hull(); rothull(); printf("%.0lf",ans); return 0; } ``` [评测记录](https://www.luogu.com.cn/record/22387228) - ## 闵可夫斯基和 **定义** : 两个区域 $A,B$ 的闵可夫斯基和定义为 $\{a+b|a\in A,b\in B\}$. 本文只讨论凸包的闵可夫斯基和。如图: ( $\rm From : WinXP$ ) ![](https://cdn.luogu.com.cn/upload/pic/22393.png?x-oss-process=image/resize,m_lfit,w_500) 不难发现两个凸壳 $A,B$ 的闵可夫斯基和也是凸壳,而且其顶点一定是 $A,B$ 各取一点相加而得。 结论 : 将两个凸包上的边按照极角序顺次连接即可得到答案。(具体见代码) 如图: ( $\rm From : xzyxzy$ ) ![](http://images.cnblogs.com/cnblogs_com/xzyxzy/1374475/o_%E9%97%B5%E5%8F%AF%E5%A4%AB%E6%96%AF%E5%9F%BA%E5%92%8C1.png) 注意可能会出现**三点共线**。 **题目** : [P4557 [JSOI2018]战争](https://www.luogu.com.cn/problem/P4557) **题意** : 给出两个凸包 $A,B$ 和 $q$ 个向量 $\vec x_i$ ,求各个 $A+\vec x_i$ 和 $B$ 有没有交。 令 $a\in A,b\in B$ 则问题变为判定是否存在 $a+x=b$。 移项得到 $x=b-a$ ,又变成判定 $x$ 是否在 $B+(-A)$ 中,求闵可夫斯基和即可。 写法中,分别求上下凸壳,分别判定。 ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define ll long long #define MaxN 100500 using namespace std; struct Point { ll x,y; Point operator + (const Point &B) const {return (Point){x+B.x,y+B.y};} Point operator - (const Point &B) const {return (Point){x-B.x,y-B.y};} Point operator * (const ll &B) const {return (Point){x*B,y*B};} ll operator ^ (const Point &B) const {return x*B.y-y*B.x;} }p1[MaxN],p2[MaxN],t0[MaxN<<1],t1[MaxN<<1]; bool cmpP(const Point &A,const Point &B) {return A.x==B.x ? A.y<B.y : A.x<B.x;} bool cmpK(const Point &A,const Point &B) {return A.y*B.x<B.y*A.x;} bool antic(Point A,Point B,Point C) {return ((A-B)^(C-B))>0;} void hull(Point *p,int n,Point *t0,int &m0,Point *t1,int &m1) { static int stk[MaxN],tot; sort(p+1,p+n+1,cmpP); stk[1]=1;stk[tot=2]=2; for (int i=3;i<=n;i++){ while(tot>=2&&!antic(p[i],p[stk[tot]],p[stk[tot-1]])) tot--; stk[++tot]=i; } for (int i=1;i<tot;i++) t0[++m0]=p[stk[i+1]]-p[stk[i]]; stk[1]=n;stk[tot=2]=n-1; for (int i=n-2;i;i--){ while(tot>=2&&!antic(p[i],p[stk[tot]],p[stk[tot-1]])) tot--; stk[++tot]=i; } for (int i=tot-1;i;i--) t1[++m1]=p[stk[i]]-p[stk[i+1]]; } int n1,n2,m0,m1,q; int main() { scanf("%d%d%d",&n1,&n2,&q); for (int i=1;i<=n1;i++) scanf("%lld%lld",&p1[i].x,&p1[i].y); hull(p1,n1,t0,m0,t1,m1); for (int i=1;i<=n2;i++){ scanf("%lld%lld",&p2[i].x,&p2[i].y); p2[i].x*=-1;p2[i].y*=-1; }hull(p2,n2,t0,m0,t1,m1); sort(t0+1,t0+m0+1,cmpK); t0[0]=p1[1]+p2[1]; for (int i=1;i<=m0;i++) t0[i]=t0[i]+t0[i-1]; int l0=0,r0=m0; while(t0[l0].x==t0[l0+1].x)l0++; while(t0[r0].x==t0[r0-1].x)r0--; sort(t1+1,t1+m1+1,cmpK); reverse(t1+1,t1+m1+1); t1[0]=p1[1]+p2[1]; for (int i=1;i<=m1;i++) t1[i]=t1[i]+t1[i-1]; int l1=0,r1=m1; while(t1[l1].x==t1[l1+1].x)l1++; while(t1[r1].x==t1[r1-1].x)r1--; Point d; for (int i=1;i<=q;i++){ scanf("%lld%lld",&d.x,&d.y); if (d.x<t0[0].x||t0[m0].x<d.x) {puts("0");continue;} int l=l0,r=r0,mid; while(l<r){ mid=(l+r+1)>>1; if (t0[mid].x<=d.x)l=mid; else r=mid-1; }if ((l<r0&&((t0[l+1]-t0[l])^(d-t0[l]))<0) ||l==r0&&d.y<t0[r0].y) {puts("0");continue;} l=l1;r=r1; while(l<r){ mid=(l+r+1)>>1; if (t1[mid].x<=d.x)l=mid; else r=mid-1; }if ((l<r1&&((t1[l+1]-t1[l])^(d-t1[l]))>0) ||l==r1&&d.y>t1[r1].y) {puts("0");continue;} puts("1"); }return 0; } ``` - ## 半平面交 题目 : [P4196 [CQOI2006]凸多边形 /【模板】半平面交](https://www.luogu.com.cn/problem/P4196) 容易转化成下列问题: 给出若干条**有向**直线,每条直线的**右边**算作一个半平面,询问所有半平面的交集面积大小,保证交集有界。 也容易证明交集一定是个凸包,感性理解就是 : 削苹果不停刀一定削不出坑来。 斜率优化有两种写法,一种是基于凸包,另一种就是基于半平面交的,我个人认为后者更易于理解,这也说明两者有很多相似之处。 先讲左凸壳 : 按照幅角排序(可以`atan2`或者手写斜率),然后依次加入,维护一个栈。 假如上一个交点不在新加入的半平面内(叉积判断即可),那么可以把上一条线弹出,如图: ![](https://cdn.luogu.com.cn/upload/image_hosting/aful1nws.png) 这样逆时针转一圈就得到了凸壳,最后要判一下首尾两条线,如图: ![](https://cdn.luogu.com.cn/upload/image_hosting/fsyy4b0d.png) 还是把交点在班平面外的线都弹掉即可。 如果保证答案有界,最后至少剩下三条线,否则无解。 某些题目里需要自行加边界。 ```cpp ``` - ## 平面最近点对分治 [P1429 平面最近点对(加强版)](https://www.luogu.com.cn/problem/P1429) 应用范围好像不广的样子,不过鉴于其思想精妙还是特别讲讲。 首先,我们按照水平序排序。 我们现在有 $n$ 个点,然后根据 $x$ 坐标开始分治(被分割线穿过的点随便丢到任意一边)。 求出了左边和右边的最近点对之后,我们更新目前的全局 $ans$。 这里如果枚举左右点对合并,复杂度肯定假了。 显然,$x$ 坐标离分割线如果距离小于 $ans$ 才有可能更优,我们可以只枚举这些点。 但是左右的这些点集仍然有可能是 $O(n)$ 大小的,直接枚举仍然有可能TLE. 上面我们只限制了 $x$ 坐标,没有限制 $y$ 坐标,导致复杂度退化。 我们把离分割线近的那些点拿出来按照 $y$ 坐标排序,然后限制两个点之间的 $y$ 坐标差不超过 $ans$ ,这样子左边的点一定对应右边的一个区间(注意要一直动态更新 $ans$ ,否则复杂度是假的) 可以证明每个点对应点不超过 $9$ 个: 我们的限制框出了一个 $3ans*3ans$ 的矩形,我们往里面放置直径为 $ans$ 的圆,需要保证圆与圆之间不相交,最多能放置几个。容易想象最多 $9$ 个 (反正是$O(1)$个)。 如果每次对 $y$ 排序使用`std::sort`,复杂度是$O(n\log^2n)$,但是由于数据随机跑得很快。 严谨的方法是按照 $y$ 归并排序,然后合并的时候再取出 $x$ 满足限制的那些点,这样子复杂度就是$O(n\log n)$了。 ```cpp #include<algorithm> #include<cstdio> #include<cmath> #define MaxN 200500 #define eps 1e-6 using namespace std; struct Point {double x,y;}p[MaxN],t[MaxN]; bool cmp(Point A,Point B) {return abs(A.x-B.x)<eps ? A.y<B.y : A.x<B.x;} double dis(Point A,Point B) {return (A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y);} double ans; void solve(int l,int r) { if (l==r)return ; int mid=(l+r)>>1; double ml=p[mid].x; solve(l,mid);solve(mid+1,r); int pl=l,pr=mid+1,tot=l-1; while(tot<r){ if (pl<=mid&&(p[pl].y<p[pr].y||pr>r))t[++tot]=p[pl++]; else t[++tot]=p[pr++]; }for (int i=l;i<=r;i++) p[i]=t[i]; tot=0; for (int i=l;i<=r;i++) if ((p[i].x-ml)*(p[i].x-ml)+eps<ans) t[++tot]=p[i]; for (int i=1;i<=tot;i++) for (int j=i+1;j<=tot;j++) if ((t[j].y-t[i].y)*(t[j].y-t[i].y)+eps<ans) ans=min(ans,dis(t[i],t[j])); else break; } int n; int main() { scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lf%lf",&p[i].x,&p[i].y); sort(p+1,p+n+1,cmp); ans=dis(p[1],p[2]); solve(1,n); printf("%.4lf",sqrt(ans)); return 0; } ``` [评测记录](https://www.luogu.com.cn/record/22222511) # 4.进阶题 - [P2116 城墙](https://www.luogu.com.cn/problem/P2116) 如果 $L=0$ ,那么直接求凸包就好了。 否则,我们可以把点变成半径为 $L$ 的柱子,然后再围起来。 不难发现,有弧度的部分合起来是一个半径为 $L$ 的正圆。 那么剩下的直线部分,平移之后能够拼成凸包,答案就是凸包周长$+2\pi L$。 [评测记录](https://www.luogu.com.cn/record/22210824) - [P3829 [SHOI2012]信用卡凸包](https://www.luogu.com.cn/problem/P3829) 和上一题类似,也是求出凸包周长再加上$2\pi r$,注意要去重($ch\acute ong$)点。 [评测记录](https://www.luogu.com.cn/record/22224474) - SMOJ2919 神仙题.jpg 题意:给你平面上n个点,点有点权(可正可负),要求你选择三个点形成**劣角**。 然后将会得到劣角内部所有点点权和的收益,注意,选择的三个点不计入收益。 最大化这个东西。$n\leq2000$ 枚举这个角的顶点,然后移动坐标系再**极角排序**,这样子能选择的东西就变成序列了,使用单队维护一下便可。 - [P4048 [JSOI2010]冷冻波](https://www.luogu.com.cn/problem/P4048) 这是一道结合了网络流与计算几何的题目。 首先建立网络流模型 : 二分时间限制,巫妖连上汇点,容量为 : 攻击次数=(时间/冷却)+1 精灵连上能够攻击他的巫妖,容量为 $1$ ,源点连精灵,容量为 $1$ 。 如果满流则方案可行。 剩下的问题就是判断某个巫妖能否攻击某个精灵。 场地里面有唯一的障碍物 : 树木。可以抽象成圆形。 我们判断一下巫妖和精灵连成的线段后没有和某个圆相交就好了(注意距离还要小于R)。 直接求线段到圆的距离,如果小于等于半径则认为有交。 没有实体在树里面,所以求端点距离可以省了。 复杂度$O(n^3+\text{网络流}log?)$ [评测记录](https://www.luogu.com.cn/record/22210824) - [P4166 [SCOI2007]最大土地面积](https://www.luogu.com.cn/problem/P4166) 题意:给出 $n$ 个点,选出四个使得面积最大。 容易证明,这四个点一定都在凸包上。 (如果凸包上只有三个点,那么直接枚举第四个点即可,正确性显然) 然后我们固定一点, $O(n)$ 枚举一条对角线(对点) 剩下的两个点就在两侧,他们对面积的贡献是与对角线连成的三角形的面积,由于该三角形底一定,那么要最大化三角形的高。 我们发现,随着对角线的转动,这两个点肯定不会往回转,因为往回转会使得高变小。 此外,高在逆时针方向上还是个单峰函数。 那么四个点一起转圈圈就好了,复杂度 $O(n^2)$。 还有基于对踵点的 $O(n\log n)$ 做法: 注意到最优解中对角线一定是一对对踵点,所以旋转卡壳先求出所有候选对角线。 然后让候选对角线转逆时针圈圈,另外的两个点类似旋转卡壳一样转,即可线性扫描完毕。 [评测记录](https://www.luogu.com.cn/record/22388367) ($O(n\log n)$) - [P3187 [HNOI2007]最小矩形覆盖](https://www.luogu.com.cn/problem/P3187) 能够感知,最优解一定有一条边与凸包上的一条边重合。 我们就依次枚举这条边,然后在其余三个方向上求出最远点,这也是具有单调性的,可以四个点一起转。 复杂度 $O(n\log n)$。 [**Todo**] - [P3680 [CERC2016]凸轮廓线 Convex Contour](https://www.luogu.com.cn/problem/P3680) 毒瘤凸包题,不想多说。 [评测记录](https://www.luogu.com.cn/record/22392904)