题意
给定一个长度为 n n n的数组 a a a, 你可以从这 n n n个数中任意选择一部分(也可以不选),假定选择了 m m m个数,那么原数组将被分成 m + 1 m+1 m+1段,把选择的 m m m个数也看作一段,所以共有 m + 2 m+2 m+2段数,对于每段,把段内的所有数字求和,得到 m + 2 m+2 m+2个数,这 m + 2 m+2 m+2个数中的最大值为花费,要求花费最小化。
做法
题目的本质要求是最小化最大值,所以考虑二分。
关键在于如何check当前二分的mid值。
通过研究样例,发现无法通过简单的贪心或者双指针之类的做法进行check, 因为无法保证得到最优解。
所以考虑动态规划。
设
d
p
[
i
]
dp[i]
dp[i]为前
i
i
i个数且选择了第
i
i
i个数的合法最小花费(这里指的是选择的数之和的最小值)。
初始有
d
p
[
0
]
=
0
dp[0] = 0
dp[0]=0
转移为
d
p
[
i
]
=
a
[
i
]
+
min
(
d
p
[
j
]
)
dp[i] = a[i] + \min(dp[j])
dp[i]=a[i]+min(dp[j]), 其中
j
<
i
j < i
j<i并且
∑
k
=
j
+
1
i
−
1
a
[
i
]
≤
t
a
r
\sum_{k = j +1}^{i -1}{a[i]} \le tar
∑k=j+1i−1a[i]≤tar,
t
a
r
tar
tar当前二分的mid值。
- 为什么不用考虑 j j j左边部分的和小于等于 t a r tar tar?
- d p [ j ] dp[j] dp[j]已经在前面计算出来,可以保证选择 d p [ j ] dp[j] dp[j]时 j j j左边的部分是合法的,所以只需要保证 j j j右边到 i − 1 i-1 i−1的和合法即可。
答案为
d
p
[
n
+
1
]
dp[n+1]
dp[n+1]
转移求最小值可以用优先队列来实现,而
j
+
1
j+1
j+1到
i
−
1
i-1
i−1数字之和合法可以用双指针来维护。
时间复杂度粗略为 O ( C ⋅ n ⋅ log n ) , C = log ( 1 e 14 ) O(C \cdot n \cdot \log n), C = \log{(1e14)} O(C⋅n⋅logn),C=log(1e14)
总结
本题的做法是非常经典的二分+DP,即二分答案并使用DP进行check
当有了二分答案的雏形后,构思check时不妨考虑一下是否可以用DP
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using al = array<ll, 2>;
void solve() {
int n;
cin >> n;
vector<ll> a(n + 2);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
ll l = 1, r = 1e14;
auto check = [&](ll tar) {
vector<ll> dp(n + 2);
priority_queue<al, vector<al>, greater<al> > q;
q.push(al{0, 0});
int L = 0, R = 0;
ll now = 0;
while (1) {
R++;
if (R >= n + 2)
break;
while (L <= n && now > tar)
now -= a[++L];
while (!q.empty() && q.top()[1] < L)
q.pop();
dp[R] = a[R] + q.top()[0];
q.push(al{dp[R], R});
now += a[R];
}
return dp[n + 1] <= tar;
};
while (l < r) {
ll mid = (l + r) >> 1;
// cerr << l << ' ' << r << ' ' << mid << '\n';
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << l << '\n';
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int _;
cin >> _;
while (_--)
solve();
return 0;
}