『模块学习』二维计算几何基础
引言
u1s1,开这个专题未免有点太唐了……不过就像gj所说,现在学到的东西,到了大学大多能用上,那我为什么不竭尽全力学呢?
至于将“算法小记”改为“模块学习”,主要还是考虑到标题的泛用性不足,毕竟以后可能会写一些trick集合或总结(?
引入
众所周知,计算机能很好地解决代数上与图论上的问题。为了让计算机能处理几何的问题,将解析几何与线性代数的知识与计算机结合,计算几何这一学科便出现了。
计算几何在OI中考到的概率几乎没有,只是有个钟情于此的SCOI而在ACM赛场上,计算几何则是十分热门的考点,几乎每一场都会有。
本文主要是二维计算几何的基础内容,以及一些不能称之为算法、体系不那么完备的零散知识点。默认读者具有高中数学基础。
向量(重点)
向量在各种类型的问题上都具有立竿见影的效果,几乎是道计算几何题,就能题目解析中看到向量两字。同时向量也是线性代数中一个非常重要的部分,因此极为重要。
基本定义
向量
向量空间(vector space) 是形如
数学与OI中提到的向量一般为 自由向量(free vector) ,其起点可任意平移,记作
向量相关
- 有向线段(directed line segment): 带有方向的线段。有向线段有三要素:起点、方向、长度。这三个要素唯一确定终点,作图时一般用有向线段表示向量。
- 模(modulus): 有向线段
的长度,即向量的 大小(magnitude) ,在数值上为 2-范数(2-norm)/欧几里得范数(Euclid norm) ,等于向量各个元素的平方和开二次根。记为 或 。 - 零向量(zero vector): 模为零的向量。其方向不是固定的,记为
或 。 - 单位向量(unit vector): 模为
的向量称为该方向上的单位向量。一般即为 或 。 - 平行向量(parallel vectors): 方向相同或相反的两个非零向量。对于多个互相平行的向量,任意一组平行向量都可以平移到同一直线上,所以也称 共线向量(collinear vectors) 。记作
。 - 相等向量(equal vectors): 模相等且方向相同的向量。
- 相反向量(negative vectors): 模相等且方向相反的向量。
- 向量角(vetorial angle): 对于两个非零向量
作 ,那么这两个向量的向量角就为 。记作 。当 两向量同向, 两向量反向, 两向量 正交(orthogonal) ,并且规定 。
基本运算
加减法
运算与物理中力的分解与合成相同,不在赘述。
具有的运算定律有交换律与结合律。
数乘
规定实数
数乘的一个作用是判定两向量是否共线:两个非零向量
点积
点积
点积在计算几何中主要作用是判断方向。点积的几何意义是
叉乘(重点)
叉乘
通过叉乘得出的向量同时与
然后变换形式得到叉乘矩阵:
其中
得到叉乘矩阵的变换方式后,我们令
最后得到的向量的第三维就是二维叉积。
值得注意的是,这个值也同时是
应用
判断点相对直线的位置
在计算几何中,叉积能处理非常多的问题。举个简单的例子:给定一个直线
例题: [COCI2019-2020#2] Zvijezda
给定一个有个点的凸多边形,保证 为偶数,对每组对边(两边之间含有 条边的两条边)所夹的含有凸多边形形的部分染色。然后询问 次,每次询问一个点 是否在染色区。询问强制在线。
。
对于这道题,由于保证了给定的一定是凸多边形,因此边按顺时针顺序斜率递减。将边编号为
而判定这个点相对于直线的位置,使用我们上面提到的叉乘方法即可。
程序段:
点击查看代码
__int128 ccw(NODE n1, NODE n2, NODE n3) {return (__int128)(n2.x-n1.x)*(n3.y-n2.y)-(__int128)(n3.x-n2.x)*(n2.y-n1.y);}//向量叉积判顺逆
bool get(int x) {return ccw(nod[x], nod[(x+1)%n], que)>=0;}//判定是否在边x的染色区
int bins(int ql, int qr) {//二分
int L = ql, R = qr, mid;
while (L != R) {
mid = (L+R+1)>>1;
if (get(mid) == get(L)) L = mid;
else R = mid-1;
}
int ret = L-ql+1;
if (!get(ql)) ret = n/2-ret;
return ret;
}
bool check() {//处理询问
int mid = n/2, a = get(0), b = get(mid);
if (a == b) return a;
return bins(0, mid-1)+bins(mid, n-1) > mid;
}
signed main() {
int opt = read();
n = read();
for (int i = 1; i <= n; ++i) {
int x = read(), y = read();
nod.eb((NODE){x,y});
}
int q = read(), cnt = 0;
while (q--) {
int x = (read()^(opt*cnt*cnt*cnt)), y = (read()^(opt*cnt*cnt*cnt));
que = (NODE){x, y};
if (check()) ++cnt, printf("DA\n");
else printf("NE\n");
}
return 0;
}
判断两条线段是否相交
有了上面算法的基础,我们再来分析判定两条线段是否相交的问题。假设要判断的线段为
所谓 快速排斥实验 ,就是将两条线段在平面直角坐标系上扩充成矩形,然后判定这两个矩形是否相交,像这样:
然后变为:
两个矩形并不相交,由此我们判定这两条线段通过了快速排斥实验。当两条线段同时通过了跨立实验与快速排斥实验,我们就判定这两条线段不相交。
平面极坐标系
我们在初中曾学过类似“在
坐标系的建立
- 在平面上选一定点
,称为 极点(polar point) ; - 自极点引出一条射线
,称为 极轴(polar axis) ; - 选定单位长度与角度的单位以及正方向( 顺时针(clockwise) 或 逆时针(counterclockwise) )
位置的描述
设我们要描述的点为
- 极点
与 之间的距离 称为 极径(polar radius) ,记为 。 - 以极轴为始边,
为终边的角 称为 极角(polar radius) ,记为 。
那么有序数对
应用
极坐标系的应用并不是独立的,它更类似于一个脚手架,方便其他工具更好地发挥。对点按极角排序后有许多优秀的性质,这样算法的其他步骤就可以更好地解决问题。比较重要的算法有平面凸包、半平面交。下面的例题就是极坐标系与叉乘的结合。
例题: JOISC2014 K 二人の星座
平面内有 个点,有三种颜色,每个点的颜色是三种中的一种。求不相交的三色三角形对数。
注意到两个三角形不相交,当且仅当存在一条直线使得两个三角形分别在直线的两侧。这样的直线可能有非常多,因此我们只考虑两个三角形的内公切线。朴素地运用这一点,我们可以得到一个
这样的复杂度还不够优秀,因此我们考虑优化算法:我们先确定一个点为极点,与
代码段:
点击查看代码
int n;
int sum[2][3], sgn[N];
struct BLCK{int x, y, c; LL px, py;}nod[N], a[N];//点的原坐标、颜色与极坐标
bool cmp(const BLCK &x, const BLCK &y) {//叉乘比较极角并排序
return x.px*y.py-x.py*y.px == 0 ? x.x < y.x : (x.px*y.py-x.py*y.px > 0)^(y.px < 0)^(x.px < 0);
}
LL ans;
void solve(int x) {//统计x为极点的贡献
memset(sum, 0, sizeof(sum));
int cntn = 0;
BLCK O = nod[x];
for (int i = 1; i <= n; ++i) if (i != x)
a[++cntn].px = nod[i].x-O.x, a[cntn].py = nod[i].y-O.y, a[cntn].c = nod[i].c;
sort(a+1, a+cntn+1, cmp);
for (int i = 1; i <= cntn; ++i) sgn[i] = (a[i].px == 0) ? 1 : abs(a[i].px)/a[i].px, ++sum[(sgn[i]+1)/2][a[i].c];
for (int i = 1; i <= cntn; ++i) {
--sum[(sgn[i]+1)/2][a[i].c];
LL tmp1 = 1, tmp2 = 1;
for (int j = 0; j <= 1; ++j)
for (int c = 0; c <= 2; ++c) {
if ((c != O.c && !j) || (c != a[i].c && j)) tmp1 *= sum[j][c];
if ((c != O.c && j) || (c != a[i].c && !j)) tmp2 *= sum[j][c];
}
ans += tmp1+tmp2;
sgn[i] *= -1;
++sum[(sgn[i]+1)/2][a[i].c];
}
}
int main() {
n = read();
for (int i = 1; i <= n; ++i)
nod[i] = (BLCK){read(), read(), read(), 0, 0};
for (int i = 1; i <= n; ++i) solve(i);//枚举极点
printf("%lld\n", ans/4);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话