向量#
平面向量#
向量: 既有大小又有方向的量称为向量,记作 或 。
有向线段: 带方向的线段。用有向线段来直观地表示向量。起点为 终点为 的有向线段表示的向量,用符号简记为 .
**向量的模:**向量的大小(或长度),用 或 表示。
零向量:模为 的向量。零向量的方向任意。记为: 或 。
单位向量:模为 的向量称为该方向上的单位向量。
平行向量:方向相同或相反的两个 非零 向量。规定 与任意向量平行。 与 平行,记作: 。
**共线向量:**与平行向量的定义相同。任一组平行向量都可以平移到同一直线上。
向量的夹角:已知两个非零向量 ,作 ,那么 就是向量 与向量 的夹角。记作:。当 时,称这两个向量垂直,记作 。规定 。
向量的线性运算#
向量的加法#
向量加法的三角形法则#
对于平面上的任意两个向量 和 ,在平面内任取一点 ,作 ,,作向量 .
称向量 为向量 和 的 和向量,
如图1,把向量首尾顺次相连,向量的和为第一个向量的起点指向最后一个向量的终点;
向量加法的平行四边形法则#
若要求和的两个向量 共起点,那么它们的和向量为以这两个向量为邻边的平行四边形的对角线,起点为两个向量共有的起点,方向沿平行四边形对角线方向。
向量的减法#
减法可以写成加上相反数的形式,即:,如图1,,.
向量的数乘#
给定一个实数 和一个向量 ,规定其乘积为一个向量,记作 ,其模与方向定义如下:
- ;
- 当 时, 与 同向,当 时,,当 时, 与 方向相反。
这种运算是向量的数乘运算。
坐标表示#
向量的数量积#
已知两个向量 ,它们的夹角为 ,那么:
就是这两个向量的 数量积,也叫 点积 或 内积。其中称 为 在 方向上的投影。数量积的几何意义即为:数量积 等于 的模与 在 方向上的投影的乘积。
这种运算得到的结果是一个实数,为标量。
可以方便的计算出 ,于是有如下应用:
向量的向量积#
给定两个向量 ,规定其向量积为一个向量,记作 ,其模与方向定义如下:
-
;
-
与 都垂直,且 符合右手法则。
向量积也叫外积,其几何意义是: 是以 为邻边的平行四边形的面积。
向量积与 所在平面垂直,其竖坐标为 .
我们根据右手法则可以推断出 相对于 的方向,逆时针方向竖坐标为正值,反之为负值。
坐标旋转公式#
若将向量 逆时针旋转 ,得到向量 ,则有:
可以通过三角恒等变换证明。
参考代码#
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平面凸包#
相关概念#
**凸多边形:**所有内角大小都在 范围内的 简单多边形。
**平面凸包:**平面上的一个子集 被称为凸的,当且仅当对于任意两点 ,线段 都完全属于 .集合 的凸包 ,是包含 的最小凸集,也就是包含 的所有凸集的交。
如上图,凸包还可以理解为平面上有若干柱子,用橡皮筋套住所有柱子,绷紧后形成的多边形即为凸包。
所以有更友好的定义(不一定准确)。
**凸包:**在平面上能包含所有给定点的最小凸多边形叫做凸包。
暴力求解#
用二维坐标 的形式给定点集 ,考虑如何暴力求解。
注意到,若线段 在凸包上,则 中的点均位于直线 的同一侧。若我们钦定 按顺时针方向,则有更强的限制,需要 中的点都在直线的右侧。
于是可以枚举有序点对 ,若 中的点都在有向线段 的右侧,则 是 中的一条边。
需要用到向量的叉积,点 在 右侧 .
这样的复杂度是 的,有很多可以优化的地方。
Andrew算法#
Andrew 算法是一种递增式算法,流程如下。
递增式算法 (incremental algorithm),在计算几何中常见。算法思想:逐一加入 中的点,每增加一个点,都更新一次目前的解,加入最后一个点后,即可得到答案。
首先把所有点 排序 ,以横坐标为第一关键字,纵坐标为第二关键字。
排序后,第一个点和末尾的点,一定在凸包上,容易通过反证法证明。
从左往右看,上下凸壳斜率的 单调性相反,即所旋转的方向不同,所以要分开求。
我们 升序枚举 求出下凸壳,然后 降序枚举 求出上凸壳,这样凸包的每条边都是向 逆时针方向 旋转的。
设当前枚举到点 ,即将把其加入凸包;当前栈顶的点为 ,栈中第二个点为 .
求凸包时,若 与 构成的新线段是顺时针旋转的,即叉积满足:,则弹出栈顶,继续检查,直到 或者栈内仅剩一个元素为止。
上图是一个弹栈的例子, 是新加入的点,细线是加入 之前的凸包状态。
记 ,则时间复杂度为 ,瓶颈在排序部分。
如上图,若将弹出栈顶元素的条件改为 ,同时停止条件改为 ,则求出的凸包中不存在三点共线。可视情况更改。
下面是参考代码。函数返回值为凸包的点数,Point ret[]
的下标从 开始。
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;
}
cppGraham算法#
Andrew 算法是 Graham 算法的改进版本。在 Graham 算法中,点集按照极角序排序。
**极角:**任取一个顶点 作为极点,作射线 ,称为极轴。平面上一点 的极角,即为向量 与极轴 的夹角。一般地,取 轴作为极轴,以逆时针方向为正。
可以利用 atan2(double y, double x)
进行极角排序。函数返回值为 与 轴的极角,数值 .
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
还是叉积,精度上都会出现不少问题,尽量避免使用这种方法。
平面凸包的周长与面积#
先求出按照顺时针排序的,构成凸包的点集 ,记 .
**求周长:**把相邻两点组成的向量的模长求和,即:
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**求面积:**任取凸包内一点(一般取 ),则有:
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)#
维护一个点集 的凸包,需要支持如下操作:
- 询问点 是否在当前的凸包中,
- 向 中添加点 ,
保证坐标均为整数。
分析#
和 Andrew 算法一样,这里的算法按照坐标字典序排序。相对于极角排序,能够减小精度误差。
用两个 std::map<int, int>
,用 记录上凸包, 记录下凸包。
**存储方法:**若上凸包中存在横坐标为 的点,则这个点的纵坐标为 , 同理。
询问操作#
只需满足:在上凸包之下且在下凸包之上。
以上凸包为例。 在上凸包之下,当且仅当 .
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下凸包同理, 在下凸包之上,当且仅当
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插入操作#
把 点加入凸包,上下凸包都要尝试。把加入 点后,删掉不满足凸性的点。
如上图,这些点一定是分布在 左右的连续段。
因此找到 点在上/下凸壳中的位置,向左右分别删点,直到满足凸性。
注意迭代器的边界问题。如果已经删没了,要及时退出循环,否则会 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叉乘: 的结果为一个三维向量 , 且 ,结果向量的模长为 ,代表以 、 为两边的平行四边形的面积。
在三维向量体系中,我们需要用坐标表示结果向量 ,推导过程如下。(来源:叉积 - 维基百科 ↗)
右手坐标系中,基向量 满足以下等式:
根据外积的定义可以得出:.
根据以上等式,结合外积的分配律,就可以确定任意向量的外积。
任取向量 和 ,两者的外积 可以根据分配率展开:
把前面的 个等式代入,则有:
因此结果向量 的三维坐标为:
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三维凸包及性质#
由 个点构成的凸多面体。
**性质:**根据欧拉公式,任意包含 个顶点的凸多面体,所含的边不会超过 条,所含的小平面不会超过 张。
随机增量法#
算法思想#
和 Andrew 算法类似,考虑每次把 加入到前 个点的凸包中,也就是将 转化为 .
第一种情况: 在 的内部或边界上,则 .
第二种情况: 在 外部。设想你站在 所在的位置,看向 . 中的某些小平面会被看到,其余在背面的平面不会被看到。如下图,从 可见的平面构成了一片连通的区域。
这片区域由一条封闭折线围成,称这条线为 在 上的边界线 。
根据这条地平线,我们可以判断出,在原先 表面上的哪些部分需要被保留,哪些需要被替换。
显然,不可见的平面在 中被保留,并且我们用 与地平线之间连接出新的小平面,来替换所有可见的小平面,如下图。
判断平面对点的可见性#
如何用几何语言表达:一个平面对 是可见的?
对于一个凸包上的小平面,它能将空间分为两半,一侧是凸包外部,一侧是凸包内部。容易发现,如果点 位于小平面的外侧,那么这个平面对于 点就是可见的,因为凸包上的其它平面都不会有遮挡。
形式化地,记 为平面三角形的三个顶点,从凸包外部看,三点按照逆时针排列。
利用叉乘的性质,记 ,则结果向量 是一个平面的法向量,且指向凸包外部。
对于空间内任意一点 ,若 ,则这个平面对点 是可见的。
下面是定义在结构体 plane
中的函数,用于判断点 是否位于平面的外侧。
bool is_above(Point3 A) {
return (normal() & (A - p[v[0]])) >= 0;
}
cpp求出边界线#
要想把凸包从 转化为 ,我们需要准确地求出凸包上的哪些边在边界线上。求出边界线之后,才能用 与边界线构成的小平面替换需要被删掉的小平面。
定义 bool g[N][N]
, 表示 所在的平面是否可见。
若规定平面 只包含 ,则对于任意有序数对 ,向量 最多被包含在一个平面内。
注意到,位于边界线上的向量 一定满足 且 。所以我们只需对每个平面判断其可见性,并更新在 g[][]
中对应的数值,即可求出边界线。
利用边界线更新凸包#
在上一步遍历小平面时,若遇到的小平面是不可见的,则把它加入新的凸包中;若可见,则单独记录。
之后遍历所有可见的小平面,若 在边界线上,则把 加入凸包中。 是新加入凸包的点。这样加入后的三点也满足逆时针排列。
参考代码#
函数返回值为三维凸包的平面数,plane ret[]
的下标从 开始。
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旋转卡壳#
概述#
旋转卡壳算法用于:在线性时间内,求凸包直径、最小矩形覆盖等于凸包性质相关的问题。线性时间是指求出凸包之后的算法时间复杂度。
求凸包直径#
给定平面上的 个点,求所有点对之间的最长距离。.
首先,求出这 个点的凸包,复杂度可以做到为 ,如何求直径?
**暴力做法:**可以遍历每个点对,求出最大距离,复杂度为 .
算法流程#
可以遍历凸包上的边,对每条边 ,去找距离这条边最远的点 。对于 点,距离它最远的点,一定是 中的一个。
我们发现,若逆时针遍历凸包上的边,那么随着边的转动,对应的最远点也在逆时针旋转。
因此,我们可以在逆时针枚举边的同时,实时维护最远点,利用单调性,复杂度为 .

