算法学习笔记(26): 计算几何

算法,学习,笔记,计算,几何 · 浏览次数 : 27

小编点评

```c++ int Minkowski(Point *pa, Point *pb, int n, int m) { static Point ta[N], tb[N]; for (int i = 0; i < n; ++i) ta[i] = pa[(i + 1) % n] - pa[i]; for (int i = 0; i < m; ++i) tb[i] = pb[(i + 1) % m] - pb[i]; int idx = 0, i = 0, j = 0; while (i < n || j < m) { if (j == m || (i < n && (ta[i] ^ tb[j]) > 0)) idx++; else idx--; if (j == m) j = 0; if (i < n) i++; else i--; } return idx; } ```

正文

计算几何

向量

高一知识,略讲。

向量外积

\(\vec x = (x_1, y_1), \vec y = (x_2, y_2)\),则有 \(\vec x \times \vec y = x_1 y_2 - y_1 x_2\)

或者表示为 \(|\vec x||\vec y| \sin \theta\),其中 \(\theta\) 表示向量间的夹角。

几何意义:两个向量构成的平行四边形的面积(可以为负数)。

性质

  • \(\vec x \times \vec y \gt 0\),则 \(\vec y\)\(\vec x\) 的逆时针方向,反之亦然。

向量旋转

将一个向量顺时针旋转 \(\alpha\),可以利用矩阵的性质。

对于向量 \(\vec x = (x, y)^T\),旋转公式为:

