洛谷-P3132 [USACO16JAN] Angry Cows G

洛谷-P3132 [USACO16JAN] Angry Cows G

洛谷-P3132 [USACO16JAN] Angry Cows G

tag: 二分, 动态规划, 双指针

\(N\) 个干草堆位于数轴上不同的整数位置 \(x_1, x_2, \ldots, x_N\)

如果一头奶牛以威力 \(R\) 被发射到位置 \(x\),这将引发一个 “半径为 \(R\)” 的爆炸,吞噬 \(x-R \ldots x+R\) 范围内的所有干草堆。

这些干草堆随后会同时爆炸,每个爆炸的半径为 \(R-1\)

任何尚未爆炸的干草堆如果被这些爆炸波及,则会同时爆炸,爆炸半径为 \(R-2\),依此类推。

请确定发射一头奶牛所需的最小威力 \(R\),使得如果它落在适当的位置,将引发所有干草堆的爆炸。

\(2\le N\le5\cdot10^4\)\(0\le x_1,\cdots,x_N\le10^9\)。答案精确到小数点后一位。

解题思路

思路 1:\(O(N\log^2V)\) 二分套二分

首先,由于 \(R\) 越大,越可能满足条件,所以可以 二分答案,找到最小的满足条件的 \(R\)

对于当前二分的 \(R\),考虑找到一个发射位置 \(x\),使得尽量地波及到所有干草堆。

由于威力在传播的过程中会衰减,我们应该找到一个比较靠中间的位置。但不能贪心做,因为我们不知道干草堆的间隔情况是怎样的。

事实上,对于 \(x\) 我们也可以 二分。在固定 \(R\) 的前提下:

  • 如果当前 \(x\) 覆盖不到最左边的干草堆,说明 \(x\) 应该往左移动;
  • 如果当前 \(x\) 覆盖不到最右边的干草堆,说明 \(x\) 应该往右移动;
  • 如果两边都可以覆盖,就找到了一个合法的 \(x\)

覆盖的过程,直接 \(O(N)\) 模拟即可。

\(R\)\(x\) 进行 实数二分 的过程,要 注意设置好精度,即最终 \(l\)\(r\) 的差值。设精度为 \(\varepsilon\),则二分的时间复杂度为 \(O\left(\log_2\dfrac{r_0-l_0}{\varepsilon}\right)\)

由于精度要求不高,取 \(\varepsilon=10^{-6}\),则二分循环次数不超过 \(\log_2\dfrac{10^9}{10^{-6}}\approx50\),总时间复杂度 \(N\cdot50^2=1.25\cdot10^8\),可以通过。

思路 2:\(O(N\log N)\) 二分优化 DP 预处理

另一种想法是,设 \(f(i),g(i)\) 分别表示 \(i\) 爆炸半径最小为多少,才能使 \(i\) 左侧 / 右侧的所有干草堆 全部 爆炸。

考虑如何 DP,有

\[f(i)=\min_{1\le j<i}\max\{f(j)+1,x_i-x_j\}.\tag1 \]

\(i\)\(j\) 两个变量,考虑固定 \(i\)。随着 \(j\) 的增大,\(f(j)\)\(0\) 开始增大(不降),\(x_i-x_j\) 减小。

因此这两个曲线的交点即为最小值点(例如 \(y=\max\{x+1,-x+2\}\)),故 二分 第一个满足 \(f(j)+1\ge x_i-x_j\) 的点即可。设这个点为 \(p\),则最小值为 \(\min\{f(p)+1,x_i-x_{p-1}\}\)

\(g(i)\) 的求法类似,总时间 \(O(N\log N)\)。求出 \(f(i),g(i)\) 之后,考虑如何求出落点 \(x\),注意 \(x\) 不一定在干草堆上。

但是,根据尽量平衡左右干草堆距离的想法,假设选定的 \(x\)\(R\) 使得第一次爆炸波及到的最左边和最右边的干草堆为 \(l\)\(r\),则落点在 \((x_l+x_r)/2\) 处最优。因此,只要把干草堆的坐标全乘以二(注意 \((1)\)\(R\) 的衰减速度也变为 \(2\)),则最优的 \(x\) 一定在整点上。

(下面的 \(x_i\) 都是翻倍后的。)进而,维护两个指针 \(l,r\) 表示初始爆炸的干草堆下标区间,则初始爆炸点为 \((x_l+x_r)/2\),答案为

\[2\mathrm{Ans}=\min_{1\le l\le n}\max_{l\le r\le n}\left\{\frac{x_r-x_l}2,f(l)+2,g(r)+2\right\}.\tag2 \]

然而看上去我们还是需要枚举 \(l,r\)。如何优化枚举?

类似上面的想法,固定 \(l\),在 \(r\) 增加时,\(x_r-x_l\) 递增,\(g(r)\) 递减且趋于 \(0\),交点即为 \(F(r)=\max\{\cdots\}\) 的最小值点,故仍然采用二分,求出第一个满足 \(\dfrac{x_r-x_l}2\ge g(r)+2\) 的点 \(r=p\),进而

\[2\mathrm{Ans}\xleftarrow{\min}\min\left\{\max\left\{f(l)+2,\dfrac{x_{p}-x_l}2\right\},\max\{f(l)+2,g(p-1)+2\}\right\}. \]

时间 \(O(N\log N)\)

