link
题面 $
给定一个数组 a, 要把它分成若干段,让每一段的和不超过 m m m ,求每一段的最大值之和最小是多少。
分析
朴素的
d
p
dp
dp 比较容易想到。
定义
d
p
[
i
]
dp[i]
dp[i] 表示从
a
[
1
:
i
]
a[1:i]
a[1:i] 分成若干段的 最小 最大值之和,
i
0
i_0
i0 表示
s
u
m
a
[
i
0
,
i
]
>
m
,
s
u
m
a
[
i
0
+
1
,
i
]
≤
m
sum \ a[i_0, i] > m, \ sum \ a[i_0 + 1, i] ≤ m
sum a[i0,i]>m, sum a[i0+1,i]≤m 的那个临界值,有如下转移:
d
p
[
i
]
=
d
p
[
j
]
+
m
a
x
a
[
j
+
1
,
i
]
(
i
0
≤
j
<
i
)
dp[i] = dp[j] + max \ a[j+1, i] \ \ (i_0 \ ≤ j \ <i)
dp[i]=dp[j]+max a[j+1,i] (i0 ≤j <i)
这样的转移是
O
(
n
2
)
O(n^2)
O(n2) 的。继续观察,发现这个 dp值一定是单调不减的,若假设
i
1
i_1
i1 是
a
[
i
0
+
1
,
i
]
a[i_0 + 1, i]
a[i0+1,i] 最大值的下标,我们可以得到对于
j
<
i
1
j <i_1
j<i1,
d
p
[
i
]
=
d
p
[
j
]
+
a
[
i
1
]
dp[i] = dp[j] + a[i_1]
dp[i]=dp[j]+a[i1],并且
j
=
i
0
j = i_0
j=i0 时一定能取得最小值。
类似地,定义
i
p
+
1
i_{p+1}
ip+1 是
a
[
i
p
+
1
,
i
]
a[i_p + 1, i]
a[ip+1,i] 最大值的下标,那么当
i
p
≤
j
<
i
p
+
1
i_p ≤ j <i_{p+1}
ip≤j<ip+1 时,
d
p
[
i
]
=
d
p
[
j
]
+
a
[
i
p
+
1
]
dp[i] = dp[j] + a[i_{p+1}]
dp[i]=dp[j]+a[ip+1],最小值在
i
p
i_p
ip 取到。
所以我们可以维护一个单调队列,然后遍历单调队列取得最小值。但是在最坏情况下,单调队列有O(n)个数,依次遍历的话复杂度还是
O
(
n
2
)
O(n^2)
O(n2)。因此可以考虑用堆去优化这个单调队列。
在位置
i
i
i,若单调队列里有
k
k
k 个元素,单调队列里维护的是
i
1
,
i
2
,
i
3
.
.
.
i
i_1, i_2, i_3...i
i1,i2,i3...i,最小值就是
d
p
[
i
0
]
+
a
[
i
1
]
,
d
p
[
i
1
]
+
a
[
i
2
]
.
.
.
d
p
[
i
k
−
1
]
+
a
[
i
]
dp[i_0] + a[i_1], dp[i_1] + a[i_2]...dp[i_{k-1}] + a[i]
dp[i0]+a[i1],dp[i1]+a[i2]...dp[ik−1]+a[i] 的最小值。所以队首和队尾需要特殊处理一下,出队入队的元素也要处理一下,其实就是要维护好一个动态改变的最小堆。单调队列里的下标就像将一个需要遍历的区间拆成若干个点,减少了复杂度。
具体细节参照代码。
#include<cstdio>
#include<queue>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 1e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
//const double pi = acos(-1.0);
const double eps = 1e-11;
struct node
{
ll val;
int p;
bool operator<(const node& m) const //按照小顶堆
{
return val > m.val;
}
node(ll k1, int k2){
val = k1;
p = k2;
}
};
priority_queue<node> que;
int n, a[maxn], q[maxn], vis[maxn];
ll m, dp[maxn], val[maxn];
int main()
{
scanf("%d %lld", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) //特判
{
if(a[i] > m)
{
printf("-1\n");
return 0;
}
}
int pos = 1, L = 1, R = 1; //单调队列用一个数组维护,范围在[L,R], sum a[pos, i] <= m, sum a[pos-1, i] > m
ll cur = a[1]; //cur = a[pos, i]
dp[1] = a[1];
q[1] = 1;
vis[1] = 1; //1 表示 表示这个位置在单调队列里
que.push(node(a[1], 1));
for (int i = 2; i <= n; i++)
{
cur += a[i]; //更新cur和pos值
while(cur > m)
cur -= a[pos++];
while(L <= R && q[L] < pos) //更新单调队列左端点
{
vis[q[L]] = 0;
L++;
}
while(L <= R && a[q[R]] <= a[i]) //更新单调队列右端点
{
vis[q[R]] = 0;
R--;
}
if(L <= R) //如果单调队列里还有元素
{
val[q[L]] = dp[pos - 1] + a[q[L]];
que.push(node(val[q[L]], q[L])); //更新左部
val[i] = dp[q[R]] + a[i];
vis[i] = 1;
que.push(node(val[i], i)); //由于在右部加入了位置i,进行更新
}
else //如果单调队列里没有元素,加入i
{
val[i] = dp[pos - 1] + a[i];
vis[i] = 1;
que.push(node(val[i], i));
}
q[++R] = i;
while(vis[que.top().p] == 0 || que.top().val != val[que.top().p]) //如果已经不在单调队列里或者该值已经被更新
que.pop();
dp[i] = que.top().val;
}
printf("%lld\n", dp[n]);
}