Flower Dance - 题解
标签与难度
标签: 计算几何, 极角排序, 双指针, 叉积, 组合数学, 思维转换 难度: 2100
题目大意喵~
各位Master,晚上好喵~ 今天我们遇到的这道题,是在一个二维平面上进行的“花之舞”呐。
墙上有 个不同的点,每个点都有自己的坐标 。LZR小哥发现了一个秘密:只要四个不同的点中,存在一个点严格位于另外三个点组成的非退化三角形内部,这四个点就能组成一朵美丽的“花”。
我们的任务就是,帮Gromah和LZR算出,这 个点中,一共能组成多少朵这样的“花”呢?也就是找出满足条件的四元组 的数量,喵~
解题思路分析
这道题的核心是识别一种特殊的四点共面几何构型,然后计数。一只我的直觉告诉我,直接去枚举所有四个点的组合肯定是不行的说!
1. 初步分析与思维转换
最朴素的想法是,枚举所有四个点的组合。从 个点中选出4个,有 种方式。对于每种组合 ,我们再依次检查:D是否在 内?C是否在 内?... 这样做的时间复杂度是 ,对于 来说,肯定是会超时的,喵呜~
所以,我们需要换个角度来思考这个问题!
一个“花”的定义是“一个点在另外三个点组成的三角形内部”。这其实给了我们一个非常重要的几何直观:这四个点的凸包是一个三角形,而不是一个四边形!
我们可以不直接去数“花”的数量,而是换一个等价的计数对象。一朵“花” ,假设是点 在 内部,那么这个组合可以唯一地表示为 (点D, 三角形ABC) 这个配对。
于是,问题就巧妙地转化为了: 求所有“点P在三角形T内部”的配对 (P, T) 的总数量。
我们可以遍历每一个点,让它作为那个“内部点”,然后计算有多少个由其他点组成的三角形包含了它。把所有点作为“内部点”时的情况加起来,就是最终的答案啦!
2. “我”在三角形里面吗?—— 子问题的解决
现在,我们的问题变成了:固定一个点 (我们称之为原点),在剩下的 个点中,可以组成多少个三角形,能够把点 包含在内部呢?
直接计算“包含”的三角形数量似乎有点困难。不过,我的智慧告诉我们,正难则反!我们可以计算有多少个三角形不包含点 ,然后用总的三角形数量减去它。
剩下的 个点,总共可以组成 个三角形。
那么,一个三角形 在什么情况下不包含原点 呢? 为了方便分析,我们进行一个坐标变换,将点 平移到坐标系的原点 ,其他所有点 也相应地平移为向量 。
此时,如果 不包含原点,当且仅当向量 都位于同一个以原点为顶点的、张角小于 的角(也就是同一个半平面)内。

