Target Practice II

USACO 2024 Feb Silver

题解与算法分析

难度评估: 普及+/提高 (绿/蓝)

1. 题目大意

  • 给定 $N$ 个矩形目标 ($x_1, x_2, y_1, y_2$)。
  • 给定 $4N$ 个斜率 $s$。
  • 目标: 确定 4N 头牛在 $y$ 轴上的位置,每头牛有它的斜率$s[i]$,向一个矩形的角射击。
  • 约束: 子弹不能穿过矩形内部。
  • 求: 使得 $\max(y_{cow}) - \min(y_{cow})$ 最小。
Target

2. 几何约束转化

牛在 $y_c$,目标点 $(x, y)$,斜率 $s$。

方程:$$y_c = y - s \cdot x$$

核心观察:
唯一会“失败”(射入内部)的情况受限于矩形的右侧两个角。
  • 若 $s < 0$ (向下射):必须击中 右上角 $(x_2, y_2)$。
  • 若 $s > 0$ (向上射):必须击中 右下角 $(x_2, y_1)$。

约束可视化

Target Internal (Fail) TR (x2, y2) BR (x2, y1) s < 0: 要射击TR s > 0: 要射击BR

3. 难点:正负斜率分离

题目最大的难点在于意识到:上边界和下边界是分开计算的

  • Positive Group ($s > 0$): 负责射击所有目标的右下角。
  • Negative Group ($s < 0$): 负责射击所有目标的右上角。

但是每个目标左侧边界 ($x_1$) 上的点 $(X_1, y_1[i])$ 和 $(X_1, y_2[i])$ 怎么办?

$\rightarrow$ 贪心分配!

左边界点的分配策略

为了让 $y_{\max}$ 尽可能小,$y_{\min}$ 尽可能大(压缩范围):

  1. 将所有 $N$ 个目标的左侧上下顶点 ($2N$ 个点) 的 $y$ 坐标排序。
  2. 较小的一半分配给负斜率组。
  3. 较大的一半分配给正斜率组。
Sorted Left Ys To Pos Slope To Neg Slope

4. 求解半个问题

正斜率组为例:

  • 点集 $P$:包含所有 $(x_2, y_2)$ 和分配过来的较大 $y$ 的 $(x_1, y)$。
  • 斜率集 $S$:所有正斜率。
  • 目标: 找到一个最大的 $y_{cow}$,使得对于 $P$ 中的每个点,都能在 $S$ 中找到唯一的 $s$,满足 $y_{cow} \le y_p - s \cdot x_p$。
即:对于每个点,需要 $s \le \frac{y_p - y_{cow}}{x_p}$。

算法流程:二分答案 + 贪心Check

由于斜率是整数,答案具有单调性。

  1. 二分答案 $y_{mid}$ (牛的位置)。
  2. Check($y_{mid}$):
    • 计算每个点 $p$ 所需的最大斜率 $s_{req} = \lfloor (y_p - y_{mid}) / x_p \rfloor$。
    • 贪心策略:为了让后面的点有更多选择,当前点应消耗掉满足条件的最大斜率
    • 使用 multiset 维护可用斜率,upper_bound 查找并删除。

复杂度:$O(N \log (\text{Range}) \cdot \log N) \approx O(N \log^2 N)$

5. 负斜率的处理技巧

不需要重写逻辑!利用对称性:

$$y_c = y - s \cdot x \iff (-y_c) = (-y) - (-s) \cdot x$$

操作步骤:
  1. 将所有点的 $y$ 取反:$y' = -y$。
  2. 将所有负斜率取反变为正:$s' = -s$。
  3. 调用之前的 solve() 函数。
  4. 结果取反回来:$ans = -solve()$。

最终答案:$Ans_{total} = (-Ans_{neg}) - Ans_{pos}$

代码实现:Check函数

bool check(vector &pts, vector &slopes, long long y) {
    multiset<int> ss;
    for (auto s: slopes) ss.insert(s);
    
    for (auto p: pts) {
        // 计算该点允许的最大斜率
        long long s0 = (p.second - y) / p.first;
        
        // 在集合中找 <= s0 的最大斜率
        auto it = ss.lower_bound(s0);
        if (it == ss.end() || *it > s0) {
            if (it == ss.begin()) return false; // 没找到
            it--;
        }
        ss.erase(it); // 贪心匹配,删掉这个斜率
    }
    return true;
}

代码实现:二分求解

// 求解在给定点集和斜率集下,牛的 y 坐标最大可能是多少
ll solve(vector<pi> &pts, vector<int> &slopes) {
    long long l = -2e14, r = 2e14; // 注意范围
    long long ans = -2e14;
    while (l <= r) {
        long long mid = (l + r) / 2; // 向下取整
        // C++整除负数有时行为怪异,最好用 floor 或手写 (l+r)>>1
        // 这里为了演示简单展示逻辑:
        if (l+r < 0 && (l+r)%2 != 0) mid = (l+r-1)/2; 
        
        if (check(pts, slopes, mid)) {
            ans = mid;
            l = mid + 1; // 尝试推高 y
        } else {
            r = mid - 1;
        }
    }
    return ans;
}

主逻辑:数据分配

// 1. 分离正负斜率
for (int i=0; i<4*n; i++) {
    if (s[i] > 0) slopes[0].push_back(s[i]);
    else slopes[1].push_back(-s[i]); // 存为正数方便复用
}

// 2. 必须的点 (TR -> pos, BR -> neg)
for (int i=0; i<n; i++) {
    pts[0].push_back({x2[i], y1[i]});      // y1是较大的那个? 题目y1,y2需确认大小
    pts[1].push_back({x2[i], -y2[i]});     // 取反
}

// 3. 左侧点排序分配
vector<int> y_all;
for (int i=0; i<n; i++) { y_all.push_back(y1[i]); y_all.push_back(y2[i]); }
sort(y_all.begin(), y_all.end());

for (auto y: y_all) {
    if (pts[1].size() < 2*n) pts[1].push_back({x1, -y}); // 给负斜率组(取反)
    else pts[0].push_back({x1, y}); // 给正斜率组
}

总结

  • 核心思想: 将问题解耦为“上半场”和“下半场”。
  • 贪心 1: 左侧 $x_1$ 处的点,小的给下半场,大的给上半场。
  • 贪心 2: 在 Check 函数中,每个点尽量消耗满足条件的“最好”资源(最大斜率),给困难的点留余地。
  • 二分答案: 将最优化问题转化为判定问题。
Answer = (-solve(neg_part)) - solve(pos_part)