算法实现#
求凸包时,若使用 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最小矩形覆盖#
题意#
给定一些点的坐标,求能够覆盖所有点的最小面积的矩形。
求出矩形面积、顶点坐标。.
分析#
先求出凸包,之后用旋转卡壳维护三个边界点。
利用叉积和点积,可以求出矩形面积及四个顶点的坐标。
实现#
下面是一种可行的表示方法。
设 ,,.
则矩形面积为:
顶点坐标为:
若不需要求出顶点坐标,也可以这样表示矩形面积:
设 , 后易证。
代码#
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;
}
cppPOJ2079 - Triangle#
题意#
给定平面上的 个点,从其中任选三个点作为顶点,求能构成的最大三角形面积。
.
分析#
显然,三角形的顶点一定都在这 个点的凸包上,所以先求出凸包。
考虑旋转卡壳。
这题中不能固定一条边,再枚举另外两个点,因为三角形的边不一定在凸包上。
因此,先逆时针枚举一个固定点 ,再逆时针旋转另外两个顶点 和 .·
由于凸包上的一些单峰性,我们旋转 时停止的条件是 最大,停止后固定 ,以相同停止条件旋转 .
最大,是指 且 .
代码#
注意特判 以及凸包大小 的情况。用叉积求面积。
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半平面交#
半平面#
半平面是一条直线和直线的一侧,是一个点集。
当点集包含直线时,称为闭半平面;当不包含直线时,称为开半平面。
直线类#
在计算几何中,用 表示直线 ,一般统一保留向量的左侧半平面。
求两直线的交#
若交点存在,只需求出 ,则 .
注意到 ,证明也不难。
如上图,把 平移到 的位置,则可以把叉乘的模看作平行四边形面积。
记 ,,由于两个平行四边形同底,所以有 .
又因为 ,所以 .
参考代码#
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半平面交#
半平面交是多个半平面的交集。
半平面交是一个点集,并且是一个凸集。在直角坐标系上是一个区域。
半平面交在代数意义下,是若干个线性约束条件,每个约束条件形如:
其中 为常数,且 和 不都为零。
半平面交有下面五种可能情况,每个半平面位于边界直线的阴影一侧。
和 比较特殊,我们一般假设产生的交集总是有界或空的。
性质#
注意到,位于半平面交边界上的任何一个点,必定来自某张半平面的边界。
并且,因为半平面交是凸集,所以每条边界线最多只能为边界贡献一条边。
因此:由 张半平面相交而成的凸多边形,其边界最多由 条边围成。
Sort-and-Incremental Algorithm#
S&I 算法,排序增量法。
算法思想#
S&I 算法利用了性质:半平面交是凸集。
因为半平面交是凸集,所以我们维护凸壳。
若我们把半平面按极角排序,那么在过程中,只可能删除队首或队尾的元素,因此使用双端队列维护。
下面是一个例子。
TODO.
算法流程#
- 把半平面按照极角序排序,需要 的时间。
- 对每个半平面,执行一次增量过程,每次根据需要弹出双端队列的头部或尾部元素。这是线性的,因为每个半平面只会被增加或删除一次。
- 最后,通过双端队列中的半平面,在线性时间内求出半平面交(一个凸多边形,用若干顶点描述)。
这样,我们得到了一个时间复杂度为 的算法,瓶颈在于排序。因此,若题目给定的半平面满足极角序,则我们可以在线性的时间内求出半平面交。
极角排序#
注意判断不要写成 <=
或 >=
。
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;
}
cppP3256 [JLOI2013]赛车#
题目描述#
赛场上一共有 辆车,分别称为 .
赛道是一条无限长的直线。最初, 位于距离起跑线前进 的位置。比赛开始后,车辆 将会以 单位每秒的恒定速度行驶。
过程中,如果一辆赛车曾经处于领跑位置的话,即没有其他的赛车跑在他的前面,这辆赛车最后就可以得奖。
求哪些赛车将会得奖。输出赛车编号。
,,.
分析#
可以想到,每辆车的 图像都是一条直线。在平面直角坐标系上画出这些直线。
若取每条直线的左侧半平面,再与 轴上侧平面、 轴右侧平面一起求交,则所有凸多边形上的直线可以获奖。
直接跑排序增量法即可,时间复杂度 .
注意到这题 和 的值域均为 ,所以运算过程中的数值会达到 ,
因此需要使用 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;
}
cppP4250 [SCOI2015]小凸想跑步#
题目描述#
小凸晚上喜欢到操场跑步,今天他跑完两圈之后,他玩起了这样一个游戏。
操场是个凸 边形, 个顶点按照逆时针从 ∼ 编号。
现在小凸随机站在操场中的某个位置,标记为 点。将 点与 个顶点各连一条边,形成 个三角形。
如果这时 点, 号点, 号点形成的三角形的面积是 个三角形中最小的一个,则认为这是一次正确站位。
现在小凸想知道他一次站位正确的概率是多少。
分析#
推一点式子。
设 ,,,,.
由题意,
展开得,
合并同类项,
注意到,这是直线解析式的形式,于是转化为了半平面交问题。
一些资料#
- 题单链接:【2021专题分享】计算几何 - TonyYin - 洛谷 _ 计算机科学教育新生态 ↗
- 参考资料: BERG M, KREVELD M, OVERMARS M H. Computational Geometry: Algorithms and Applications[M]. 2008.
[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 ↗