TonyYin's Blog

Back

向量#

平面向量#

向量: 既有大小又有方向的量称为向量,记作 a\vec {a}a\boldsymbol a

有向线段: 带方向的线段。用有向线段来直观地表示向量。起点为 AA 终点为 BB 的有向线段表示的向量,用符号简记为 AB\overrightarrow{AB}.

**向量的模:**向量的大小(或长度),用 AB|\overrightarrow{AB}|a|\boldsymbol{a}| 表示。

零向量:模为 00 的向量。零向量的方向任意。记为:0\vec 00\boldsymbol{0}

单位向量:模为 11 的向量称为该方向上的单位向量。

平行向量:方向相同或相反的两个 非零 向量。规定 0\boldsymbol 0 与任意向量平行。a\boldsymbol{a}b\boldsymbol{b} 平行,记作:ab\boldsymbol{a}\parallel \boldsymbol{b}

**共线向量:**与平行向量的定义相同。任一组平行向量都可以平移到同一直线上。

向量的夹角:已知两个非零向量 a,b\boldsymbol a,\boldsymbol b,作 OA=a,OB=b\overrightarrow{OA}=\boldsymbol a,\overrightarrow{OB}=\boldsymbol b,那么 θ=AOB\theta=\angle AOB 就是向量 a\boldsymbol a 与向量 b\boldsymbol b 的夹角。记作:a,b\langle \boldsymbol a,\boldsymbol b\rangle。当 θ=π2\theta=\frac{\pi}{2} 时,称这两个向量垂直,记作 ab\boldsymbol a\perp \boldsymbol b。规定 θ[0,π]\theta \in [0,\pi]

向量的线性运算#

向量的加法#

向量加法的三角形法则#

对于平面上的任意两个向量 a\boldsymbol{a}b\boldsymbol{b},在平面内任取一点 AA,作 AB=a\overrightarrow{AB}=\boldsymbol{a}BC=b\overrightarrow{BC}=\boldsymbol{b},作向量 AC\overrightarrow{AC}.

称向量 AC\overrightarrow{AC} 为向量 a\boldsymbol{a}b\boldsymbol{b}和向量AB+BC=AC.\overrightarrow{AB}+\overrightarrow{BC}=\overrightarrow{AC}.

如图1,把向量首尾顺次相连,向量的和为第一个向量的起点指向最后一个向量的终点;

向量加法的平行四边形法则#

若要求和的两个向量 共起点,那么它们的和向量为以这两个向量为邻边的平行四边形的对角线,起点为两个向量共有的起点,方向沿平行四边形对角线方向。

向量的减法#

减法可以写成加上相反数的形式,即:ab=a+(b)a-b=a+(-b),如图1,b=cab=c-aa=cba=c-b.

向量的数乘#

给定一个实数 λ\lambda 和一个向量 a\boldsymbol a,规定其乘积为一个向量,记作 λa\lambda \boldsymbol a,其模与方向定义如下:

  1. λa=λa|\lambda \boldsymbol a|=|\lambda| |\boldsymbol a|
  2. λ>0\lambda >0 时,λa\lambda\boldsymbol aa\boldsymbol a 同向,当 λ=0\lambda =0 时,λa=0\lambda \boldsymbol a=\boldsymbol 0,当 λ<0\lambda<0 时,λa\lambda \boldsymbol aa\boldsymbol a 方向相反。

这种运算是向量的数乘运算。

坐标表示#

a+b=(ax+bx,  ay+by)ab=(axbx,  ayby)λa=(λax,  λay)\boldsymbol a + \boldsymbol b=(\boldsymbol a_x+\boldsymbol b_x,\;\boldsymbol a_y+\boldsymbol b_y)\\ \boldsymbol a - \boldsymbol b=(\boldsymbol a_x-\boldsymbol b_x,\;\boldsymbol a_y-\boldsymbol b_y)\\ \lambda \boldsymbol a=(\lambda\boldsymbol a_x,\;\lambda\boldsymbol a_y)

向量的数量积#

已知两个向量 a,b\boldsymbol a,\boldsymbol b,它们的夹角为 θ\theta,那么:

ab=abcosθ=axbx+ayby\boldsymbol a \cdot \boldsymbol b=|\boldsymbol a||\boldsymbol b|\cos \theta=\boldsymbol a_x\boldsymbol b_x+\boldsymbol a_y\boldsymbol b_y

就是这两个向量的 数量积,也叫 点积内积。其中称 acosθ|\boldsymbol a|\cos \thetaa\boldsymbol ab\boldsymbol b 方向上的投影。数量积的几何意义即为:数量积 ab\boldsymbol a \cdot \boldsymbol b 等于 a\boldsymbol a 的模与 b\boldsymbol ba\boldsymbol a 方向上的投影的乘积。

这种运算得到的结果是一个实数,为标量。

可以方便的计算出 cosθ\cos \theta,于是有如下应用:

  1. ab\boldsymbol a \perp \boldsymbol b     \iff ab=0\boldsymbol a\cdot \boldsymbol b=0

  2. a=λb\boldsymbol a = \lambda \boldsymbol b     \iff ab=ab|\boldsymbol a\cdot \boldsymbol b|=|\boldsymbol a||\boldsymbol b|

  3. a=m2+n2|\boldsymbol a|=\sqrt {m^2+n^2}

  4. cosθ=abab\cos \theta=\cfrac{\boldsymbol a\cdot\boldsymbol b}{|\boldsymbol a||\boldsymbol b|}

向量的向量积#

给定两个向量 a,b\boldsymbol a,\boldsymbol b,规定其向量积为一个向量,记作 a×b\boldsymbol a\times \boldsymbol b,其模与方向定义如下:

  1. a×b=absina,b|\boldsymbol a\times \boldsymbol b|=|\boldsymbol a||\boldsymbol b|\sin \langle \boldsymbol a,\boldsymbol b\rangle

  2. a×b\boldsymbol a\times \boldsymbol ba,b\boldsymbol a,\boldsymbol b 都垂直,且 a,b,a×b\boldsymbol a,\boldsymbol b,\boldsymbol a\times \boldsymbol b 符合右手法则。

向量积也叫外积,其几何意义是:a×b|\boldsymbol a\times \boldsymbol b| 是以 a,b\boldsymbol a,\boldsymbol b 为邻边的平行四边形的面积

向量积与 a,b\boldsymbol a,\boldsymbol b 所在平面垂直,其竖坐标为 axbyaybx\boldsymbol a_x\boldsymbol b_y-\boldsymbol a_y\boldsymbol b_x.

我们根据右手法则可以推断出 b\boldsymbol b 相对于 a\boldsymbol a 的方向,逆时针方向竖坐标为正值,反之为负值

坐标旋转公式#

