洛谷-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,有
有 \(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\),答案为
然而看上去我们还是需要枚举 \(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\),进而
时间 \(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;
}

浙公网安备 33010602011771号