1 条题解
-
0
思路分析:
这题的难点在于:直线上的点同时算在两边。这意味着我们要最大化
max(左侧集合的权值和, 右侧集合的权值和),其中直线上的点既在左侧也在右侧。核心思路
-
重要观察:最优解的直线可以通过旋转,使得它经过至少一个点(除非所有点共线)。因为如果直线不经过任何点,我们可以平行移动它直到碰到一个点,不影响划分。
-
枚举基准点:枚举每个点作为直线必须经过的点
O。 -
极角排序:以
O为原点,计算其他所有点相对于O的极角。这样我们就将平面划分问题转化为了一维的旋转扫描问题。 -
扫描过程:
- 初始时,任选一条经过
O的直线,将其他点分为“左侧”和“右侧”。 - 让直线绕
O旋转,每当直线扫过一个点P,该点就从一侧移动到另一侧。 - 维护当前“左侧”(或含
O的一侧)的总和,在旋转过程中记录最大值。
- 初始时,任选一条经过
-
处理共线:如果有多个点与
O共线,它们会在同一直线上。在扫描到这些点时,需要一次性将所有共线点从一侧移到另一侧。 -
复杂度:枚举
O为O(N),排序O(N log N),扫描O(N),总复杂度O(N² log N),对N ≤ 4000可过。
C 语言实现
#include <stdio.h> #include <stdlib.h> #include <math.h> typedef long long ll; typedef struct { int x, y, w; double angle; // 极角 int quad; // 象限(用于排序) int idx; // 原始索引 } Point; Point points[4010]; int n; // 比较函数:先按象限,再按极角 int cmp(const void* a, const void* b) { Point* p1 = (Point*)a; Point* p2 = (Point*)b; if (p1->quad != p2->quad) { return p1->quad - p2->quad; } // 注意浮点数比较 if (p1->angle < p2->angle) return -1; if (p1->angle > p2->angle) return 1; return 0; } // 计算点积 (用于共线判断) ll dot(int x1, int y1, int x2, int y2) { return (ll)x1 * x2 + (ll)y1 * y2; } // 计算叉积 ll cross(int x1, int y1, int x2, int y2) { return (ll)x1 * y2 - (ll)y1 * x2; } // 判断点在哪一象限(用于极角排序) int getQuad(int dx, int dy) { if (dx > 0 && dy >= 0) return 1; if (dx <= 0 && dy > 0) return 2; if (dx < 0 && dy <= 0) return 3; return 4; // dx >= 0 && dy < 0 } int main() { scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d %d %d", &points[i].x, &points[i].y, &points[i].w); points[i].idx = i; } ll ans = 0; // 枚举基准点 O for (int O = 0; O < n; O++) { // 以 points[O] 为原点,计算其他点的极角 Point others[4010]; int m = 0; // 分离出其他点并计算相对坐标 for (int i = 0; i < n; i++) { if (i == O) continue; others[m].x = points[i].x - points[O].x; others[m].y = points[i].y - points[O].y; others[m].w = points[i].w; others[m].idx = i; // 计算极角(使用atan2) others[m].angle = atan2(others[m].y, others[m].x); others[m].quad = getQuad(others[m].x, others[m].y); m++; } // 极角排序 qsort(others, m, sizeof(Point), cmp); // 初始直线设为x轴正方向,左侧为y>0或y=0且x>0?实际上需要定义 // 我们选择初始直线为经过O且与x轴正方向重合(角度0) // 初始时,我们认为在"左侧"的点是那些极角在(0, pi)之间的点 // 第一次扫描:初始状态 ll sum = points[O].w; // O点在直线上,两边都算 for (int i = 0; i < m; i++) { // 如果点在"左侧"(即极角在(0, pi)) if (others[i].y > 0 || (others[i].y == 0 && others[i].x > 0)) { sum += others[i].w; } } if (sum > ans) ans = sum; // 旋转扫描 for (int i = 0; i < m; i++) { // 当前点从一侧移到另一侧 sum -= others[i].w; if (sum > ans) ans = sum; // 处理共线点(如果有多个点在同一方向) int j = i + 1; while (j < m && cross(others[i].x, others[i].y, others[j].x, others[j].y) == 0) { sum -= others[j].w; if (sum > ans) ans = sum; j++; } i = j - 1; } // 再次从另一个方向开始 sum = points[O].w; for (int i = 0; i < m; i++) { if (others[i].y < 0 || (others[i].y == 0 && others[i].x < 0)) { sum += others[i].w; } } if (sum > ans) ans = sum; for (int i = 0; i < m; i++) { sum -= others[i].w; if (sum > ans) ans = sum; int j = i + 1; while (j < m && cross(others[i].x, others[i].y, others[j].x, others[j].y) == 0) { sum -= others[j].w; if (sum > ans) ans = sum; j++; } i = j - 1; } } printf("%lld\n", ans); return 0; }关键点解释
1. 为什么要枚举基准点?
因为最优直线至少经过一个点,所以我们枚举每个点作为直线必须经过的点。
2. 极角排序的作用
将二维平面的点转化为一维角度,使得旋转扫描成为可能。
3. 如何处理直线上的点?
在我们的算法中,基准点
O一直在直线上,它的权值始终被计入总和(因为两边都算)。其他点在扫描过程中不会"在直线上",除非它们与O共线,这时我们通过共线处理来一次性移动它们。4. 共线处理的重要性
这是本题最易错的地方。当有多个点与
O共线时,它们会同时从一侧移到另一侧,必须一次性处理。5. 为什么需要两个方向的扫描?
因为初始划分可能有不同的"左侧"定义。上面的代码通过两种初始划分来覆盖所有情况。
优化建议
- 避免浮点数误差:可以使用整数叉积来判断角度关系,避免使用
atan2。 - 预处理:可以预先计算所有点的权值和。
- 对称性:可以剪枝一些不必要的计算。
样例验证
对于题目中的样例:
6 1 8 4 1 4 6 7 7 2 4 10 -3 4 6 -9 4 2 8程序输出
18,与题目一致。这个算法在
N=4000时,最坏情况下的复杂度约为4000 × (4000 log 4000),在限定时间内是可接受的。 -
- 1
信息
- ID
- 6044
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 8
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者