若将向量 a=(x,  y)\boldsymbol a=(x,\;y) 逆时针旋转 α\alpha,得到向量 b\boldsymbol b,则有:

b=(xcosαysinα,  ycosα+xsinα)\boldsymbol b=(x\cos \alpha-y\sin \alpha,\;y\cos \alpha+x\sin\alpha)

可以通过三角恒等变换证明。

参考代码#

const double pi = 3.1415926535898;
const double eps = 1e-8;
inline int sgn(double x) {
	if(fabs(x) < eps) return 0;
	else return x > 0 ? 1 : -1;
}
inline int fcmp(double x, double y) {
	if(fabs(x - y) < eps) return 0;
	else return x > y ? 1 : -1;
}
struct Point{
	double x, y;
	Point(){};
	Point(double a, double b): x(a), y(b) {}
	Point(Point a, Point b): x(b.x - a.x), y(b.y - a.y) {}
	friend Point operator + (const Point &a, const Point &b) {
		return Point(a.x + b.x, a.y + b.y);
	}
	friend Point operator - (const Point &a, const Point &b) {
		return Point(a.x - b.x, a.y - b.y);
	}
	friend bool operator == (const Point &a, const Point &b) {
		return fcmp(a.x, b.x) == 0 && fcmp(a.y, b.y == 0);
	}
	friend Point operator * (const double &a, const Point &b) {
		return Point(a * b.x, a * b.y);
	}
	friend Point operator * (const Point &a, const double &b) {
		return Point(a.x * b, a.y * b);
	}
	friend double operator * (const Point &a, const Point &b) {
		return a.x * b.y - a.y * b.x;
	}
	friend double operator & (const Point &a, const Point &b) {
		return a.x * b.x + a.y * b.y;
	}
	friend Point operator / (const Point &a, const double &b) {
		return Point(a.x / b, a.y / b);
	}
	friend bool operator < (const Point &a, const Point &b) {
		return (a.x < b.x) || (a.x == b.x && a.y < b.y);
	}
	inline double len() {
		return sqrt(1.0 * x * x + 1.0 * y * y);
	}
	friend double area(Point &a, Point &b, Point &c) {
		return (b - a) * (c - a) / 2.0;
	}
};
typedef Point Vec;
inline double dis(Point &a, Point &b) {
	return (a - b).len();
}
inline double angle(Point &a, Point &b) {
	return acos((a & b) / a.len() / b.len());
}
inline Vec rotate(Vec &a, double k) {
	return Vec(a.x * cos(k) - a.y * sin(k), a.x * sin(k) - a.y * cos(k));
}
cpp

平面凸包#

相关概念#

**凸多边形:**所有内角大小都在 [0,π][0,\pi] 范围内的 简单多边形

**平面凸包:**平面上的一个子集 SS 被称为凸的,当且仅当对于任意两点 p,qSp, q\in S,线段 pq\overline{pq} 都完全属于 SS.集合 SS 的凸包 CH(S)\mathcal{CH}(S),是包含 SS 的最小凸集,也就是包含 SS 的所有凸集的交。

如上图,凸包还可以理解为平面上有若干柱子,用橡皮筋套住所有柱子,绷紧后形成的多边形即为凸包。

所以有更友好的定义(不一定准确)。

**凸包:**在平面上能包含所有给定点的最小凸多边形叫做凸包。

暴力求解#

用二维坐标 (xi,yi)(x_i, y_i) 的形式给定点集 PP,考虑如何暴力求解。

注意到,若线段 pq\overline{pq} 在凸包上,则 PP 中的点均位于直线 pqpq 的同一侧。若我们钦定 pqp\rightarrow q 按顺时针方向,则有更强的限制,需要 PP 中的点都在直线的右侧。

于是可以枚举有序点对 (p,q)P×P(p, q)\in P\times P,若 PP 中的点都在有向线段 pq\overline{pq} 的右侧,则 pq\overline{pq}CH(P)\mathcal{CH}(P) 中的一条边。

需要用到向量的叉积,点 ttpq\overrightarrow {pq} 右侧 \Longleftrightarrow pt×pq>0\overrightarrow{pt}\times \overrightarrow{pq}>0.

这样的复杂度是 O(n3)\mathcal{O}(n^3) 的,有很多可以优化的地方。

Andrew算法#

Andrew 算法是一种递增式算法,流程如下。

递增式算法 (incremental algorithm),在计算几何中常见。算法思想:逐一加入 PP 中的点,每增加一个点,都更新一次目前的解,加入最后一个点后,即可得到答案。

首先把所有点 排序 ,以横坐标为第一关键字,纵坐标为第二关键字。

排序后,第一个点和末尾的点,一定在凸包上,容易通过反证法证明。

从左往右看,上下凸壳斜率的 单调性相反,即所旋转的方向不同,所以要分开求。

我们 升序枚举 求出下凸壳,然后 降序枚举 求出上凸壳,这样凸包的每条边都是向 逆时针方向 旋转的。

设当前枚举到点 PP,即将把其加入凸包;当前栈顶的点为 S1S_1,栈中第二个点为 S2S_2.

求凸包时,若 PPS1S_1 构成的新线段是顺时针旋转的,即叉积满足:S2S1×S1P<0\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}<0,则弹出栈顶,继续检查,直到 S2S1×S1P0\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}\ge 0 或者栈内仅剩一个元素为止。

上图是一个弹栈的例子,pip_i 是新加入的点,细线是加入 pip_i 之前的凸包状态。

n=Pn=|P|,则时间复杂度为 O(nlogn)\mathcal{O}(n\log n),瓶颈在排序部分。

如上图,若将弹出栈顶元素的条件改为 S2S1×S1P0\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}\leq 0,同时停止条件改为 S2S1×S1P>0\overrightarrow{S_2S_1}\times \overrightarrow{S_1P}> 0,则求出的凸包中不存在三点共线。可视情况更改。

下面是参考代码。函数返回值为凸包的点数,Point ret[] 的下标从 00 开始。

