一.前言
我翻阅了许多blog,都没有讲清楚为什么最底层的宽度最小,高度最高,我决定尝试解释一下,由于本人井中视星,只能大致讲讲证明思路
二.证明
(1)LH sb 证明1,暴力讨论法
我们很难从正面去直接解释高度最值的条件,但我们不妨可以使用周老师的 “正难则反” 和柯老师的 “better than anyone”。
假设当最底层的宽度为 f [ i ] f[i] f[i]时干草堆最高,如图
只需证明最底层宽度变大只会使高度降低而不是升高
由于干草堆只能选择连续一段作为同一层,所以如果使底层变宽而又不新添干草堆的方法只能是从上层取出干草堆放入最底层
1.上层不满足要求
例如上图将③号草堆取下来,则上层已不满足要求,则又必须向上取草堆
如此同理,则高度只会下降。
2.上层满足要求
如图,干草堆的初始状态如此
将三号干草堆放入底层,上层仍然满足要求
若上层能将某一段干草堆放入上上层,则与假设(此方案为最佳方案)矛盾
若上层不能将某一段干草堆放入上上层,则一定不会出现比此方案更优的方案(因为除了三号干草堆以外没有草堆变化,所以高度不会变化)
综上,即证:当最底层的高度为最小时,高度最大
(2)证明2,由实现得到的灵感
若如图
上图变为下图,则上图可选择的干草堆为[1, 3],下图则为 [1, 3),所以上图可以选择的干草堆更多,在最坏情况下,也不过是下图(将干草堆3作为底层),所以往底层多放干草堆只会使高度变小或不变
三.实现
那么我们很容易写出 d p dp dp转移方程
h
[
i
]
=
m
a
x
j
f
[
j
]
<
=
f
a
l
l
[
i
]
−
f
a
l
l
[
j
]
h
[
j
]
+
1
h[i] = max_{j}^{f[j] <= fall[i] - fall[j]}h[j] +1
h[i]=maxjf[j]<=fall[i]−fall[j]h[j]+1
注:
f
a
l
l
[
i
]
fall[i]
fall[i]为后缀和
由于干草堆多的所堆积的高度一定比干草堆少的所堆积的高度高或等高(最坏情况下也是将多余的干草堆放在最底层),所以我们选择最小的 j j j
轻松写出代码
#include <stack>
#include <cmath>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define LL long long
#define ULL unsigned long long
using namespace std;
template <typename T> void read (T &x) {x = 0; T f = 1;char tem = getchar ();while (tem < '0' || tem > '9') {if (tem == '-') f = -1;tem = getchar ();}while (tem >= '0' && tem <= '9') {x = (x << 1) + (x << 3) + tem - '0';tem = getchar ();}x *= f;}
template <typename T> void write (T x) {if (x < 0) {x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
template <typename T> T Max (T x, T y) { return x > y ? x : y; }
template <typename T> T Min (T x, T y) { return x < y ? x : y; }
template <typename T> T Abs (T x) { return x > 0 ? x : -x; }
const int Maxn = 1e5 + 5;
int n;
int a[Maxn], f[Maxn], h[Maxn], fall[Maxn];
int main () {
scanf ("%d", &n);
for (int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
}
for (int i = n; i >= 1; i--) {
fall[i] = fall[i + 1] + a[i];//后缀和
}
for (int i = n; i >= 1; i--) {
for (int j = i + 1; j <= n + 1; j++) {
if (f[j] <= fall[i] - fall[j]) {
f[i] = fall[i] - fall[j];
h[i] = h[j] + 1;
break;//选择最小的j
}
}
}
write (h[1]);
return 0;
}
由于刚才我们也提及,我们要选择最小的 j j j,也不难发现,我们的判断条件可以等价变形为 f [ j ] + f a l l [ j ] < = f a l l [ i ] f[j] + fall[j] <= fall[i] f[j]+fall[j]<=fall[i],且 f a l l [ i ] fall[i] fall[i]持续递增,所以可以使用以 f [ j ] + f a l l [ j ] f[j] + fall[j] f[j]+fall[j]为关键字的升序的单调队列来维护最小的 j j j
细节见代码
#include <stack>
#include <cmath>
#include <queue>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define LL long long
#define ULL unsigned long long
using namespace std;
template <typename T> void read (T &x) {x = 0; T f = 1;char tem = getchar ();while (tem < '0' || tem > '9') {if (tem == '-') f = -1;tem = getchar ();}while (tem >= '0' && tem <= '9') {x = (x << 1) + (x << 3) + tem - '0';tem = getchar ();}x *= f;}
template <typename T> void write (T x) {if (x < 0) {x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
template <typename T> T Max (T x, T y) { return x > y ? x : y; }
template <typename T> T Min (T x, T y) { return x < y ? x : y; }
template <typename T> T Abs (T x) { return x > 0 ? x : -x; }
const int Maxn = 1e5 + 5;
int n;
int a[Maxn], f[Maxn], h[Maxn], fall[Maxn];
int hh = 1, tt = 0;
int q[Maxn];
void add (int x) {//加入f[x]
while (hh <= tt && f[x] + fall[x] <= f[q[tt]] + fall[q[tt]])//维护升序
tt--;
q[++tt] = x;
}
int main () {
scanf ("%d", &n);
for (int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
}
for (int i = n; i >= 1; i--) {
fall[i] = fall[i + 1] + a[i];
}
add (n + 1);//可以只搭一层,即选择[i, n],所以加入 n + 1 号元素来处理这种情况来避免特判
for (int i = n; i >= 1; i--) {
while (hh < tt && f[q[hh + 1]] + fall[q[hh + 1]] <= fall[i])//找到最小的满足要求的j
hh++;
if (hh <= tt) {//存在满足要求的(由于n + 1是一定满足要求的,所以也可以不写,遵从代码习惯)
f[i] = fall[i] - fall[q[hh]];
h[i] = h[q[hh]] + 1;
}
add (i);//加入f[i]
}
write (h[1]);
return 0;
}