一.题面:
题意转化:将给定序列分成若干连续段,满足每一段的和都不大于前一段,求最大段数。
二.分析:
我们考虑倒序去做,即求出每一段之和都大于等于前一段。这里有一个贪心的思路:我们考虑将最后一个单独作为第一段,然后倒序依次将元素累加,如果累加和大于前一段,就新开一段,最后输出段数即可。但是这样显然是错误的(虽然我一开始没想到为啥错 ),这里提供一组数据:
5
65
25
80
31
32
如果按照贪心思路,那么我们的输出显然是 2 2 2,但是实际可以将 [ 31 , 32 ] [31,32] [31,32] 划为一段, [ 80 ] [80] [80] , [ 65 , 25 ] [65,25] [65,25]分别划为一段,那么实际答案应该是 3 3 3。
我们考虑 D P DP DP。设 f [ i ] f[i] f[i] 表示从 n n n 到 i i i 的序列最多划分成多少段。 但是在转移的过程中既要考虑 段数,也要考虑 在这种段数下的最后一段的和。会不会出现最优段数的情况下最后一段的和不成立,需要一个不是最优段数但最后一段和却成立的状态来转移呢? 需不需要多开一维同时存储这种高度下的宽度呢? 答案是否定的。 我们注意到一条性质: 在 f [ i ] f[i] f[i] 最大时,也就是段数最多时,它所对应的最后一段的和一定是最小的。举个例子,比如如果 f [ 3 ] = 5 f[3] = 5 f[3]=5,那么从 3 3 3 到 n n n 这个序列被划分成五段时最后一段的和最小,如果划分成 4 4 4 段最后一段的很显然不会减小。因此 在段数达到最优的情况下,最后一段的和也对应最小(让转移更有可能性了)。我们就不需要考虑其它段数下的宽度的情况了。因此我们多开一个数组 g g g, 设 g [ i ] g[i] g[i] 表示 n n n 到 i i i 的序列划分成 f [ i ] f[i] f[i] 段,最后一段的最小和。
我们考虑转移:
f
[
i
]
=
m
a
x
(
f
[
j
]
+
1
)
(
s
u
m
[
i
]
−
s
u
m
[
j
]
≥
g
[
j
]
)
f[i] = max(f[j]+1)(sum[i]-sum[j]\ge g[j])
f[i]=max(f[j]+1)(sum[i]−sum[j]≥g[j])。
如果
f
[
i
]
<
f
[
j
]
+
1
f[i]<f[j]+1
f[i]<f[j]+1,
g
[
i
]
=
s
u
m
[
i
]
−
s
u
m
[
j
]
g[i]=sum[i]-sum[j]
g[i]=sum[i]−sum[j]。
如果
f
[
i
]
=
f
[
j
]
+
1
f[i]=f[j]+1
f[i]=f[j]+1,
g
[
i
]
=
m
i
n
(
g
[
i
]
,
s
u
m
[
i
]
−
s
u
m
[
j
]
)
g[i]=min(g[i],sum[i]-sum[j])
g[i]=min(g[i],sum[i]−sum[j])。
特别的
f
[
n
+
1
]
=
0
f[n+1]=0
f[n+1]=0,
g
[
n
+
1
]
=
0
g[n+1]=0
g[n+1]=0。
那么这样时间复杂度显然是
O
(
n
2
)
O(n^2)
O(n2),无法过掉这道题,考虑优化。我们观察到如果
j
j
j 能够满足条件转移
i
i
i,那么它很显然也可以转移给
i
−
1
i-1
i−1。那么它对
i
−
1
i-1
i−1的转移我们其实是不需要枚举的,我们实际上只需要让
f
[
i
−
1
]
f[i-1]
f[i−1] 的初值等于
f
[
i
]
f[i]
f[i],
g
[
i
−
1
]
g[i-1]
g[i−1] 的初值等于
g
[
i
]
+
a
[
i
−
1
]
g[i]+a[i-1]
g[i]+a[i−1] ,就相当于把所有可以转移给
i
i
i 的
j
j
j 都考虑了。(可以想一想这是为什么)。那么现在我们把所有
j
∈
[
i
,
n
+
1
]
j∈[i,n+1]
j∈[i,n+1] 分成两个集合,一个是可以转移给
i
i
i 的,一个是不能转移个
i
i
i 的,可以转移给
i
i
i,我们通过赋初值已经转移给了
i
−
1
i-1
i−1,那么我们考虑怎样处理不能转移给
i
i
i 的。
如果我们用 set 维护这个集合,那么只需要让元素按照 转移难度 升序排列,然后从前往后判断是否可以转移转移,如果可以转移,我们将它转移给
i
−
1
i-1
i−1后弹出即可。最后把
i
−
1
i-1
i−1 加入 set,每个元素最多进 set 一次,出 set一次,时间复杂度
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)。
至于怎样判断一个元素的 转移难度 呢,我们考虑
y
y
y 给
x
x
x 转移条件是
s
u
m
[
x
]
−
s
u
m
[
y
]
≥
g
[
y
]
sum[x] -sum[y]\ge g[y]
sum[x]−sum[y]≥g[y],移项可得:
s
u
m
[
x
]
≥
s
u
m
[
y
]
+
g
[
y
]
sum[x]\ge sum[y]+g[y]
sum[x]≥sum[y]+g[y],而
x
x
x 变化的过程中
s
u
m
[
y
]
+
g
[
y
]
sum[y]+g[y]
sum[y]+g[y]是始终不变的,因此我们可以按照
s
u
m
[
y
]
+
g
[
y
]
sum[y]+g[y]
sum[y]+g[y]从小到大排序,越小转移就越容易。
此时问题已经得到了解决,但是是否还存在更优秀的算法呢?
我们注意到
f
f
f 数组是 从后往前 递增的,如果当前新加入元素它的转移难度也优于前面的元素,那么前面的元素不是就没有用了吗?
基于这个想法,我们考虑 单调队列。我们对于当前的元素,从队首扫描并转移,出队。在队尾将所有转移难度比它更大的出队,最后将它加入队尾。这样,我们就维护了一个 决策点的转移难度单调递增,决策点的转移价值单调递增的单调队列。时间复杂度
O
(
n
)
O(n)
O(n)。
CODE:
#include<bits/stdc++.h>//高度最高时最底层一定最窄?
using namespace std;// f[]数组单调递减, 并且如果i能满足g数组,那么 i - 1 也一定能满足
const int N = 1e5 + 10;
int n, w[N], sum[N], res, f[N], g[N];
deque< int > q;// q维护决策集合 f不降, 能够成功的难度递增
int main(){
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
for(int i = n; i >= 1; i--) sum[i] = sum[i + 1] + w[i];
memset(g, 0x3f, sizeof(g));
g[n + 1] = 0;
q.push_back(n + 1);
for(int i = n; i >= 1; i--){
f[i] = f[i + 1], g[i] = g[i + 1] + w[i];//i和 i + 1 拼在一起
while(!q.empty() && (sum[i] - sum[q.front()] >= g[q.front()])){
if(f[q.front()] + 1 >= f[i]){
f[i] = f[q.front()] + 1;
g[i] = min(g[i], sum[i] - sum[q.front()]);
}
q.pop_front();
}
while(!q.empty() && ((g[i] + sum[i] - sum[q.back()]) <= g[q.back()])) q.pop_back();
q.push_back(i);
}
cout << f[1] << endl;
return 0;
}
/*
7
1
26
72
17
61
67
20
12
86
53
73
69
89
23
27
65
29
89
49
63
*/
三.总结
单调队列 优化DP是很套路的技巧,我们需要考虑 一个元素新加入后会不会有前面的元素用不到 。如果满足了这个性质,我们就可以考虑使用单调队列来进行优化。