inline bool check(Point s1, Point s2, Point p) {
	return Vec(s2, s1) * Vec(s1, p) > 0;
}
int Convex_hull_2d(int n, Point *p, Point *ret) {
	sort(p, p + n, cmp1);
	int top = -1;
	for (int i = 0; i < n; i++) {
		while (top > 0 && !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	int k = top;
	for (int i = n - 2; i >= 0; i--) {
		while (top > k && !check(ret[top], ret[top - 1], p[i]))
			top--;
		ret[++top] = p[i];
	}
	return top;
}
cpp

Graham算法#

Andrew 算法是 Graham 算法的改进版本。在 Graham 算法中,点集按照极角序排序。

**极角:**任取一个顶点 OO 作为极点,作射线 OXOX,称为极轴。平面上一点 pp 的极角,即为向量 Op\overrightarrow{Op} 与极轴 OXOX 的夹角。一般地,取 xx 轴作为极轴,以逆时针方向为正。

可以利用 atan2(double y, double x) 进行极角排序。函数返回值为 (x,y)(x, y)xx 轴的极角,数值 (π,π]\in(-\pi, \pi].

bool cmp(Point a, Point b) {
    if(atan2(a.y, a.x) - atan2(b.y, b.x) == 0)
        return a.x < b.x;
    return atan2(a.y, a.x) < atan2(b.y, b.x);
}
cpp

另外一种方式是利用叉积排序。

bool cmp(Point a, Point b) {
    return a * b > 0;
}
cpp

注意极角排序时,无论用 atan2 还是叉积,精度上都会出现不少问题,尽量避免使用这种方法。

平面凸包的周长与面积#

先求出按照顺时针排序的,构成凸包的点集 pp,记 n=pn=|p|.

**求周长:**把相邻两点组成的向量的模长求和,即:

l=i=1npipi+1+p1pnl=\sum_{i=1}^n|\overrightarrow{p_{i}p_{i+1}}|+|\overrightarrow{p_{1}p_{n}}|
double dis(Point a, Point b) {
	return (a - b).len();
}
double Convex_hull_2d_L(int n, Point *p) {
	Point convex[N];
	int siz = Convex_hull_2d(n, p, convex);
	double ans = dis(convex[0], convex[siz - 1]);
	for (int i = 1; i < siz; i++)
		ans += dis(convex[i - 1], convex[i]);
	return ans;
}
cpp

**求面积:**任取凸包内一点(一般取 p1p_1),则有:

s=i=2n1area(p1,pi,pi+1)=i=2n1(pip1)×(pi+1p1)2s=\sum_{i=2}^{n-1} \operatorname{area}(p_1, p_i, p_i+1)=\sum_{i=2}^{n-1}\frac{|(p_i-p_1)\times (p_{i+1}-p_1)|}{2}
double area(Point a, Point b, Point c) {
	return (b - a) * (c - a) / 2.0;
}
double Convex_hull_2d_S(int n, Point *p) {
	Point convex[N];
	int siz = Convex_hull_2d(n, p, convex);
	double ans = 0;
	for (int i = 2; i < siz; i++)
		ans += area(convex[0], convex[i - 1], convex[i]);
	return ans;
}
cpp

动态凸包(CF70D)#

维护一个点集 SS 的凸包,需要支持如下操作:

  • 询问点 pp 是否在当前的凸包中,
  • SS 中添加点 pp

保证坐标均为整数。

分析#

和 Andrew 算法一样,这里的算法按照坐标字典序排序。相对于极角排序,能够减小精度误差。

用两个 std::map<int, int>,用 top\operatorname{top} 记录上凸包,down\operatorname{down} 记录下凸包。

**存储方法:**若上凸包中存在横坐标为 xx 的点,则这个点的纵坐标为 top[x]\operatorname{top}[x]down\operatorname{down} 同理。

询问操作#

只需满足:在上凸包之下在下凸包之上

以上凸包为例。ii 在上凸包之下,当且仅当 ik×ij0|\overrightarrow{ik}\times \overrightarrow{ij}|\geq 0.

bool check_top(int x, int y) { //是否在上凸包下面
	auto k = top.lower_bound(x);
	if(k == top.end())
		return false;
	if(k -> first == x)
		return y <= k->second;
	if(k == top.begin()) return false;
	auto j = k; j--;
	return Point(k->first - x, k->second - y) *
		   Point(j->first - x, j->second - y) >= 0;
}
cpp

下凸包同理,ii 在下凸包之上,当且仅当 ik×ij0|\overrightarrow{ik}\times \overrightarrow{ij}|\leq 0

bool check_down(int x, int y) { //是否在下凸包上面
	auto k = down.lower_bound(x);
	if(k == down.end())
		return false;
	if(k -> first == x)
		return y >= k->second;
	if(k == down.begin()) return false;
	auto j = k; j--;
	return Point(k->first - x, k->second - y) *
		   Point(j->first - x, j->second - y) <= 0;
}
cpp

插入操作#

pp 点加入凸包,上下凸包都要尝试。把加入 pp 点后,删掉不满足凸性的点。

如上图,这些点一定是分布在 pxp_x 左右的连续段。

因此找到 pp 点在上/下凸壳中的位置,向左右分别删点,直到满足凸性。

注意迭代器的边界问题。如果已经删没了,要及时退出循环,否则会 RE。

void insert_top(int x, int y) {
	if(check_top(x, y)) return;
	top[x] = y;
	auto it = top.find(x);
	auto jt = it;
	if(it != top.begin()) { //remove left
		jt--;
		while(remove_top(jt++)) jt--;
	}
	if(++jt != top.end()) { //remove right
		while(remove_top(jt--)) jt++;
	}
}
void insert_down(int x, int y) {
	if(check_down(x, y)) return;
	down[x] = y;
	auto it = down.find(x);
	auto jt = it;
	if(it != down.begin()) { //remove left
		jt--;
		while(remove_down(jt++)) jt--;
	}
	if(++jt != down.end()) { //remove right
		while(remove_down(jt--)) jt++;
	}
}
cpp

下面的函数用于:判断能否删除当前点,若能删,则执行删除操作。

bool remove_top(map<int, int>::iterator it) {
	if(it == top.begin()) return false; //到边界就不删了
	if(++it == top.end()) return false; it--;
	auto jt = it, kt = it;
	jt--; kt++;
	if(Point(it -> first - jt -> first, it->second - jt->second) *
	   Point(it -> first - kt -> first, it->second - kt->second) <= 0) {
		   top.erase(it);
		   return true;
	}
	return false;
}
bool remove_down(map<int, int>::iterator it) {
	if(it == down.begin()) return false;
	if(++it == down.end()) return false; it--;
	auto jt = it, kt = it;
	--jt; ++kt;
	if(Point(it -> first - jt -> first, it->second - jt->second) *
	   Point(it -> first - kt -> first, it->second - kt->second) >= 0) {
		   down.erase(it);
		   return true;
	}
	return false;
}
cpp

三维凸包#

三维向量类#

**存储:**用结构体记录三维坐标,方便重载运算符。

struct Point3 {
	double x, y, z;
	Point3(){};
	Point3(double a, double b, double c) : x(a), y(b), z(c) {}
};
cpp

加法、减法和点乘等操作:与二维向量类似。

Point3 operator + (const Point3 &b) {
    return Point3(x + b.x, y + b.y, z + b.z);
}
Point3 operator - (const Point3 &b) {
    return Point3(x - b.x, y - b.y, z - b.z);
}
Point3 operator * (const Point3 &b) {
    return Point3(y*b.z - z*b.y, z*b.x - x*b.z, x*b.y - y*b.x);
}
bool operator == (const Point3 &b) {
    return fcmp(x, b.x) == 0 && fcmp(y, b.y) == 0 && fcmp(z, b.z) == 0;
}
double len() {
    return sqrt(x * x + y * y + z * z);
}
cpp

叉乘:a×b\boldsymbol a\times \boldsymbol b 的结果为一个三维向量 c\boldsymbol cca\boldsymbol c\perp \boldsymbol acb\boldsymbol c\perp \boldsymbol b,结果向量的模长为 absina,b|\boldsymbol a||\boldsymbol b|\sin \langle \boldsymbol a,\boldsymbol b\rangle,代表以 a\boldsymbol ab\boldsymbol b 为两边的平行四边形的面积。

在三维向量体系中,我们需要用坐标表示结果向量 a\boldsymbol{a},推导过程如下。(来源:叉积 - 维基百科

右手坐标系中,基向量 i,j,k\boldsymbol{i},\boldsymbol{j},\boldsymbol{k} 满足以下等式:

i×j=kj×i=kj×k=ik×j=ik×i=ji×k=j\begin{array}{c} \boldsymbol{i}\times\boldsymbol{j} = \boldsymbol{k}\\ \boldsymbol{j\times i} = -\boldsymbol{k}\\ \boldsymbol{j}\times\boldsymbol{k} = \boldsymbol{i}\\ \boldsymbol{k\times j} = -\boldsymbol{i}\\ \boldsymbol{k}\times\boldsymbol{i} = \boldsymbol{j}\\ \boldsymbol{i\times k} = -\boldsymbol{j} \end{array}

根据外积的定义可以得出:i×i=j×j=k×k=0\boldsymbol{i}\times\boldsymbol{i} = \boldsymbol{j}\times\boldsymbol{j} = \boldsymbol{k}\times\boldsymbol{k} = \boldsymbol{0}.

根据以上等式,结合外积的分配律,就可以确定任意向量的外积。

任取向量 u=u1i+u2j+u3k\boldsymbol{u}=u_1\boldsymbol{i} + u_2\boldsymbol{j} + u_3\boldsymbol{k} v=v1i+v2j+v3k\boldsymbol{v}=v_1\boldsymbol{i} + v_2\boldsymbol{j} + v_3\boldsymbol{k},两者的外积 u×v\boldsymbol{u} \times \boldsymbol{v} 可以根据分配率展开:

u×v=(u1i+u2j+u3k)×(v1i+v2j+v3k)=u1v1(i×i)+u1v2(i×j)+u1v3(i×k)+u2v1(j×i)+u2v2(j×j)+u2v3(j×k)+u3v1(k×i)+u3v2(k×j)+u3v3(k×k)\begin{align} \boldsymbol{u}\times\boldsymbol{v} = &(u_1\boldsymbol{i} + u_2\boldsymbol{j} + u_3\boldsymbol{k}) \times (v_1\boldsymbol{i} + v_2\boldsymbol{j} + v_3\boldsymbol{k})\\ = &u_1v_1(\boldsymbol{i} \times \boldsymbol{i}) + u_1v_2(\boldsymbol{i} \times \boldsymbol{j}) + u_1v_3(\boldsymbol{i} \times \boldsymbol{k}) + \\ &u_2v_1(\boldsymbol{j} \times \boldsymbol{i}) + u_2v_2(\boldsymbol{j} \times \boldsymbol{j}) + u_2v_3(\boldsymbol{j} \times \boldsymbol{k}) + \\ &u_3v_1(\boldsymbol{k} \times \boldsymbol{i}) + u_3v_2(\boldsymbol{k} \times \boldsymbol{j}) + u_3v_3(\boldsymbol{k} \times \boldsymbol{k})\\ \end{align}

把前面的 66 个等式代入,则有:

u×v=u1v10+u1v2ku1v3ju2v1ku2v20+u2v3i+u3v1ju3v2iu3v30=(u2v3u3v2)i+(u3v1u1v3)j+(u1v2u2v1)k\begin{align} \boldsymbol{u}\times\boldsymbol{v} = {} &- u_1v_1\boldsymbol{0} + u_1v_2\boldsymbol{k} - u_1v_3\boldsymbol{j} \\ &- u_2v_1\boldsymbol{k} - u_2v_2\boldsymbol{0} + u_2v_3\boldsymbol{i} \\ &+ u_3v_1\boldsymbol{j} - u_3v_2\boldsymbol{i} - u_3v_3\boldsymbol{0} \\ = {} &(u_2v_3 - u_3v_2)\boldsymbol{i} + (u_3v_1 - u_1v_3)\boldsymbol{j} + (u_1v_2 - u_2v_1)\boldsymbol{k}\\ \end{align}

因此结果向量 s=u×v=s1i+s2j+s3k\boldsymbol{s} = \boldsymbol{u} \times \boldsymbol{v} = s_1\boldsymbol{i} + s_2\boldsymbol{j} + s_3\boldsymbol{k} 的三维坐标为:

s1=u2v3u3v2s2=u3v1u1v3s3=u1v2u2v1\begin{align} s_1 &= u_2v_3-u_3v_2\\ s_2 &= u_3v_1-u_1v_3\\ s_3 &= u_1v_2-u_2v_1 \end{align}
Point3 operator * (const Point3 &b) {
    return Point3(y*b.z - z*b.y, z*b.x - x*b.z, x*b.y - y*b.x);
}
cpp

平面类#

用三个向量表示一个三角形的平面。一个多面体可以通过三角剖分,用若干个三角形表示。

为了节省空间,用 point3 p[N] 存储所有可能出现的向量,结构体plane只记录向量在 p[] 中的下标。

记录的三个向量按逆时针首尾相接,这样在判断方向时比较方便。

struct plane{
	int v[3]; //逆时针
	plane(){};
	plane(int a, int b, int c) { v[0] = a, v[1] = b, v[2] = c; }
};
cpp

**平面的法向量:**是指垂直于该平面的三维向量。一个平面具有无限个法向量,这些法向量有两个方向。

根据叉积的性质,将三角形的两条邻边叉乘,得到的向量即为法向量。

Point3 normal() {
    return (p[v[1]] - p[v[0]]) * (p[v[2]] - p[v[0]]);
}
cpp

利用法向量的模长,也可以算出三角形的面积。

double area() {
    return normal().len() / 2.0;
}
cpp

三维凸包及性质#

nn 个点构成的凸多面体。

**性质:**根据欧拉公式,任意包含 nn 个顶点的凸多面体,所含的边不会超过 3n63n-6 条,所含的小平面不会超过 2n42n-4 张。

随机增量法#

算法思想#

和 Andrew 算法类似,考虑每次把 prp_r 加入到前 r1r-1 个点的凸包中,也就是将 CH(Pr1)\mathcal{CH}(P_{r-1}) 转化为 CH(Pr)\mathcal{CH}(P_{r}).

第一种情况:prp_rCH(Pr1)\mathcal{CH}(P_{r-1}) 的内部或边界上,则 CH(Pr1)CH(Pr)\mathcal{CH}(P_{r-1})\rightarrow \mathcal{CH}(P_{r}).

第二种情况:prp_rCH(Pr1)\mathcal{CH}(P_{r-1}) 外部。设想你站在 prp_r 所在的位置,看向 CH(Pr1)\mathcal{CH}(P_{r-1}). CH(Pr1)\mathcal{CH}(P_{r-1}) 中的某些小平面会被看到,其余在背面的平面不会被看到。如下图,从 prp_r 可见的平面构成了一片连通的区域。

这片区域由一条封闭折线围成,称这条线为 prp_rCH(Pr1)\mathcal{CH}(P_{r-1}) 上的边界线 (horizon)(\rm{horizon})

根据这条地平线,我们可以判断出,在原先 CH(Pr1)\mathcal{CH}(P_{r-1}) 表面上的哪些部分需要被保留,哪些需要被替换。

显然,不可见的平面在 CH(Pr)\mathcal{CH}(P_{r}) 中被保留,并且我们用 prp_r 与地平线之间连接出新的小平面,来替换所有可见的小平面,如下图。

判断平面对点的可见性#

如何用几何语言表达:一个平面对 prp_r 是可见的?

对于一个凸包上的小平面,它能将空间分为两半,一侧是凸包外部,一侧是凸包内部。容易发现,如果点 pp 位于小平面的外侧,那么这个平面对于 pp 点就是可见的,因为凸包上的其它平面都不会有遮挡。

形式化地,记 a,b,ca,b,c 为平面三角形的三个顶点,从凸包外部看,三点按照逆时针排列。

利用叉乘的性质,记 s=ab×ac\boldsymbol{s}=\overrightarrow{ab}\times \overrightarrow{ac},则结果向量 s\boldsymbol{s} 是一个平面的法向量,且指向凸包外部。

对于空间内任意一点 pp,若 ap×s>0\overrightarrow{ap}\times \boldsymbol{s}>0,则这个平面对点 pp 是可见的。

下面是定义在结构体 plane 中的函数,用于判断点 AA 是否位于平面的外侧。

bool is_above(Point3 A) {
    return (normal() & (A - p[v[0]])) >= 0;
}
cpp

求出边界线#

要想把凸包从 CH(Pr1)\mathcal{CH}(P_{r-1}) 转化为 CH(Pr)\mathcal{CH}(P_{r}),我们需要准确地求出凸包上的哪些边在边界线上。求出边界线之后,才能用 pp 与边界线构成的小平面替换需要被删掉的小平面。

定义 bool g[N][N]g[i][j]g[i][j] 表示 pipj\overrightarrow{p_ip_j} 所在的平面是否可见。

若规定平面 (a,b,c)(a,b,c) 只包含 ab,bc,ca\overrightarrow{ab},\overrightarrow{bc},\overrightarrow{ca},则对于任意有序数对 (i,j)(i, j),向量 pipj\overrightarrow{p_ip_j} 最多被包含在一个平面内。

注意到,位于边界线上的向量 pipj\overrightarrow{p_ip_j} 一定满足 g[i][j]=1g[i][j]=1g[j][i]=0g[j][i]=0。所以我们只需对每个平面判断其可见性,并更新在 g[][] 中对应的数值,即可求出边界线。

利用边界线更新凸包#

在上一步遍历小平面时,若遇到的小平面是不可见的,则把它加入新的凸包中;若可见,则单独记录。

之后遍历所有可见的小平面,若 pipj\overrightarrow{p_ip_j} 在边界线上,则把 (pi,pj,pr)(p_i, p_j, p_r) 加入凸包中。prp_r 是新加入凸包的点。这样加入后的三点也满足逆时针排列。

参考代码#

函数返回值为三维凸包的平面数,plane ret[] 的下标从 00 开始。

int Convex_hull_3d(int n, plane *ret) {
	plane tmp[N];
	bool g[N][N];
	for (int i = 0; i < n; i++) p[i].shake();
	int top = -1;
	ret[++top] = plane(0, 1, 2);
	ret[++top] = plane(0, 2, 1);
	for (int i = 3; i < n; i++) {
		int cnt = -1;
		for (int j = 0; j <= top; j++) {
			bool flag = ret[j].is_above(p[i]);
			if (!flag)
				tmp[++cnt] = ret[j];
			for (int k = 0; k < 3; k++)
				g[ret[j].v[k]][ret[j].v[(k + 1) % 3]] = flag;
		}
		for (int j = 0; j <= top; j++) {
			for (int k = 0; k < 3; k++) {
				int a = ret[j].v[k], b = ret[j].v[(k + 1) % 3];
				if (g[a][b] && !g[b][a])
					tmp[++cnt] = plane(a, b, i);
			}
		}
		for (int j = 0; j <= cnt; j++) ret[j] = tmp[j];
		top = cnt;
	}
	return (top + 1);
}
cpp

旋转卡壳#

概述#

旋转卡壳算法用于:在线性时间内,求凸包直径、最小矩形覆盖等于凸包性质相关的问题。线性时间是指求出凸包之后的算法时间复杂度。

求凸包直径#

给定平面上的 nn 个点,求所有点对之间的最长距离。2n50000,x,y1042\leq n\leq 50000, |x|, |y|\leq 10^4.

首先,求出这 nn 个点的凸包,复杂度可以做到为 O(nlogn)\mathcal{O}(n\log n),如何求直径?

**暴力做法:**可以遍历每个点对,求出最大距离,复杂度为 O(n2)\mathcal{O}(n^2).

算法流程#

可以遍历凸包上的边,对每条边 (a,b)(a, b),去找距离这条边最远的点 pp。对于 pp 点,距离它最远的点,一定是 a,ba,b 中的一个。

我们发现,若逆时针遍历凸包上的边,那么随着边的转动,对应的最远点也在逆时针旋转。

因此,我们可以在逆时针枚举边的同时,实时维护最远点,利用单调性,复杂度为 O(n)\mathcal{O}(n).

算法实现#

求凸包时,若使用 Andrew 算法,则凸包上的点已经按照逆时针排序了。问题在如何判断下一个点到当前边的距离是否更大。

一种方法是用点到直线距离公式,但利用叉积的精度更高,也更方便。

struct Point{
	int x, y;
    //......
	int sqr_len() { return x * x + y * y; }
};
inline int sqr_dis(Point a, Point b) { return (a - b).sqr_len(); }
int Get_Max(int n, Point *ch) {//传入convex-hull
	int ret = 0;
	ch[n] = ch[0];
	int j = 1;
	for(int i = 0; i < n; i++) {
		while((ch[i] - ch[j+1]) * (ch[i+1] - ch[j+1]) >
			  (ch[i] - ch[j]) * (ch[i+1] - ch[j]))
			j = (j + 1) % n;
		ret = max(ret, max(sqr_dis(ch[i], ch[j]), sqr_dis(ch[i+1], ch[j])));
	}
	return ret;
}
cpp

最小矩形覆盖#

题意#

给定一些点的坐标,求能够覆盖所有点的最小面积的矩形。

求出矩形面积、顶点坐标。3n500003\leq n\leq 50000.

分析#

先求出凸包,之后用旋转卡壳维护三个边界点。

利用叉积和点积,可以求出矩形面积及四个顶点的坐标。

实现#

下面是一种可行的表示方法。

H=P0P3=AB×BUABH=|P_0P_3|=\frac{|\overrightarrow{AB}\times \overrightarrow{BU}|}{|AB|}L=BAALBAL=\frac{|\overrightarrow{BA}\cdot \overrightarrow{AL}|}{|BA|}R=ABBRABR=\frac{|\overrightarrow{AB}\cdot \overrightarrow{BR}|}{|AB|}.

则矩形面积为:

S=H×(L+AB+R)S=H\times (L+|\overrightarrow{AB}|+R)

顶点坐标为:

P0=A+L×BABAP1=B+R×ABABP2=P1+H×P1RP1RP3=P0+H×P0LP0L\begin{array}{l} &\overrightarrow{P_0}&=&\overrightarrow A+L\times \frac{\overrightarrow{BA}}{|\overrightarrow{BA}|}\\ &\overrightarrow{P_1}&=&\overrightarrow B+R\times \frac{\overrightarrow{AB}}{|\overrightarrow{AB}|}\\ &\overrightarrow{P_2}&=&\overrightarrow P_1+H\times \frac{\overrightarrow{P_1R}}{|\overrightarrow{P_1R}|}\\ &\overrightarrow{P_3}&=&\overrightarrow P_0+H\times \frac{\overrightarrow{P_0L}}{|\overrightarrow{P_0L}|} \end{array}

若不需要求出顶点坐标,也可以这样表示矩形面积:

S=AB×BU×(ADAB+BCBAABBA)ABBAS=\frac{|\overrightarrow{AB}\times \overrightarrow{BU}|\times (|\overrightarrow{AD}\cdot\overrightarrow{AB}|+|\overrightarrow{BC}\cdot\overrightarrow{BA}|-|\overrightarrow{AB}\cdot\overrightarrow{BA}|)}{|\overrightarrow{AB}\cdot\overrightarrow{BA}|}

P0B=m|\overrightarrow{P_0B}|=mAP1=n|\overrightarrow{AP_1}|=n 后易证。

代码#

double Get_Max(int n, Point *ch) {
	ch[n] = ch[0];
	int u = 2, l, r = 2;
	//u是距离AB最远的点;在AB为底时,l和r是两个最靠边的点
	double ret = 1e100, H, L, R, S;
	for(int i = 0; i < n; i++) {
		Point A = ch[i], B = ch[i+1]; Vec AB = B - A, BA = A - B;
		while((AB * Vec(B, ch[u+1])) >= (AB * Vec(B, ch[u])))
			u = (u + 1) % n;
		while((AB & Vec(B, ch[r+1])) >= (AB & Vec(B, ch[r])))
			r = (r + 1) % n;
		if(i == 0) l = r;
		while((AB & Vec(B, ch[l+1])) <= (AB & Vec(B, ch[l])))
			l = (l + 1) % n;
		H = (AB * Vec(B, ch[u])) / AB.len(); //以AB所在直线为底边,矩形的高
		L = (BA & Vec(A, ch[l])) / BA.len(); //A距离左侧顶点的距离
		R = (AB & Vec(B, ch[r])) / AB.len(); //B距离右侧顶点的距离
		S = H * (L + AB.len() + R); //矩形面积
		if(S < ret) { //求矩形顶点坐标
			ret = S;
			P[0] = A + L * BA.unit();
			P[1] = B + R * AB.unit();
			P[2] = P[1] + H * (ch[r]-P[1]).unit();
			P[3] = P[0] + H * (ch[l]-P[0]).unit();
		}
	}
	return ret;
}
cpp

POJ2079 - Triangle#

题意#

给定平面上的 nn 个点,从其中任选三个点作为顶点,求能构成的最大三角形面积。

1n500001\leq n\leq 50000.

分析#

显然,三角形的顶点一定都在这 nn 个点的凸包上,所以先求出凸包。

考虑旋转卡壳。

这题中不能固定一条边,再枚举另外两个点,因为三角形的边不一定在凸包上。

因此,先逆时针枚举一个固定点 ii,再逆时针旋转另外两个顶点 jjkk

由于凸包上的一些单峰性,我们旋转 jj 时停止的条件是 SijkS_{\triangle ijk} 最大,停止后固定 jj,以相同停止条件旋转 kk.

SijkS_{\triangle ijk} 最大,是指 Si,j_last,k<SijkS_{\triangle i,j\operatorname{\_last},k}<S_{\triangle ijk}Sijk>Si,j_next,kS_{\triangle ijk}>S_{\triangle i,j\operatorname{\_next},k}.

代码#

注意特判 n2n\leq 2 以及凸包大小 siz2\operatorname{siz}\leq 2 的情况。用叉积求面积。

double Get_Max(int n, Point *ch) {
	double ret = 0;
	ch[n] = ch[0];
	int j = 1, k = 2;
	for(int i = 0; i < n; i++) {
		while(Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]) <
			  Vec(ch[i], ch[j]) * Vec(ch[i], ch[k+1]))
			k = (k + 1) % n;
		while(Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]) <
			  Vec(ch[i], ch[j+1]) * Vec(ch[i], ch[k]))
			j = (j + 1) % n;
		ret = max(ret, Vec(ch[i], ch[j]) * Vec(ch[i], ch[k]));
	}
	return ret;
}
cpp

