1. 题目背景与需求解析
P6149 [USACO20FEB] Triangles S 是美国计算机奥林匹克竞赛(USACO)2020年2月银组的一道经典题目。这道题考察的是对平面几何和算法优化的综合运用能力,特别适合用来训练二维坐标系下的数据处理技巧。
题目给定N个不同的整数坐标点(x,y),要求找出所有可能由这些点组成的直角三角形(满足勾股定理),并计算这些三角形面积之和。最终结果需要对10^9+7取模。数据范围是3≤N≤10^5,坐标绝对值不超过10^4。
注意:题目中的直角三角形要求直角边必须与坐标轴平行,这是解题的关键限制条件
2. 核心算法设计思路
2.1 暴力解法与优化方向
最直观的暴力解法是三重循环枚举所有可能的三元组,检查是否满足直角条件,然后计算面积。但这样的时间复杂度是O(N^3),对于N=10^5的数据量完全不可行。
优化方向很明确:我们需要找到一种方法,能够快速统计出与每个点形成直角三角形的其他点对。具体来说,对于任意点p(x,y),我们需要:
- 找出与p在同一水平线上(y相同)的其他点
- 找出与p在同一垂直线上(x相同)的其他点
- 这些点对与p组合就能形成符合条件的直角三角形
2.2 预处理数据结构设计
为了实现高效查询,我们需要预处理两个哈希表:
x_map:以x坐标为键,存储所有具有相同x坐标的点的y坐标集合y_map:以y坐标为键,存储所有具有相同y坐标的点的x坐标集合
在C++中可以用unordered_map<int, vector<int>>来实现。预处理的时间复杂度是O(N)。
2.3 面积计算数学推导
对于点p(x0,y0),假设在同一水平线上有m个点,在同一垂直线上有n个点。那么这些点与p组成的三角形面积总和为:
总面积 = Σ (|x_i - x0| * |y_j - y0|) / 2 = (Σ|x_i - x0|) * (Σ|y_j - y0|) / 2这个推导将O(m*n)的计算简化为O(m+n),是算法优化的关键。
3. C++实现详解
3.1 数据结构定义与输入处理
#include <iostream> #include <vector> #include <unordered_map> #include <algorithm> using namespace std; const int MOD = 1e9 + 7; int main() { int N; cin >> N; vector<pair<int, int>> points(N); unordered_map<int, vector<int>> x_map, y_map; for(int i = 0; i < N; ++i) { int x, y; cin >> x >> y; points[i] = {x, y}; x_map[x].push_back(y); y_map[y].push_back(x); } // 对每个坐标轴上的点进行排序 for(auto& [x, ys] : x_map) sort(ys.begin(), ys.end()); for(auto& [y, xs] : y_map) sort(xs.begin(), xs.end()); // 后续处理... }3.2 前缀和预处理
为了快速计算绝对差之和,我们需要为每个坐标轴上的点序列计算前缀和:
// 在排序后计算前缀和 unordered_map<int, vector<long long>> x_prefix, y_prefix; for(auto& [x, ys] : x_map) { int m = ys.size(); vector<long long> prefix(m+1, 0); for(int i = 0; i < m; ++i) prefix[i+1] = prefix[i] + ys[i]; x_prefix[x] = prefix; } for(auto& [y, xs] : y_map) { int n = xs.size(); vector<long long> prefix(n+1, 0); for(int i = 0; i < n; ++i) prefix[i+1] = prefix[i] + xs[i]; y_prefix[y] = prefix; }3.3 面积计算实现
对于每个点,计算其贡献的面积:
long long total = 0; for(auto& [x, y] : points) { auto& ys = x_map[x]; // 同一垂直线上的y坐标 auto& xs = y_map[y]; // 同一水平线上的x坐标 int m = ys.size(), n = xs.size(); if(m < 2 || n < 2) continue; // 无法形成三角形 auto& y_pre = x_prefix[x]; auto& x_pre = y_prefix[y]; // 计算y方向的绝对差之和 long long sum_y = 0; int pos_y = lower_bound(ys.begin(), ys.end(), y) - ys.begin(); sum_y = y * pos_y - y_pre[pos_y]; sum_y += (y_pre[m] - y_pre[pos_y+1]) - y * (m - pos_y - 1); // 计算x方向的绝对差之和 long long sum_x = 0; int pos_x = lower_bound(xs.begin(), xs.end(), x) - xs.begin(); sum_x = x * pos_x - x_pre[pos_x]; sum_x += (x_pre[n] - x_pre[pos_x+1]) - x * (n - pos_x - 1); total = (total + sum_x * sum_y) % MOD; } cout << total % MOD << endl;4. 算法复杂度分析
预处理阶段:
- 输入处理:O(N)
- 排序:O(N log N)(每个坐标轴上的点需要排序)
- 前缀和计算:O(N)
计算阶段:
- 每个点的处理:O(log m + log n)(二分查找)
- 总体:O(N log N)
因此总时间复杂度是O(N log N),完全能够处理N=10^5的数据规模。
5. 常见问题与调试技巧
5.1 边界条件处理
- 当某个坐标轴上只有1个点时(无法形成三角形),需要跳过处理
- 前缀和数组的大小要比坐标点多1(从0开始)
- 取模运算要在每次累加后进行,防止溢出
5.2 调试技巧
- 小规模测试用例验证:
3 0 0 0 1 1 0预期输出:1(面积为0.5,乘以2个三角形)
- 检查排序是否正确
- 验证前缀和计算是否正确
- 打印中间计算结果定位问题
5.3 性能优化
- 使用
reserve预分配哈希表空间减少rehash - 使用
emplace_back替代push_back - 在竞赛中可以用
scanf/printf替代cin/cout提升IO速度
6. 扩展思考
这道题还可以进一步扩展:
- 如果不限制直角边平行于坐标轴,该如何解决?
- 如果要求统计所有直角三角形的数量而非面积和,代码如何修改?
- 如果坐标范围扩大到10^9,但N仍然在10^5,算法需要如何调整?
在实际编码比赛中,这类几何题的关键往往在于:
- 发现问题的特殊性质(如本题直角边必须平行坐标轴)
- 将暴力解法通过数学推导转化为可高效计算的形式
- 使用合适的数据结构加速查询
我个人的经验是,USACO的银组题目通常需要将时间复杂度从O(N^2)优化到O(N log N)才能通过,因此识别问题的可优化点是关键。这道题就是一个很好的训练素材,建议反复练习直到能够独立完成。