1 条题解
-
0
D. 封锁元素 题解(单调队列优化版)
题意回顾
给定长度为 的数组 ,选择一些位置封锁,设封锁位置为 。代价定义为以下两者的最大值:
$$\max\left( \sum_{k=1}^m a_{b_k},\ \max_{1 \le i \le m+1} \text{sum}(segment_i) \right) $$其中 是移除封锁元素后形成的连续段。求最小可能代价。
核心思路:二分答案 + 动态规划 + 单调队列
1. 二分答案的单调性
若代价 可行,则任意 显然也可行(保持原封锁方案即可)。因此答案关于 单调,可以使用二分搜索。
- 下界 (或 ,但 足够)。
- 上界 (最大可能代价为封锁所有元素)。
- 二分次数固定为 次(),足以覆盖范围,且避免边界判断。
2. 判定函数:检查给定 是否可行
给定候选代价 ,我们需要判断是否存在一种封锁方案,满足:
- 封锁元素的总和 ;
- 任意未封锁的连续段的和 。
我们可以在数组末尾添加一个虚拟元素 ,作为必然被封锁的终点(代价为 )。这样,原问题等价于选择一组位置 ,满足:
- 对于每对相邻封锁位置 ,中间段的和 ;
- 所有封锁位置上的元素和(不含虚拟终点) 。
我们希望封锁元素和尽可能小,因此定义 表示考虑位置 被封锁时,从 到 这一段内封锁元素的最小总代价(包含 ,若 则代价为 )。
转移方程:
$$dp[j] = a_j + \min_{\substack{k > j \\ \sum_{i=j+1}^{k-1} a_i \le x}} dp[k] $$其中 是下一个封锁位置,要求中间段的和 。特别地,。
最终,若存在某个 ()满足:
- 从数组开头到 的和 (即第一段合法),
- 且 (封锁代价合法),
则 可行。此外,完全不封锁(即整个数组和 )的情况也需考虑。
3. 滑动窗口与单调队列优化
直接枚举 的转移复杂度为 ,无法接受。注意到条件 等价于前缀和约束:
$$pre[k-1] - pre[j] \le x \quad \Longleftrightarrow \quad pre[k-1] \le pre[j] + x $$其中 (设 )。随着 的减小, 递减,因此满足条件的 的范围是一个向左滑动的窗口:左端点随 减小而向右移动。
我们采用从后向前的 DP 顺序。对于当前 ,维护一个窗口 ,其中 是满足 的最大右端点。窗口内的所有 均可作为下一个封锁位置。我们需要查询窗口内最小的 。
由于窗口边界单调移动,且需要维护最小值,可以使用 单调队列:
- 队列中存储候选下标,并按 值递增排列(队首最小)。
- 当窗口右移( 减小)时,若队首元素等于被移出的下标,则弹出队首。
- 当插入新下标 时,从队尾弹出所有 值大于等于 的元素,以保持单调性,然后将 加入队尾。
这样,每次转移可以 获取最小值,整体判定复杂度降为 。
4. 算法步骤
- 读入 和数组 ,计算总和 。
- 二分答案 ,范围 (或固定 次迭代)。
- 对每个 进行判定:
- 初始化 ,清空单调队列,将 入队。
- 维护窗口右端点 ,当前窗口和 。
- 从 递减到 :
- 将 加入 (扩展窗口)。
- 当 时,循环右移 : 减去 ,若队首为 则弹出,。
- 此时队首即为窗口内最小的 值对应的下标,计算 。
- 将 插入队列:弹出队尾所有 值 的元素,再将 入队。
- 检查可行性:计算前缀和,若存在 满足 且 ,则 可行。同时判断完全不封锁的情况。
- 输出二分得到的最终 值。
复杂度分析
- 单次判定:每个元素最多入队、出队一次,时间复杂度 。
- 二分次数:固定 次或 。
- 总时间复杂度:,在 下约为 次操作,非常快。
参考代码(含详细注释)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int MAXN = 2e5 + 10; // 全局静态数组,避免动态分配 ll a[MAXN]; ll dp[MAXN]; deque<int> q; // 单调队列,存储下标,按 dp 值递增 // 快速读入 inline ll read() { ll x = 0; char c = getchar_unlocked(); while (c < '0' || c > '9') c = getchar_unlocked(); while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar_unlocked(); return x; } int main() { int t = read(); while (t--) { int n = read(); ll sum_a = 0; for (int i = 0; i < n; i++) { a[i] = read(); sum_a += a[i]; } ll l = 0, r = sum_a; // 固定 60 次二分,足够覆盖 1e14 范围且无边界判断 for (int _ = 0; _ < 60; _++) { ll m = (l + r) >> 1; q.clear(); int p2 = n; dp[n] = 0; q.push_back(n); ll sum = 0; // 窗口 [j+1, p2-1] 的元素和 for (int j = n - 1; j >= 0; j--) { sum += a[j]; // 维护窗口和 <= m,若不满足则右移 p2 while (sum > m) { sum -= a[p2 - 1]; // 如果队首下标刚好是被移出窗口的 p2,弹出队首 if (!q.empty() && q.front() == p2) q.pop_front(); p2--; } // 队首即为窗口内最小 dp 值对应的下标 dp[j] = dp[q.front()] + a[j]; // 维护单调队列:保持 dp 值递增 while (!q.empty() && dp[q.back()] >= dp[j]) q.pop_back(); q.push_back(j); } // 检查是否存在可行的起始封锁点 ll s = 0; bool ok = false; for (int j = 0; j < n; j++) { if (s <= m && dp[j] <= m) { ok = true; break; } s += a[j]; } // 完全不封锁的情况 if (!ok && sum_a <= m) ok = true; if (ok) r = m; else l = m + 1; } printf("%lld\n", l); } return 0; }
总结
本题综合考察了二分答案、动态规划与滑动窗口最小值维护。通过将问题转化为判定性问题,并利用单调队列在 时间内完成判定,再结合二分法,最终在 的时间复杂度内求解,完美契合了数据范围的要求。代码中采用固定次数的二分与快速读入进一步提升了运行效率。
- 1
信息
- ID
- 6514
- 时间
- 4000ms
- 内存
- 256MiB
- 难度
- 7
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者