半平面交#

半平面#

半平面是一条直线和直线的一侧,是一个点集。

当点集包含直线时,称为闭半平面;当不包含直线时,称为开半平面。

直线类#

在计算几何中,用 (s,t)(\boldsymbol s, \boldsymbol t) 表示直线 st{st},一般统一保留向量的左侧半平面。

求两直线的交#

若交点存在,只需求出 t=AsPAsAtt=\dfrac{|A_sP|}{|A_sA_t|},则 P=As+tAsAt\overrightarrow P=\overrightarrow{A_s}+t\cdot \overrightarrow{A_sA_t}.

注意到 t=AsPAsAt=BsBt×BsAsBsBt×AsAtt=\dfrac{|A_sP|}{|A_sA_t|}=\dfrac{|\overrightarrow{B_sB_t}\times \overrightarrow{B_sA_s}|}{|\overrightarrow{B_sB_t}\times \overrightarrow{A_sA_t}|},证明也不难。

如上图,把 AsAt\overrightarrow{A_sA_t} 平移到 BsC\overrightarrow{B_sC} 的位置,则可以把叉乘的模看作平行四边形面积。

S1=BsBt×BsAsS_1=|\overrightarrow{B_sB_t}\times \overrightarrow{B_sA_s}|S2=BsBt×AsAtS_2=|\overrightarrow{B_sB_t}\times \overrightarrow{A_sA_t}|,由于两个平行四边形同底,所以有 S1S2=AsMCN\dfrac{S_1}{S_2}=\dfrac{|A_sM|}{|CN|}.