上图中,左边的 就不包含原点,因为三个顶点都在一个半平面里。而右边的 就包含了原点。
3. 极角排序与双指针
如何高效地统计出所有顶点都在同一个半平面的“坏三角形”数量呢?这就要用到计算几何中的经典武器——极角排序和双指针啦!
对于固定的原点 ,我们把剩下的 个点(现在是向量了)按照绕 点的极角进行逆时针排序。排好序的向量我们记为 (其中 )。
现在,我们来数“坏三角形”的数量。我们可以枚举其中一个顶点 ,然后看有多少个三角形 是“坏”的。为了不重复计数,我们只考虑 在 之后的情况(按逆时针顺序)。
一个三角形 是坏的,意味着 在同一个半平面。由于我们已经排好序了,如果我们固定 ,那么所有和 构成“坏三角形”的另外两个顶点 一定都在从 开始逆时针旋转 的范围内。
我们可以用双指针来高效地完成这个统计:
- 为了处理环状结构(比如 和 也能组成三角形),我们把排好序的向量数组复制一份接在后面。
- 我们用一个指针
i遍历 到 ,作为“坏三角形”的第一个顶点。 - 另一个指针
j从i+1开始向后移动。只要 还在以 为起始边的半平面内,j就继续前进。 - 如何判断 是否在 的逆时针 范围内呢?我们可以用叉积!只要 ,就说明 在 的逆时针方向,并且夹角小于 。(叉积等于0表示共线,也要特殊处理)。
- 当
j移动到不能再移动时,从i的下一个点到j的前一个点,这些点(假设有count个)都和i位于同一个半平面。从这count个点中任意选择两个,和i组合,都能形成一个“坏三角形”。数量就是 。 - 我们将
i遍历一遍,把所有计算出的“坏三角形”数量加起来,就得到了以 为原点时所有“坏三角形”的总数。
因为指针 j 在整个过程中只会单调向后移动,所以对于每个 i,我们不需要重置 j,这使得内层循环的总时间复杂度是 的。
4. 算法总览
总结一下,我们的完整算法是这样的喵~
- 初始化总花朵数
total_flowers = 0。 - 外层循环:遍历 个点中的每一个点
P_k,让它当一次假想的“内部点” 。 - 内层计算: a. 创建一个包含 个向量的列表,每个向量是 (对于所有 )。 b. 对这 个向量进行极角排序。 c. 使用双指针法,计算出有多少个“坏三角形”(顶点都在同一个半平面)。记为
bad_triangles。 d. 总的三角形数是 。那么包含原点 的“好三角形”数量就是 。 e. 将这个数量累加到total_flowers。 - 循环结束后,
total_flowers就是我们要求的答案啦!
这个算法的复杂度是多少呢?外层循环 次,内层排序是 ,双指针是 。所以总时间复杂度是 ,对于 来说是完全可以接受的!
代码实现
这是我根据上面的思路,精心重构的一份代码,希望能帮助Master更好地理解呐!
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
// 定义点的结构体,喵~
struct Point {
long long x, y;
// 重载减法运算符,方便计算向量
Point operator-(const Point& other) const {
return {x - other.x, y - other.y};
}
};
// 计算两个向量的叉积,用来判断方向和角度
long long cross_product(Point a, Point b) {
return a.x * b.y - a.y * b.x;
}
// 组合数 C(n, k) 的计算,这里我们只需要 C(n, 3)
long long combinations(int n, int k) {
if (k < 0 || k > n) {
return 0;
}
if (k == 0 || k == n) {
return 1;
}
if (k > n / 2) {
k = n - k;
}
long long res = 1;
for (int i = 1; i <= k; ++i) {
res = res * (n - i + 1) / i;
}
return res;
}
int main() {
// 使用 stdio 可以更快地读写,喵~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n;
std::cin >> n;
std::vector<Point> points(n);
for (int i = 0; i < n; ++i) {
std::cin >> points[i].x >> points[i].y;
}
if (n < 4) {
std::cout << 0 << std::endl;
return 0;
}
long long total_flowers = 0;
// 1. 外层循环:枚举每个点作为可能的“内部点” O
for (int i = 0; i < n; ++i) {
Point origin = points[i];
std::vector<Point> others;
// 2. 创建相对于原点 O 的向量
for (int j = 0; j < n; ++j) {
if (i == j) continue;
others.push_back(points[j] - origin);
}
// 3. 极角排序
// 排序规则:
// a. 先按半平面分,上半平面(y>0 或 y=0,x>0)的点在前
// b. 同一半平面内的点,按叉积(逆时针)排序
std::sort(others.begin(), others.end(), [](Point a, Point b) {
bool a_is_upper = (a.y > 0 || (a.y == 0 && a.x > 0));
bool b_is_upper = (b.y > 0 || (b.y == 0 && b.x > 0));
if (a_is_upper != b_is_upper) {
return a_is_upper;
}
return cross_product(a, b) > 0;
});
// 4. 双指针计算“坏三角形”数量
long long bad_triangles = 0;
int m = others.size();
if (m < 3) continue;
int p2 = 1;
for (int p1 = 0; p1 < m; ++p1) {
// p2 指针从 p1 的下一个开始,如果它在 p1 的半平面内就前进
// 为了处理循环,p2 可能小于 p1
if (p1 == p2) p2 = (p2 + 1) % m;
while (p1 != p2 && cross_product(others[p1], others[p2]) > 0) {
p2 = (p2 + 1) % m;
}
// 从 p1 到 p2 之间的点(不含 p1, p2)有 count 个
// 它们与 p1 都在同一个半平面
int count = (p2 - p1 - 1 + m) % m;
bad_triangles += combinations(count, 2);
}
// 5. 计算“好三角形”的数量并累加
long long total_possible_triangles = combinations(m, 3);
long long good_triangles = total_possible_triangles - bad_triangles;
total_flowers += good_triangles;
}
std::cout << total_flowers << std::endl;
return 0;
}复杂度分析
时间复杂度:
- 我们有一个外层循环,迭代 次,每次选择一个点作为原点。
- 在循环内部,最耗时的部分是极角排序,对 个点进行排序,时间复杂度为 。
- 随后的双指针扫描部分,两个指针
p1和p2在整个内层循环中都只绕一圈,所以是线性的 。 - 因此,总的时间复杂度是 ,这对于题目给定的数据范围是足够快的,喵~
空间复杂度:
- 在每次外层循环中,我们需要一个额外的
std::vector来存储 个向量。所以,我们使用的额外空间与 成正比。
- 在每次外层循环中,我们需要一个额外的
知识点总结
这道题是计算几何领域一道很好的练习题,它融合了多种思想和技巧,值得好好回味一下呐!
- 思维转换: 这是解题的关键!将一个看似困难的直接计数问题(数四元组),转化为一个更容易处理的等价问题(数“点-三角形”配对)。
- 补集思想: 当直接计算“好”的组合很困难时,尝试计算“坏”的组合,然后用总量去减。在这里就是用总三角形数减去不包含原点的三角形数。
- 极角排序: 计算几何中的基本操作。通过将点按角度排序,可以将几何问题转化为序列问题,从而使用更多序列上的算法(如双指针)。要注意使用基于叉积的整数比较,避免
atan2带来的精度和速度问题。 - 叉积的应用: 叉积是二维几何的灵魂!它可以判断三个点的转向(共线、顺时针、逆时针),从而判断点与线的相对位置,以及向量间的夹角关系。
- 双指针: 在排好序的(环形)数组上,使用双指针可以高效地统计满足特定条件的区间或配对数量,将 的暴力枚举优化到 。
希望这篇题解能帮到你,Master!如果还有不明白的地方,随时可以再来问这只我哦,喵~