D. Blocking Elements
You are given an array of numbers $a_1, a_2, \ldots, a_n$. Your task is to block some elements of the array in order to minimize its cost. Suppose you block the elements with indices $1 \leq b_1 < b_2 < \ldots < b_m \leq n$. Then the cost of the array is calculated as the maximum of:
- the sum of the blocked elements, i.e., $a_{b_1} + a_{b_2} + \ldots + a_{b_m}$.
- the maximum sum of the segments into which the array is divided when the blocked elements are removed. That is, the maximum sum of the following ($m + 1$) subarrays: [$1, b_1 − 1$], [$b_1 + 1, b_2 − 1$], [$\ldots$], [$b_{m−1} + 1, b_m - 1$], [$b_m + 1, n$] (the sum of numbers in a subarray of the form [$x,x − 1$] is considered to be $0$).
For example, if $n = 6$, the original array is [$1, 4, 5, 3, 3, 2$], and you block the elements at positions $2$ and $5$, then the cost of the array will be the maximum of the sum of the blocked elements ($4 + 3 = 7$) and the sums of the subarrays ($1$, $5 + 3 = 8$, $2$), which is $\max(7,1,8,2) = 8$.
You need to output the minimum cost of the array after blocking.
Input
The first line of the input contains a single integer $t$ ($1 \leq t \leq 30\,000$) — the number of queries.
Each test case consists of two lines. The first line contains an integer $n$ ($1 \leq n \leq 10^5$) — the length of the array $a$. The second line contains $n$ elements $a_1, a_2, \ldots, a_n$ ($1 \leq a_i \leq 10^9$) — the array $a$.
It is guaranteed that the sum of $n$ over all test cases does not exceed $10^5$.
Output
For each test case, output a single number — the minimum cost of blocking the array.
Example
input
3
6
1 4 5 3 3 2
5
1 2 3 4 5
6
4 1 6 3 10 7
output
7
5
11
Note
The first test case matches with the array from the statement. To obtain a cost of $7$, you need to block the elements at positions $2$ and $4$. In this case, the cost of the array is calculated as the maximum of:
- the sum of the blocked elements, which is $a_2 + a_4 = 7$.
- the maximum sum of the segments into which the array is divided when the blocked elements are removed, i.e., the maximum of $a_1$, $a_3$, $a_5 + a_6 = \max(1,5,5) = 5$.
So the cost is $\max(7,5) = 7$.
In the second test case, you can block the elements at positions $1$ and $4$.
In the third test case, to obtain the answer $11$, you can block the elements at positions $2$ and $5$. There are other ways to get this answer, for example, blocking positions $4$ and $6$.
解题思路
显然答案具有二段性,即如果存在一种合法方案使得分割点总和以及每个分割段的和都不超过 $x$,那么大于 $x$ 的值也可以作为一个合法的答案,为此需要二分出最小的 $x$。
对于二分值 $\text{mid}$ 则需要判断是否存在合法的分割方案。由于选择分割点相当于选择总和不超过 $\text{mid}$ 的子序列,所以可以尝试 dp 找到使得每个分割段的和都不超过 $\text{mid}$ 时分割点总和的最小值是多少,如果不超过 $\text{mid}$ 则说明存在分割方案。
定义 $f(i)$ 表示将前 $i$ 个元素进行分割,且分割点总和以及每个分割段的和都不超过 $\text{mid}$ 的所有方案中,分割点总和的最小值。根据第 $i$ 个元素是否为分割点进行状态划分:
- 如果 $a_i$ 是分割点,那么状态转移方程为 $f(i) = f(i-1) + a_i$。
- 如果 $a_i$ 不是分割点,那么找到前一个分割点 $a_j$,要求满足最后一个分割段的和不超过 $\text{mid}$,即 $s_i - s_j \leq \text{mid}$,其中 $s_i = \sum_{k=1}^{i}{a_i}$ 表示前缀和。状态转移方程为 $f(i) = \min\limits_{s_i - s_j \leq \text{mid}}\left\{ f(j-1) + a_j \right\}$。
其中定义 $f(-1) = f(0) = 0$,$a_0 = 0$。
显然第二个部分需要优化,注意到本质就是在 $j \in [0, i-1]$ 中找到所有满足 $s_j \geq s_i - \text{mid}$ 的 $j$,然后对 $f(j-1) + a_j$ 取最小值。所以可以用值域线段树来优化,在每个 $s_i$ 处存储 $f(i-1) + a_i$ 并维护最小值,询问就相当于求以 $s_i - \text{mid}$ 为左端点的后缀的最小值。需要对 $s_i$ 和 $s_i - \text{mid}$ 离散化。
最后如果 $f(n) \leq \text{mid}$ 则说明存在分割方案。
AC 代码如下,时间复杂度为 $O\left({n \log{n} \cdot \log{\sum{a_i}}}\right)$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
const LL INF = 0x3f3f3f3f3f3f3f3f;
int n;
int a[N];
LL s[N];
LL xs[N], sz;
LL f[N];
struct Node {
int l, r;
LL mn;
}tr[N * 4];
int find(LL x) {
int l = 1, r = sz;
while (l < r) {
int mid = l + r >> 1;
if (xs[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
void build(int u, int l, int r) {
tr[u] = {l, r, INF};
if (l != r) {
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
}
void modify(int u, int x, LL c) {
if (tr[u].l == tr[u].r) {
tr[u].mn = min(tr[u].mn, c);
}
else {
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify(u << 1, x, c);
else modify(u << 1 | 1, x, c);
tr[u].mn = min(tr[u << 1].mn, tr[u << 1 | 1].mn);
}
}
LL query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mn;
int mid = tr[u].l + tr[u].r >> 1;
LL ret = INF;
if (l <= mid) ret = query(u << 1, l, r);
if (r >= mid + 1) ret = min(ret, query(u << 1 | 1, l, r));
return ret;
}
bool check(LL mid) {
sz = 0;
for (int i = 0; i <= n; i++) {
xs[++sz] = s[i];
xs[++sz] = s[i] - mid;
}
sort(xs + 1, xs + sz + 1);
sz = unique(xs + 1, xs + sz + 1) - xs - 1;
build(1, 1, sz);
memset(f, 0x3f, n + 10 << 3);
f[0] = 0;
for (int i = 1; i <= n; i++) {
modify(1, find(s[i - 1]), f[max(0, i - 2)] + a[i - 1]);
f[i] = min(f[i - 1] + a[i], query(1, find(s[i] - mid), sz));
}
return f[n] <= mid;
}
void solve() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
s[i] = s[i - 1] + a[i];
}
LL l = 1, r = 1e14;
while (l < r) {
LL mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%lld\n", l);
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
比赛的时候我是用上面的做法过的,还可以用单调队列来优化。注意到 $s_j \geq s_i - \text{mid}$ 中的 $s_i - \text{mid}$,随着 $i$ 的增加 $s_i$ 也会增加,因此 $s_i - \text{mid}$ 也会增加,具有单调性。因此可以用单调队列来维护 $j \in [0, i-1]$ 中 $f(j-1) + a_j$ 的最小值。当枚举到 $i$,如果对头元素对应的 $s_j < s_i - \text{mid}$ 则弹出。
AC 代码如下,时间复杂度为 $O\left({n\cdot \log{\sum{a_i}}}\right)$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n;
int a[N];
LL s[N];
LL f[N];
int q[N], hh, tt;
bool check(LL mid) {
memset(f, 0x3f, n + 10 << 3);
f[0] = 0;
hh = 0, tt = -1;
for (int i = 1; i <= n; i++) {
// 把f(j-1)+a[j]加入单调队列中
while (hh <= tt && f[max(0, q[tt] - 1)] + a[q[tt]] >= f[max(0, i - 2)] + a[i - 1]) {
tt--;
}
q[++tt] = i - 1; // 队列中存的是下标
while (hh <= tt && s[q[hh]] < s[i] - mid) { // 把队头不满足s[j]>=s[i]-mid的元素删除
hh++;
}
if (hh <= tt) f[i] = min(f[i - 1] + a[i], f[max(0, q[hh] - 1)] + a[q[hh]]);
}
return f[n] <= mid;
}
void solve() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
s[i] = s[i - 1] + a[i];
}
LL l = 1, r = 1e14;
while (l < r) {
LL mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%lld\n", l);
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
参考资料
Codeforces Round #922 (Div. 2) Editorial:https://codeforces.com/blog/entry/125300%E3%80%81
标签:Elements,int,text,LL,mid,elements,array,Blocking From: https://www.cnblogs.com/onlyblues/p/17999932