又因为 AsMPCNBs\triangle A_sMP\sim \triangle CNB_s,所以 S1S2=AsMCN=AsPCBs=AsPAsAt\dfrac{S_1}{S_2}=\dfrac{|A_sM|}{|CN|}=\dfrac{|A_sP|}{|CB_s|}=\dfrac{|A_sP|}{|A_sA_t|}.

参考代码#

struct Line{
	Point s, t;
	Line() {};
	Line(Point a, Point b) : s(a), t(b) {}
	double ang() { return atan2((t - s).y, (t - s).x); };
	Line(double a, double b, double c) { //ax + by + c = 0
		if(sgn(a) == 0) s = Point(0, -c/b), t = Point(1, -c/b);
		else if(sgn(b) == 0) s = Point(-c/a, 0), t = Point(-c/a, 1);
		else s = Point(0, -c/b), t = Point(1, (-c-a)/b);
	}
	friend bool parallel(const Line &A, const Line &B) {
		return sgn((A.s - A.t) * (B.s - B.t)) == 0;
	}
	friend bool Calc_intersection(const Line &A, const Line &B, Point &res) {
		if(parallel(A, B)) return false;
		double s1 = (B.t - B.s) * (B.s - A.s);
		double s2 = (B.t - B.s) * (A.t - A.s);
		res = A.s + (A.t - A.s) * (s1 / s2);
		return true;
	}
};
cpp