思路 3:\(O(N)\) 双指针(除了排序 \(O(N\log N)\)

或者,对于转移方程 \((1)\)\(i\) 增加时,\(x_i-x_j\) 曲线向上平移,\(f(j)\) 曲线不变,故交点 \(p\) 只会向右移动。因此,可以用 双指针(滑动窗口)优化。对于方程 \((2)\) 同理。时间复杂度 \(O(N\log N+N)\),瓶颈在于排序。

参考代码

思路 1

#include <bits/stdc++.h>
using namespace std;
int const N = 5e4 + 10;
double const eps = 1e-6;
int n, a[N];

int check2(double x, double R) { // 模拟爆炸过程, 爆炸起始点为 x, 初始半径为 R
    int t = 0;
    while (t <= n && a[t + 1] + eps < x) ++t;
    double pos = x, r = R;
    for (int i = t; i >= 1; r--) {
        if (pos - a[i] - eps > r) return 1; // 够不到左边
        while (i >= 1 && pos - a[i] + eps < r) --i;
        pos = a[i + 1];
    }
    pos = x, r = R;
    for (int i = t + 1; i <= n; r--) {
        if (a[i] - pos - eps > r) return 2; // 够不到右边
        while (i <= n && a[i] - pos + eps < r) ++i;
        pos = a[i - 1];
    }
    return 0;
}

bool check(double R) {
    double l = a[1], r = a[n], mid;
    while (r - l > eps) {
        mid = (l + r) / 2;
        int _ = check2(mid, R);
        if (_ == 0) return true;
        else if (_ == 1) r = mid; // 最左侧没覆盖上, 向左移动
        else if (_ == 2) l = mid; // 最右侧没覆盖上, 向右移动
    }
    return false;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    double l = 0, r = a[n] - a[1], mid;
    while (r - l > eps) {
        mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    cout << fixed << setprecision(1) << r << '\n';

    return 0;
}

思路 2

#include <bits/stdc++.h>
using namespace std;
int const INF = 2e9;
int const N = 5e4 + 10;
int n, x[N], f[N], g[N];

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> x[i], x[i] <<= 1;
    sort(x + 1, x + n + 1);

    f[1] = 0;
    for (int i = 2; i <= n; i++) {
        int l = 0, r = i, mid;
        while (l + 1 < r) {
            mid = l + r >> 1;
            if (f[mid] + 2 >= x[i] - x[mid]) r = mid;
            else l = mid;
        }
        if (r == i) f[i] = x[i] - x[i - 1];
        else if (r == 1) f[i] = f[r] + 2;
        else f[i] = min(f[r] + 2, x[i] - x[l]);
    }

    g[n] = 0;
    for (int i = n - 1; i >= 1; i--) {
        int l = i, r = n + 1, mid;
        while (l + 1 < r) {
            mid = l + r >> 1;
            if (g[mid] + 2 >= x[mid] - x[i]) l = mid;
            else r = mid;
        }
        if (l == i) g[i] = x[i + 1] - x[i];
        else if (l == n) g[i] = g[l] + 2;
        else g[i] = min(g[l] + 2, x[r] - x[i]);
    }

    int ans = INF;
    for (int l = 1; l <= n; l++) {
        int lo = l - 1, hi = n, mid;
        while (lo + 1 < hi) {
            mid = lo + hi >> 1;
            if ((x[mid] - x[l]) / 2 >= g[mid] + 2) hi = mid;
            else lo = mid;
        }
        ans = min(ans, max(f[l] + 2, (x[hi] - x[l]) / 2));
        if (lo >= l) ans = min(ans, max(f[l] + 2, g[lo] + 2));
    }
    cout << fixed << setprecision(1) << ans / 2.0 << '\n';

    return 0;
}

思路 3

#include <bits/stdc++.h>
using namespace std;
int const INF = 2e9;
int const N = 5e4 + 10;
int n, x[N], f[N], g[N];

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> x[i], x[i] <<= 1;
    sort(x + 1, x + n + 1);

    f[1] = 0;
    for (int i = 2, j = 1; i <= n; i++) {
        int cur, nxt;
        cur = nxt = max(f[j] + 2, x[i] - x[j]);
        while (j + 1 < i) {
            nxt = max(f[j + 1] + 2, x[i] - x[j + 1]);
            if (nxt <= cur) cur = nxt, ++j;
            else break;
        }
        f[i] = cur;
    }

    g[n] = 0;
    for (int i = n - 1, j = n; i >= 1; i--) {
        int cur, nxt;
        cur = nxt = max(g[j] + 2, x[j] - x[i]);
        while (j - 1 > i) {
            nxt = max(g[j - 1] + 2, x[j - 1] - x[i]);
            if (nxt <= cur) cur = nxt, --j;
            else break;
        }
        g[i] = cur;
    }

    int ans = INF;
    for (int l = 1, p = 1; l <= n; l++) {
        if (p < 1) p = 1;
        int cur, nxt;
        cur = nxt = max(x[p] - x[l] >> 1, g[p] + 2);
        while (p < n) {
            nxt = max(x[p + 1] - x[l] >> 1, g[p + 1] + 2);
            if (nxt <= cur) cur = nxt, ++p;
            else break;
        }
        ans = min(ans, max(cur, f[l] + 2));
    }
    cout << fixed << setprecision(1) << ans / 2.0 << '\n';

    return 0;
}
posted @ 2026-01-24 13:47  f2021ljh  阅读(1)  评论(0)    收藏  举报