1 条题解

  • 0
    @ 2025-12-10 21:20:02

    思路分析:

    这题的难点在于:直线上的点同时算在两边。这意味着我们要最大化 max(左侧集合的权值和, 右侧集合的权值和),其中直线上的点既在左侧也在右侧

    核心思路

    1. 重要观察:最优解的直线可以通过旋转,使得它经过至少一个点(除非所有点共线)。因为如果直线不经过任何点,我们可以平行移动它直到碰到一个点,不影响划分。

    2. 枚举基准点:枚举每个点作为直线必须经过的点 O

    3. 极角排序:以 O 为原点,计算其他所有点相对于 O 的极角。这样我们就将平面划分问题转化为了一维的旋转扫描问题。

    4. 扫描过程

      • 初始时,任选一条经过 O 的直线,将其他点分为“左侧”和“右侧”。
      • 让直线绕 O 旋转,每当直线扫过一个点 P,该点就从一侧移动到另一侧。
      • 维护当前“左侧”(或含 O 的一侧)的总和,在旋转过程中记录最大值。
    5. 处理共线:如果有多个点与 O 共线,它们会在同一直线上。在扫描到这些点时,需要一次性将所有共线点从一侧移到另一侧。

    6. 复杂度:枚举 OO(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. 为什么需要两个方向的扫描?

    因为初始划分可能有不同的"左侧"定义。上面的代码通过两种初始划分来覆盖所有情况。

    优化建议

    1. 避免浮点数误差:可以使用整数叉积来判断角度关系,避免使用 atan2
    2. 预处理:可以预先计算所有点的权值和。
    3. 对称性:可以剪枝一些不必要的计算。

    样例验证

    对于题目中的样例:

    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
    上传者