半平面交#

半平面交是多个半平面的交集

半平面交是一个点集,并且是一个凸集。在直角坐标系上是一个区域。

半平面交在代数意义下,是若干个线性约束条件,每个约束条件形如:

aix+biycia_ix+b_iy\leq c_i

其中 ai,bi,cia_i, b_i, c_i 为常数,且 aia_ibib_i 不都为零。

半平面交有下面五种可能情况,每个半平面位于边界直线的阴影一侧。

(iii)\rm{(iii)}(v)\rm{(v)} 比较特殊,我们一般假设产生的交集总是有界或空的。

性质#

注意到,位于半平面交边界上的任何一个点,必定来自某张半平面的边界。

并且,因为半平面交是凸集,所以每条边界线最多只能为边界贡献一条边。

因此:nn 张半平面相交而成的凸多边形,其边界最多由 nn 条边围成

Sort-and-Incremental Algorithm#

S&I 算法,排序增量法。

算法思想#

S&I 算法利用了性质:半平面交是凸集。

因为半平面交是凸集,所以我们维护凸壳

若我们把半平面按极角排序,那么在过程中,只可能删除队首或队尾的元素,因此使用双端队列维护。

下面是一个例子。

TODO.

算法流程#

  1. 把半平面按照极角序排序,需要 O(nlogn)\mathcal{O}(n\log n) 的时间。
  2. 对每个半平面,执行一次增量过程,每次根据需要弹出双端队列的头部或尾部元素。这是线性的,因为每个半平面只会被增加或删除一次。
  3. 最后,通过双端队列中的半平面,在线性时间内求出半平面交(一个凸多边形,用若干顶点描述)。

