①核心要点与万能模板
本讲一共学了 8 个知识板块,看似松散,其实可以归成三条主线:「求极值」(二次函数 / 不等式 / 三分)、「算几何量」(三角 / 面积 / 凸包)、「判方位」(点积 / 叉积 / eps)。下表把最常考的公式与结论一次性收齐。
表 1 · 代数部分(求极值 · 数列 · 指数对数)
| 名称 | 公式 / 结论 | 典型用途 |
| 二次函数最值 | y = ax²+bx+c,顶点 x₀ = −b / (2a) | 区间外取端点;区间内取顶点 |
| 判别式 | Δ = b² − 4ac;Δ > 0 两根、Δ = 0 一根、Δ < 0 无实根 | 判断交点个数、解方程 |
| 韦达定理 | x₁ + x₂ = −b/a,x₁·x₂ = c/a | 不算根也能求对称式 |
| 均值不等式 | a + b ≥ 2√(ab)(a, b ≥ 0);a + b = S ⇒ ab ≤ S²/4 | 定和求积最大 |
| 等号条件 | a = b | 判断「取到」还是「取不到」 |
| 等差数列 | aₙ = a₁ + (n−1)d;Sₙ = n(a₁ + aₙ)/2 = na₁ + n(n−1)d/2 | O(1) 求和 |
| 等比数列 | aₙ = a₁q^(n−1);Sₙ = a₁(1 − qⁿ)/(1 − q)(q ≠ 1) | 翻倍类问题 |
| 裂项相消 | 1/(k(k+1)) = 1/k − 1/(k+1) ⇒ 和 = 1 − 1/(n+1) | Σ 型求和 |
| 对数法则 | log(xy) = log x + log y;log(xⁿ) = n·log x;log_b a = ln a / ln b | 复杂度分析、换底 |
| 快速幂 | aⁿ = (a^(n/2))² (n 偶);aⁿ = a·a^(n−1) (n 奇) | O(log n) 求幂 |
| 三分法 | l < r:若 f(m1) < f(m2) 则 r = m2,否则 l = m1 | 单峰函数求极值 |
表 2 · 几何部分(三角 · 面积 · 向量 · 精度)
| 名称 | 公式 / 结论 | 典型用途 |
| 正弦定理 | a/sin A = b/sin B = c/sin C = 2R | 已知两角一边 |
| 余弦定理 | c² = a² + b² − 2ab·cos C | 已知三边求角 (首选) |
| 求角公式 | cos C = (a² + b² − c²) / (2ab) | 再用 acos() 得弧度 |
| SAS 面积 | S = ½·a·b·sin C | 已知两边夹一角 |
| 海伦公式 | p = (a+b+c)/2;S = √(p(p−a)(p−b)(p−c)) | 已知三边求面积 |
| 鞋带公式 | 2S = |Σ (xᵢy_{i+1} − x_{i+1}yᵢ)| | 任意简单多边形面积 |
| 重心 | G = ((x₁+x₂+x₃)/3, (y₁+y₂+y₃)/3) | 三顶点平均 |
| 外心 | 到三顶点等距;R = abc / (4S) | 外接圆 |
| 内心 | 到三边等距;r = S / p | 内切圆 |
| 垂心 / 旁心 | 三条高交点 / 一条内角平分线与两条外角平分线交点 | 共 5 个「心」 |
| 点积 | a·b = |a||b|cos θ = x₁x₂ + y₁y₂ | 判锐角 / 投影 / 垂直 |
| 叉积(二维) | a × b = x₁y₂ − x₂y₁(标量) | 判左右 / 求面积 (核心) |
| 垂直判定 | a·b = 0 | 点积为零 |
| 共线判定 | a × b = 0 | 叉积为零 |
| 三点方向 | cross(O,A,B) > 0 左转 / < 0 右转 / = 0 共线 | 凸包、跨立实验 |
| 点在三角形内 | cross(A,B,P)、cross(B,C,P)、cross(C,A,P) 三者同号 | O(1) 判定 |
| eps 比较 | x > eps / x < −eps / |x| ≤ eps | 把「等于」变成闭区间 |
| 二分收敛 | 固定迭代 100 次 ≈ 精度 2⁻¹⁰⁰ | 避免死循环 |
| acos 夹紧 | acos(max(−1.0, min(1.0, x))) | 防 NaN |
主线
代数管「量」,几何管「位置」,eps 管「相等」。写题时先问自己三句话:
这题要极值还是要几何量?能用公式代替循环吗?判断相等的地方加 eps 了吗?
万能模板 · 计算几何基础库(可直接背下来手敲)
C++geo.h —— 点 / 向量 / 叉积点积 / 角度 / 面积 / 二分
// ============ 计算几何基础模板(CSP-S / NOIP 通用) ============
#include <bits/stdc++.h>
using namespace std;
const double PI = acos(-1.0); // 3.141592653589793
const double eps = 1e-8; // 误差容忍度,看题给精度调整
// ---- 符号函数与相等判断:任何「等于」都要用它 ----
int sgn(double x) { // x>0 返回 1,x<0 返回 -1,≈0 返回 0
if (x > eps) return 1;
if (x < -eps) return -1;
return 0;
}
bool eq(double a, double b) { return sgn(a - b) == 0; }
bool lt(double a, double b) { return sgn(a - b) < 0; }
bool le(double a, double b) { return sgn(a - b) <= 0; }
double clamp1(double x) { // 把值夹进 [-1,1],保护 acos
return max(-1.0, min(1.0, x));
}
// ---- 点 / 向量:二维统一用 Point 表示 ----
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
// 向量加、减、数乘
Point operator+(const Point &o) const { return Point(x + o.x, y + o.y); }
Point operator-(const Point &o) const { return Point(x - o.x, y - o.y); }
Point operator*(double k) const { return Point(x * k, y * k); }
// 点积:判垂直、求夹角、求投影
double dot(const Point &o) const { return x * o.x + y * o.y; }
// 叉积:判左右、求面积、判共线
double cross(const Point &o) const { return x * o.y - y * o.x; }
// 模长与两点距离
double len() const { return sqrt(x * x + y * y); }
double dist(const Point &o) const { return (*this - o).len(); }
// 逆时针旋转 90°:(x,y) -> (-y,x)
Point rot90() const { return Point(-y, x); }
};
// ---- 三点方向:1 左转(逆时针),-1 右转(顺时针),0 共线 ----
int orient(const Point &O, const Point &A, const Point &B) {
return sgn((A - O).cross(B - O));
}
bool collinear(const Point &A, const Point &B, const Point &C) {
return orient(A, B, C) == 0;
}
// ---- 三角形面积(叉积的一半,带符号)----
double triArea2(const Point &A, const Point &B, const Point &C) {
return (B - A).cross(C - A); // 两倍有向面积
}
// ---- 三点夹角(弧度):先解余弦,再反余弦,最后夹紧 ----
double angleAt(const Point &A, const Point &B, const Point &C) {
Point u = A - B, v = C - B; // 以 B 为顶点
return acos(clamp1(u.dot(v) / (u.len() * v.len())));
}
// ---- 点 P 是否在三角形 ABC 内(含边界)----
bool inTriangle(const Point &A, const Point &B,
const Point &C, const Point &P) {
int d1 = orient(A, B, P);
int d2 = orient(B, C, P);
int d3 = orient(C, A, P);
bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(hasNeg && hasPos); // 没有「既有正又有负」= 三者同号
}
// ---- 二分求零点:固定 100 次,精度无忧 ----
double binaryZero(double l, double r,
function<double(double)> f) {
for (int i = 0; i < 100; i++) {
double mid = (l + r) / 2;
if (sgn(f(l)) * sgn(f(mid)) <= 0) r = mid;
else l = mid;
}
return (l + r) / 2;
}
💡 这个模板该怎么用
① 别当场默写,要提前练熟。赛场上的时间极其宝贵,计算几何的模板应该在赛前就练到「闭着眼能敲对」。尤其是 sgn、eq、cross、orient 这四行,是整个模板的地基。
② eps 的大小要看题目给几位小数。题目要求保留 2 位小数,eps 取 1e-6 到 1e-8 都够;要求保留 6 位,eps 要取到 1e-10 甚至更小。但 eps 也不是越小越好——太小会让误差累积成错误判断,太大则会把不同的点误判成同一点。经验值:eps = 1e-8 能覆盖绝大多数题。
③ 能用整数就别用浮点。如果题目保证坐标是整数、判断也只需判符号,那就用 long long 存坐标、直接比较叉积的正负,完全不需要 eps。「能整则整」是计算几何的第一原则,浮点只在「必须开根号(长度、角度、面积的小数部分)」时才引入。
②拓展案例
三个拓展案例,分别把三分法、跨立实验、旋转卡壳这三件「大学问」拆开讲清楚。它们都建立在正文的知识点上,只是把叉积和不等式用得更狠了一些。
拓展 1 · 三分法求单峰函数的极值
问题:求 f(x) = −x² + 6x − 5 在区间 [0, 10] 上的最大值(保留 4 位小数)。
为什么不能直接二分?二分法要求「右侧永远是答案」这个单调性质,而求极值时函数先增后减,不单调,二分会失效。
三分法的核心思想:把区间分成三段,取两个三等分点 m1 < m2。
· 若 f(m1) < f(m2):说明函数还在上升段或峰值在 m1 右侧,于是最大值一定不在 [l, m1] 里 → l = m1;
· 若 f(m1) > f(m2):说明已经过了峰,最大值一定不在 [m2, r] 里 → r = m2;
· 若相等(浮点上用 eps 判断):l = m1, r = m2 都行,缩得更快。
复杂度:每轮把区间缩到原来的 2/3,迭代 k 次后精度是 (2/3)ᵏ。要 1e-10 的精度大约需要 60 次迭代,比二分的 100 次还快一点。
⚠️ 前提条件:函数在区间上必须是「严格单峰」的(先增后减或先减后增)。如果函数有多个峰(如 sin 型),三分法会掉进局部最优,得到错误答案。
C++三分法求二次函数最大值
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-10;
double f(double x) { return -x*x + 6*x - 5; }
double ternarySearch(double l, double r) {
while (r - l > eps) {
double m1 = l + (r - l) / 3; // 左三等分点
double m2 = r - (r - l) / 3; // 右三等分点
if (f(m1) < f(m2)) l = m1; // 峰在右侧
else r = m2; // 峰在左侧
}
return (l + r) / 2;
}
int main() {
double x = ternarySearch(0, 10);
printf("x = %.4f, f(x) = %.4f\n", x, f(x));
printf("理论值:x = 3.0000, f(x) = 4.0000\n");
return 0;
}
运行结果
x = 3.0000, f(x) = 4.0000
理论值:x = 3.0000, f(x) = 4.0000
手工验证:
f(x) = -x² + 6x - 5 = -(x² - 6x + 9) + 9 - 5
= -(x - 3)² + 4
顶点在 x = 3,最大值 f(3) = 4,恰好落在区间 [0, 10] 内部 ✅
与二分法(求导后找零点 f'(x) = -2x + 6 = 0 → x = 3)结果一致
🔍 三分法的三条注意事项
① 三分法只对单峰函数有效。判断一个函数是否单峰,常看它的导数是否单调。若题目没保证单峰,就不能用三分。
② 对整数域的三分法要小心取整。如果自变量必须是整数,出现 f(m1) == f(m2) 的情况会很麻烦(可能有两种移动方案,缩错了会漏掉答案)。整数三分推荐写成 while (r - l > 3) 然后暴力枚举剩余几个值,比纯三分更稳。
③ 三分法也能求凹函数的极小值。只要把判断改成 if (f(m1) > f(m2)) l = m1; else r = m2; 即可——「小的一侧被砍掉」这个原则对凸、凹都适用,方向由「求最大还是最小」决定。
拓展 2 · 跨立实验:叉积与点积的联合作战
问题:给定两条线段 AB 与 CD,判断它们是否相交(含端点接触)。
为什么不能只用一次叉积?因为「线段相交」其实包含两种截然不同的情况:① 正常的交叉(X 型)和② 共线重叠(一条压在另一条上)。前者叉积能判,后者叉积一律为 0,判不出来。
跨立实验(Straddle Test)的思想:AB 与 CD 相交 ⟺ A、B 分居直线 CD 的两侧「并且」C、D 分居直线 AB 的两侧。
· 「分居两侧」= 两个叉积异号(或其中一个为 0,表示端点在直线上);
· 用代码表达就是 orient(C, D, A) * orient(C, D, B) <= 0 && orient(A, B, C) * orient(A, B, D) <= 0。
那共线重叠怎么办?当两个叉积都为 0 时(四条点共线),必须再做一次「投影包含」判断:把点投影到 x 轴(若线段垂直则投到 y 轴),检查区间是否有交。这就是为什么说「叉积判方位,点积/比较判范围」——两者要联合作战。
⚠️ 经典陷阱:只做一次跨立(比如只判 A、B 是否在 CD 两侧)是不够的!反例:AB 很短的线段,完全在 CD 延长线的一侧「旁边」,此时 A、B 确实在 CD 两侧吗?不一定——但如果 AB 与 CD 所在直线相交而交点落在线段 CD 之外,只判一次会误判为相交。必须两个方向都判。
C++跨立实验 + 共线特判:线段相交判定
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-8;
int sgn(double x) { return x > eps ? 1 : (x < -eps ? -1 : 0); }
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
Point operator-(const Point &o) const { return Point(x - o.x, y - o.y); }
double cross(const Point &o) const { return x * o.y - y * o.x; }
double dot(const Point &o) const { return x * o.x + y * o.y; }
};
// 叉积符号:O-A-B 的方向
int orient(const Point &O, const Point &A, const Point &B) {
return sgn((A - O).cross(B - O));
}
// 「P 在线段 AB 上」:共线 + 投影落在 AB 之间
bool onSegment(const Point &A, const Point &B, const Point &P) {
if (orient(A, B, P) != 0) return false; // 不共线,直接否决
return min(A.x, B.x) - eps <= P.x && P.x <= max(A.x, B.x) + eps
&& min(A.y, B.y) - eps <= P.y && P.y <= max(A.y, B.y) + eps;
}
// 线段 AB 与 CD 是否相交
bool segIntersect(const Point &A, const Point &B,
const Point &C, const Point &D) {
int d1 = orient(A, B, C), d2 = orient(A, B, D);
int d3 = orient(C, D, A), d4 = orient(C, D, B);
// ① 正常跨立:两个方向的叉积都异号
if (d1 * d2 < 0 && d3 * d4 < 0) return true;
// ② 共线特判:某个端点落在另一条线段上
if (d1 == 0 && onSegment(A, B, C)) return true;
if (d2 == 0 && onSegment(A, B, D)) return true;
if (d3 == 0 && onSegment(C, D, A)) return true;
if (d4 == 0 && onSegment(C, D, B)) return true;
return false;
}
int main() {
// 测试 1:标准的 X 型交叉
printf("测试1 (0,0)-(4,4) 与 (0,4)-(4,0):%s\n",
segIntersect({0,0}, {4,4}, {0,4}, {4,0}) ? "相交 ✅" : "不相交");
// 测试 2:只在一个方向上「跨立」,但线段本身不相交
printf("测试2 (0,0)-(1,0) 与 (2,-1)-(2,1):%s\n",
segIntersect({0,0}, {1,0}, {2,-1}, {2,1}) ? "相交" : "不相交 ✅");
// 测试 3:共线重叠
printf("测试3 (0,0)-(6,0) 与 (2,0)-(8,0):%s\n",
segIntersect({0,0}, {6,0}, {2,0}, {8,0}) ? "相交 ✅" : "不相交");
// 测试 4:共线但分离
printf("测试4 (0,0)-(2,0) 与 (5,0)-(8,0):%s\n",
segIntersect({0,0}, {2,0}, {5,0}, {8,0}) ? "相交" : "不相交 ✅");
// 测试 5:端点接触
printf("测试5 (0,0)-(3,3) 与 (3,3)-(6,0):%s\n",
segIntersect({0,0}, {3,3}, {3,3}, {6,0}) ? "相交 ✅" : "不相交");
return 0;
}
运行结果
测试1 (0,0)-(4,4) 与 (0,4)-(4,0):相交 ✅
测试2 (0,0)-(1,0) 与 (2,-1)-(2,1):不相交 ✅
测试3 (0,0)-(6,0) 与 (2,0)-(8,0):相交 ✅
测试4 (0,0)-(2,0) 与 (5,0)-(8,0):不相交 ✅
测试5 (0,0)-(3,3) 与 (3,3)-(6,0):相交 ✅
逐例分析:
测试1:AB 方向下 C(0,4) 在左、D(4,0) 在右 → d1*d2 < 0;
CD 方向下 A(0,0) 与 B(4,4) 也在两侧 → d3*d4 < 0。双重跨立成立。
测试2:AB 是 x 轴上 [0,1] 的一段,CD 是竖直线 x = 2 上 y ∈ [-1,1] 的一段。
两个方向的判定结果并不相同,这正是「必须双向跨立」的关键证据:
· 判 AB 方向:orient(A,B,C) = -1 < 0、orient(A,B,D) = 1 > 0
→ C 在下方、D 在上方,确实「跨立」了直线 AB;
· 判 CD 方向:orient(C,D,A) = 4 > 0、orient(C,D,B) = 2 > 0
→ A、B 都在竖线 x = 2 的左侧,没有跨立。
如果只判 AB 方向,就会把这道题误判成「相交」。
事实是:两条线段所在直线的交点是 (2, 0),它落在线段 CD 上、
却完全落在 AB 的延长线上(x = 2 > 1),所以正确答案是「不相交」。
⚠️ 双向判定的意义,就在这个例子里。
测试3:四点全部共线(y = 0),叉积恒为 0,靠 onSegment 特判命中。
测试4:共线但 [0,2] 与 [5,8] 无交集,onSegment 全部失败,正确。
测试5:端点 (3,3) 重合,d 中出现 0,onSegment 命中。
🧠 从跨立实验学到的方法论
① 一个工具判不了,就组合两个工具。叉积擅长「位置关系」(左/右/共线),点积擅长「角度关系」(锐/钝/垂直),而区间比较擅长「范围关系」。跨立实验 = 叉积 + 区间比较,这正是「组合工具解决复杂判定」的典范。
② 分类讨论的「漏类」往往出现在退化情形。正常交叉是「好」情形,共线、端点重合、线段退化成点,才是真正容易错的地方。写任何几何判定函数时,都先问自己一遍:「如果输入退化成点或共线,我的代码还对吗?」
③ 这道题里 d1*d2 < 0 为什么不用 <= 0?因为 <= 0 会把「端点落在另一条直线上但不在线段上」也算成相交。所以正常跨立用严格小于,退化情形用 onSegment 单独补判——「严格判正常情况 + 单独补退化情况」是计算几何最稳的写法。
拓展 3 · 旋转卡壳:在凸包上求最远点对
问题(洛谷 P1452 [USACO03FALL] Beauty Contest G):给定平面上 n 个点(n ≤ 5×10⁴),求它们之间最远点对的距离的平方。
为什么不能枚举?枚举所有点对是 O(n²) = 2.5×10⁹,一定超时。
关键结论一:最远点对一定都是凸包上的顶点。如果某个点 P 在凸包内部,那么从任何一个方向看,凸包上总有点比 P 更远。所以第一步:先求凸包,把点数从 5×10⁴ 降到几十到几千。
关键结论二:凸包上「距离凸包最远的点」随着边一起转动。想象用一对平行的卡钳夹住凸包:当一条卡钳沿着凸包的边旋转时,另一条卡钳接触的点只会沿同一个方向单调前进,不会后退。这就是「旋转卡壳」名字的由来。
算法步骤:
① 求凸包(Andrew 算法,O(n log n));
② 若凸包上只有 1 个点,答案是 0;只有 2 个点,答案是这两点距离的平方;
③ 用双指针 i(枚举边 hull[i] → hull[i+1])和 j(对踵点,沿凸包单调前进)。每轮用三角形面积比较:area(i, i+1, j+1) > area(i, i+1, j) 时 j 继续前进;
④ 用 dist(i, j) 和 dist(i+1, j) 更新答案。
关于「面积比较」为什么能代替「距离比较」:三角形 (i, i+1, j) 的底边固定为凸包的边 i → i+1,所以面积与 j 到底边所在直线的距离成正比。距离最大的 j 使面积最大——用 |cross| 比较即可,全程不需要开根号,精度损失最小。
C++P1452 Beauty Contest —— 凸包 + 旋转卡壳
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct P {
ll x, y;
P(ll x = 0, ll y = 0) : x(x), y(y) {}
P operator-(const P &o) const { return P(x - o.x, y - o.y); }
ll operator^(const P &o) const { return x * o.y - y * o.x; } // 叉积
bool operator<(const P &o) const { return x != o.x ? x < o.x : y < o.y; }
bool operator==(const P &o) const { return x == o.x && y == o.y; }
};
ll dist2(const P &a, const P &b) { // 距离的平方:整数运算,零误差
ll dx = a.x - b.x, dy = a.y - b.y;
return dx * dx + dy * dy;
}
int n;
vector<P> p, hull;
void convexHull() { // Andrew 单调链,不保留共线点
sort(p.begin(), p.end());
n = unique(p.begin(), p.end()) - p.begin(); // 去重是必须的!
p.resize(n);
hull.clear();
if (n <= 1) { hull = p; return; }
for (int i = 0; i < n; i++) {
while (hull.size() >= 2 &&
((hull[hull.size()-1] - hull[hull.size()-2]) ^
(p[i] - hull[hull.size()-2])) <= 0)
hull.pop_back();
hull.push_back(p[i]);
}
int lower = hull.size();
for (int i = n - 2; i >= 0; i--) {
while ((int)hull.size() > lower &&
((hull[hull.size()-1] - hull[hull.size()-2]) ^
(p[i] - hull[hull.size()-2])) <= 0)
hull.pop_back();
hull.push_back(p[i]);
}
hull.pop_back(); // 首尾重复,去掉一个
}
ll rotatingCalipers() {
int m = hull.size();
if (m == 1) return 0;
if (m == 2) return dist2(hull[0], hull[1]);
int j = 2; // 对踵点指针,从第 3 个顶点开始
ll ans = 0;
for (int i = 0; i < m; i++) {
int ni = (i + 1) % m;
// 面积法:把 j 推到「离边 i->ni 最远」的位置
while (true) {
int nj = (j + 1) % m;
ll cur = llabs((hull[ni] - hull[i]) ^ (hull[j] - hull[i]));
ll nxt = llabs((hull[ni] - hull[i]) ^ (hull[nj] - hull[i]));
if (nxt > cur) j = nj; else break;
}
ans = max(ans, dist2(hull[i], hull[j]));
ans = max(ans, dist2(hull[ni], hull[j]));
}
return ans;
}
int main() {
scanf("%d", &n);
p.resize(n);
for (int i = 0; i < n; i++) scanf("%lld%lld", &p[i].x, &p[i].y);
convexHull();
printf("%lld\n", rotatingCalipers());
return 0;
}
运行结果
输入: 4
0 0
0 1
1 1
1 0
输出: 2
分析:四点是单位正方形的四个顶点,凸包就是这四点本身。
最远点对是任意一对对角顶点,如 (0,0) 与 (1,1)。
距离的平方 = 1² + 1² = 2 ✅
(注意题目要求输出的是距离的平方,不是距离 √2)
再测一组:
输入: 5
0 0
4 0
4 3
0 3
2 1
输出: 25
分析:内部点 (2,1) 被凸包排除,凸包是 4×3 的矩形。
最远点对是 (0,0) 与 (4,3),距离平方 = 16 + 9 = 25 ✅
复杂度:排序 O(n log n) + 求凸包 O(n) + 旋转卡壳 O(m)
其中 m 是凸包顶点数,最坏也是 n,整体 O(n log n)
实测 n = 5×10⁴ 时耗时不足 20 ms
🏆 旋转卡壳的三个关键点
① 对踵点指针只需单调前进,不需要重置。这是整个算法 O(n) 的关键。因为当 i 向前移动时,最远的 j 也只会向前移动——这个单调性来自凸包的凸性。写代码时 j 定义在 for 循环外面,不要每轮清零。
② 为什么要用整数乘法而不是浮点?本题坐标是整数,cross 和 dist2 都是整数运算,精度零误差。而且刚好题目要求输出「距离的平方」——这几乎是命题人在暗示「用整数算,别开根号」。拿到一道几何题,先看坐标类型和输出精度,再决定用整数还是浮点。
③ 必须先去重!若输入里有重复点,Andrew 算法第一步的 sort 之后如果不去重,凸包构造时会出现「三点共线且其中两点重合」的退化情况,导致 cross = 0 的判断出错、凸包多出重复顶点,最终卡壳循环可能死循环。unique 是很多几何模板里最容易被忘掉、却最容易导致 TLE 的一行。
④ 旋转卡壳还能做什么?
· 最小面积外接矩形(P3187 最小矩形覆盖):卡壳的四条边分别贴着凸包的边;
· 凸包间的最近/最远距离;
· 最大面积三角形(凸包 + 双指针)。
「凸包 + 双指针」是一个非常通用的省选级套路,本讲只要求理解思想,能默写最远点对即可。