\[(x, y) \begin{pmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \end{pmatrix} \]

逆时针旋转的矩阵为其转置矩阵:

\[(x, y) \begin{pmatrix} \cos \alpha & \sin \alpha \\ -\sin \alpha & \cos \alpha \end{pmatrix} \]

矩阵可以不好理解,可以通过极坐标推导。这里不展开。

向量的极角

定义为向量与 \(x\) 轴正半轴的夹角,一般用 \(atan2(y, x)\) 实现。

\(atan(y, x)\) 的取值范围为 \([-\pi, \pi]\)

利用一个向量 \(\vec x\) 表示其坐标。

这个向量等价于原点到目标点的向量。

线

可以用多种方法表示,视情况而定:

  • 一般式表达:\(Ax+By + C = 0\)。方便表示一条直线,或者无向的线段。

  • 两个点表达:\((x_1, y_1) \to (x_2, y_2)\),可以方便表示有向的线段或者射线。

  • 点与向量:\((x,y) + k \vec d\),可以方便的表示射线。

补充

两个点求解一般式,有 \(A = y_2 - y_1, B = x_1 - x_2, C = x_2y_1 - x_1 y_2\)

判断两个一般式直线平行,用:\(A_1B_2 = A_2B_1\)。本质是求斜率。

求一般式的交点,将式子变为:

\[A_1 A_2 x + B_1 A_2 y + C_1 A_2 = 0 \\ A_2 A_1 x + B_2 A_1 y + C_2 A_1 = 0 \\ \]

相减可得:

\[y = (C_1 A_2 - C_2 A_1) / (A_1 B_2 - A_2 B_1) \]

同理可得:

\[x = (C_2 B_1 - C_1 B_2) / (A_1 B_2 - A_2 B_1) \]

需要保证两条直线不平行!

线线交点

首先排除平行与重合的情况,判断是否相交,利用向量外积即可。

判断有无交点:若 \(\vec {AC} \otimes \vec {AD}\)\(\vec{AC} \otimes \vec {AB}\) 异号,以及 \(\vec {BD} \otimes \vec {BA}\)\(\vec {BD} \otimes {BC}\) 异号,则有交点。

然后可以利用面积法求交点。

用向量外积求出 \(S_{ABCD}\),以及 \(S_{ABD}\)

那么 \(AO : AC = S_{ABD} : S_{ABCD}\),然后就可以找出 \(O\) 的坐标了。


点在线上

线上去两点 \(S, T\),对于点 \(P\),若 \(P\) 在线段 \(ST\) 上,则 \(\vec{SP} \otimes \vec {PT} = 0\)

反之不一定成立,需要再判断坐标范围。

点到线的距离

也就是垂线长度。在直线上任取两个点,利用向量外积求出所构成的三角形的面积,除以底边长度,进而求出垂线长度。

double height(const Point &dot, const Point &st, const Point &ed) {
    const Point vec = ed - st;
    return (vec ^ (dot - st)) / vec.abs();
}

代码中 vec.abs() 表示向量的模长,也就是底边的长度。

点到线的垂线段

还是找到两个点。

利用这两个点,找到在直线方向上的单位向量,旋转 \(\frac \pi 2\),乘上垂线长,与原坐标相加即可。

Point foot(const Point &dot, const Point &st, const Point &ed) {
    const Point vec = ed - st;
    double h = (vec ^ (dot - st)) / vec.abs();
    return dot + (Point){vec.y, -vec.x} / vec.abs() * h;
}

点关于直线对称

类似于找垂足的方法。只是再加上的向量的基础上 \(\times 2\)

Point flip(const Point &dot, const Point &st, const Point &ed) {
    const Point vec = ed - st;
    double h = (vec ^ (dot - st)) / vec.abs();
    return dot + (Point){vec.y, -vec.x} / vec.abs() * h * 2;
}

可能需要注意方向!


多边形

一般按照顺时针或者逆时针的方向一一列出所有的顶点。

判断顺时针或者逆时针

钦定一个起点,编号为 \(1\)

枚举 \(i\) 利用 \(\vec{1i}\)\(\vec{1(i+1)}\) 的叉乘,可以算出整个多边形的面积(的两倍)。

但是考虑到叉乘的正负性,如果结果为正,则所给的顺序为逆时针(因为 \(\vec{1i}\)\(\vec{1(i+1)}\) 的顺时针方向)。

判断凸多边形

判断折线段拐向是否相同:

对于折线 \(A \to B \to C\),若 \((B - A) \otimes (C - B) \lt 0\),则为顺时针,反之为逆时针。

判断点在多边形内

有很多方法,这里说两种常用的。

  • 通用射线法:从该点做一条射线,如果该射线与多边形的交点数为奇数,则在内部,反之在外部。

  • 凸多边形二分法:注意的是,基准点需要作为原点!因为凸多边形可以被划分为若干个不相交的三角形。那么我们只需要二分其右边最左边的边界即可。注意的是需要特判一下在下面的情况:

    // cvx 指的是凸包,c 表示凸包的大小,vec 表示所求的点的坐标
    int checkIn(const Point *cvx, int c, const Point &vec) {
        // 特判
        if ((vec ^ cvx[0]) > 0 || (cvx[c - 1] ^ vec) > 0) return 0;
    
        // + 二分
        int lt = 0;
        for (int w = 1 << (int)log2(c); w; w >>= 1) {
            if (lt + w < c && (cvx[lt + w] ^ vec) > 0)
                lt += w;
        }
    
        // 判断是否在三角形
        return ((cvx[lt + 1] - cvx[lt]) ^ (vec - cvx[lt])) >= 0;
    }
    

凸包算法

Graham 算法

首先极角排序,然后单调栈做一遍,好写不易错!

不过需要注意的是,在极角相同的情况下,要按照距离 原点 排序。

例题:

记录

Andrew 算法

没意思,不好用,虽然蛮快的,但是写着很容易错


旋转卡壳

题面[USACO03FALL] Beauty Contest G /【模板】旋转卡壳 - 洛谷

经典凸包上应用。用于求各种神秘的最远东西。

偷一张图……QwQ

这就是旋转卡壳的流程。

显然是双指针。但是不一样。这里枚举的不是每一个点,而是每一条边。

为什么?考虑对于点来说,其最远的点不一定是单调的。考虑这样一个图形:

成功的卡掉了点点的双指针。

然而,对于一条线来说,凸包上的点到他的距离(垂线长)是单调的,距离也随之改变

而距离的改变意味着面积的变化,得出可以利用向量外积!

所以这里的双指针是一个线段,一个点。

lint ans = 0;
for (int l = 0, r = 0; l < csiz; ++l) {
    ans = max(ans, dis(convex[l], convex[l + 1]));
    while (cross(convex[l], convex[l + 1], convex[r])
        < cross(convex[l], convex[l + 1], convex[r + 1])) {
        ++r;
    }

    ans = max(dis(convex[r], convex[l]), ans);
    ans = max(dis(convex[r], convex[l + 1]), ans);
}

例题[HNOI2007] 最小矩形覆盖 - 洛谷

旋转卡壳的高级用法!很适合练手。

这里不仅仅需要求一个方向的最远,而是三个方向。

所以是维护三个指针……QwQ

double ans = 1e18;
int l = 0, m = 1, r = 1, mi;
for (int i = 0; i < n; ++i) {
	// 维护最上端点
	while (height(A[m + 1], A[i], A[i + 1]) > height(A[m], A[i], A[i + 1]) + eps)
		++m;
	double h = height(A[m], A[i], A[i + 1]);

	const Point vec = A[i + 1] - A[i];
	Point ed = A[i] + (Point){-vec.y, vec.x};
	while (height(A[l + 1], A[i], ed) > height(A[l], A[i], ed) + eps)
		++l;

    // 保证第一次是往左边跑,其他时候这个东西没有影响
	while (height(A[(l - 1 + n) % n], A[i], ed) > height(A[l], A[i], ed) + eps)
		l = (l - 1 + n) % n;

	double w = height(A[l], A[i], ed);

	ed = A[i] + (Point){vec.y, -vec.x};
	while (height(A[r + 1], A[i], ed) > height(A[r], A[i], ed) + eps)
		++r;

	w += height(A[r], A[i], ed);

	rct[i] = {i, l, m, r};
	if (w * h + eps < ans) {
		ans = w * h;
		mi = i;
	}	
}

闵可夫斯基和

对于两个凸包 \(A, B\),其闵可夫斯基和定义为 \(\{\vec v| \vec v = \vec a + \vec b , \vec a \in A, \vec b \in B\}\)

如果画一下,可以发现,两个凸包的闵可夫斯基和就是凸包的所有边极角排序一遍。

虽然可以 \(O(n \log n)\) 完成,但是显然不够优秀。

考虑求出的两个凸包,一定是按照极角已经有序了的。

所以可以借鉴归并排序的思路搞一遍。

int Minkowski(Point *pa, Point *pb, int n, int m) {
	static Point ta[N], tb[N];
	for (int i = 0; i < n; ++i)
		ta[i] = pa[(i + 1) % n] - pa[i];
	for (int i = 0; i < m; ++i)
		tb[i] = pb[(i + 1) % m] - pb[i];
 
	int idx = 0, i = 0, j = 0;
	minkow[idx++] = pa[0] + pb[0];
	while (i < n || j < m) {
		if (j == m || (i < n && (ta[i] ^ tb[j]) >= 0))
			minkow[idx++] = ta[i++];
		else
			minkow[idx++] = tb[j++];
	}

	for (int i = 1; i < idx; ++i)
		minkow[i] = minkow[i] + minkow[i - 1];
	return idx;
}

注意一下,这里的求法有一些前提:

  • 两个凸包起始点的斜率范围相同!

  • unkown error


例题[JSOI2018] 战争 - 洛谷

很好的综合题。

需要使用 Minkowski,以及神秘的二分判断点在凸多边形内。

首先转换模型。

对于题目的要求是判断命题:$\exists \vec b \in A, \vec b + \vec w \in A $。或者表示为:

\[\exists \vec a \in A, \vec b \in B, \vec b + \vec w = \vec a \]

转化来说,成为判断:

\[\vec w \in \{\vec v | \vec v = \vec a - \vec b, \vec a \in A, \vec b \in B \} \]

不难发现,后者其实就是一个 Minkowski

由于闵可夫斯基和还是一个凸包,所以,可以满足二分法判断是否在凸包内。

然后,就搞定了!

与算法学习笔记(26): 计算几何相似的内容:

算法学习笔记(26): 计算几何

# 计算几何 ## 向量 > 高一知识,略讲。 #### 向量外积 若 $\vec x = (x_1, y_1), \vec y = (x_2, y_2)$,则有 $\vec x \times \vec y = x_1 y_2 - y_1 x_2$。 或者表示为 $|\vec x||\vec y|

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

算法学习笔记(4): 并查集及其优化

# 并查集 并查集,Disjoint-Set,或者通俗一点,叫做MergeFind-Set,是一种可以动态维护若干个不重叠的集合,并支持集合之间的合并与查询的数据结构。 集体来说,并查集支持下列两个操作: - Find,查询元素所属集合 - Merge,将两个元素所属集合合并 一般来说,为了具体实现