这样,我们得到了一个时间复杂度为 O(nlogn)\mathcal{O}(n\log n) 的算法,瓶颈在于排序。因此,若题目给定的半平面满足极角序,则我们可以在线性的时间内求出半平面交。

极角排序#

注意判断不要写成 <=>=

inline bool cmp(Line A, Line B) {
	//极角相等时,位置靠右的排在前面
	if(!sgn(A.ang - B.ang)) return (A.t - A.s) * (B.t - A.s) > 0;
	return A.ang < B.ang;
}
cpp

求半平面交#

bool Halfplane_intersection(int n, Line *hp, Point *p) {
	if(n < 3) return false;
	sort(hp, hp + n, cmp);
	Halfplane_unique(n, hp);
	st = 0; ed = 1;
	que[0] = 0; que[1] = 1;
	if(parallel(hp[0], hp[1])) return false;
	Calc_intersection(hp[0], hp[1], p[1]);
	for(int i = 2; i < n; i++) {
		while(st < ed &&
			  sgn((hp[i].t - hp[i].s) * (p[ed] - hp[i].s)) < 0)
			ed--;
		while(st < ed &&
			  sgn((hp[i].t - hp[i].s) * (p[st + 1] - hp[i].s)) < 0)
			st++;
		que[++ed] = i;
		assert(ed >= 1);
		if(parallel(hp[i], hp[que[ed - 1]])) return false;
		Calc_intersection(hp[i], hp[que[ed - 1]], p[ed]);
	}
	while(st < ed &&
		  sgn((hp[que[st]].t - hp[que[st]].s) * (p[ed] - hp[que[st]].s)) < 0)
		ed--;
	while(st < ed &&
		  sgn((hp[que[ed]].t - hp[que[ed]].s) * (p[st + 1] - hp[que[ed]].s)) < 0)
		st++;
	if(st + 1 >= ed) return false;
	return true;
}
cpp

求凸多边形的顶点#

int Get_convex_hull(Line *hp, Point *p, Point *ch) {
	Calc_intersection(hp[que[st]], hp[que[ed]], p[st]);
	for(int i = 0, j = st; j <= ed; i++, j++) ch[i] = p[j];
	return ed - st + 1;
}
cpp

计算面积#

double Calc_area(int n, Point *ch) {
	double ans = 0;
	for (int i = 2; i < n; i++)
		ans += area(ch[0], ch[i - 1], ch[i]);
	return ans;
}
cpp

P3256 [JLOI2013]赛车#

题目描述#

赛场上一共有 nn 辆车,分别称为 g1,g2,...,gng_1,g_2,...,g_n.

赛道是一条无限长的直线。最初,gig_i 位于距离起跑线前进 xix_i 的位置。比赛开始后,车辆 gig_i 将会以 viv_i 单位每秒的恒定速度行驶。

过程中,如果一辆赛车曾经处于领跑位置的话,即没有其他的赛车跑在他的前面,这辆赛车最后就可以得奖。

求哪些赛车将会得奖。输出赛车编号。

1n1041\leq n\leq 10^40xi1090\leq x_i\leq 10^90vi1090\leq v_i\leq 10^9.

分析#

可以想到,每辆车的 xtx-t 图像都是一条直线。在平面直角坐标系上画出这些直线。

若取每条直线的左侧半平面,再与 xx 轴上侧平面、yy 轴右侧平面一起求交,则所有凸多边形上的直线可以获奖。

直接跑排序增量法即可,时间复杂度 O(nlogn)\mathcal{O}(n\log n).

注意到这题 xix_iviv_i 的值域均为 10910^9,所以运算过程中的数值会达到 109×109=101810^9\times 10^9=10^{18}

因此需要使用 long double,并且需要 const long double eps = 1e-18.

代码#

这道题不能直接去重,因为需要输出获奖车辆的编号,所以用 std::map 维护。

同时,还需要在 Line 类中存储所有重合直线的编号,用 std::vector 维护。

struct Line{
	Point s, t;
	vector<int> id;
	Line() {};
	Line(Point a, Point b) : s(a), t(b) {}
	Line(Point a, Point b, vector<int> c) : s(a), t(b), id(c) {}
	//...
};
//...
signed main() {
	int n, x[MAXN], v[MAXN], N = 0;
	map<pair<int, int>, vector<int> > mp;
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> x[i];
	for(int i = 1; i <= n; i++) cin >> v[i];

	for(int i = 1; i <= n; i++) mp[make_pair(x[i], v[i])].push_back(i);
	L[N++] = Line(Point(0, 1), Point(0, 0));
	L[N++] = Line(Point(0, 0), Point(1, 0));
	for(auto it = mp.begin(); it != mp.end(); it++) {
		pair<int, int> tmp = it -> first;
		L[N++] = Line(Point(0, tmp.first),
					  Point(1, tmp.first+tmp.second), it -> second);
	}

	Halfplane_intersection(N, L, p);

	int ans[MAXN], cnt_ans = 0;
	for(int i = st + 1; i <= ed; i++) {
		for(int j = 0; j < L[que[i]].id.size(); j++) {
			ans[cnt_ans++] = L[que[i]].id[j];
		}
	}
	sort(ans, ans + cnt_ans);
	cout << cnt_ans << endl;
	for(int i = 0; i < cnt_ans; i++) cout << ans[i] << " "; cout << endl;
	return 0;
}
cpp

P4250 [SCOI2015]小凸想跑步#

题目描述#

小凸晚上喜欢到操场跑步,今天他跑完两圈之后,他玩起了这样一个游戏。

操场是个凸 nn 边形, nn 个顶点按照逆时针00n1n - 1 编号。

现在小凸随机站在操场中的某个位置,标记为 pp 点。将 pp 点与 nn 个顶点各连一条边,形成 nn 个三角形。

如果这时 pp 点, 00 号点, 11 号点形成的三角形的面积是 nn 个三角形中最小的一个,则认为这是一次正确站位。

现在小凸想知道他一次站位正确的概率是多少。

分析#

推一点式子。

A=(xa,ya)\overrightarrow A=(x_a,y_a)B=(xb,yb)\overrightarrow B=(x_b, y_b)C=(xc,yc)\overrightarrow C=(x_c, y_c)D=(xd,yd)\overrightarrow D=(x_d, y_d)P=(x,y)\overrightarrow P=(x, y).

由题意,

AB×AP<CD×CP(xbxa)(yya)(ybya)(xxa)<(xdxc)(yyc)(ydyc)(xxc)\begin{array}{rl} |\overrightarrow {AB}\times \overrightarrow{AP}|&<&|\overrightarrow {CD}\times \overrightarrow{CP}|\\ (x_b-x_a)(y-y_a) - (y_b-y_a)(x-x_a) &<& (x_d-x_c)(y-y_c)-(y_d-y_c)(x-x_c)\\ \end{array}

展开得,

左式=(xbxa)yxbya+xaya(ybya)x+xaybxaya右式=(xdxc)yxdyc+xcyc(ydyc)x+xcydxcyc\begin{array}{rl} \text{左式}&=&(x_b-x_a)y-x_by_a+x_ay_a-(y_b-y_a)x+x_ay_b-x_ay_a\\ \text{右式}&=&(x_d-x_c)y-x_dy_c+x_cy_c-(y_d-y_c)x+x_cy_d-x_cy_c \end{array}

合并同类项,

(xbxa)(yya)(ybya)(xxa)<(xdxc)(yyc)(ydyc)(xxc)(xbxa+xcxd)y+(yayb+ydyc)x+(ybxaxbyaydxc+xdyc)<0\begin{array}{l} &(x_b - x_a)(y - y_a) - (y_b - y_a)(x - x_a) < (x_d - x_c)(y - y_c) - (y_d - y_c)(x - x_c) \\ \Rightarrow & (x_b - x_a + x_c - x_d)y + (y_a - y_b + y_d - y_c)x + (y_b x_a - x_b y_a - y_d x_c + x_d y_c) < 0 \end{array}

注意到,这是直线解析式的形式,于是转化为了半平面交问题。

一些资料#

[2] Preparata F P, Shamos M I. Computational geometry: an introduction[M]. Springer Science & Business Media, 2012.

[3] TOUSSAINT G T. Solving geometric problems with the rotating calipers[C]//Proc. IEEE Melecon.1983:A10.

[4] TOUSSAINT G T. The rotating calipers: An efficient, multipurpose, computational tool[C]//The International Conference on Computing Technology and Information Management (ICCTIM).Citeseer,2014:215.

[5] THOMAS H C, CHARLES,E.LEISERSON,RONALD,L.RIVEST,CLIFFORD,STEIN,殷建平,徐 云,王刚,刘晓光,苏明,邹恒明,王宏志 2013. 算法导论(原书第 3 版). 计算机教育 [J]: 1.

[6] 董永建. 信息学奥赛一本通[M].科学技术文献出版社,2013.

[7] PRATA S, 张海龙, 袁国忠. C++ Primer Plus(第 6 版)中文版[M]. 人民邮电出版社, 2012.

[8] 同济大学数学系.高等数学.下册[M].北京: 高等教育出版社,2014.1-23.\

[9] 人民教育出版社 课程教材研究所 中学数学教材实验研究组.普通 高中教科书 数学(B 版) 必修 第二册[M].北京:人民教育出版社,2019:131-172.

https://cp-algorithms.com/geometry/halfplane-intersection.html

计算几何
https://www.tonyyin.top/blog/oi-notes/geometry
Author TonyYin
Published at December 7, 2021
Comment seems to stuck. Try to refresh?✨