题解 P4954 【[USACO09Open] Tower of Hay 干草塔】

· · 题解

单调队列 动态规划

背景

小萌新 \varnothing 初遇此题是在模拟赛上,私以为数据很水,果断贪心.......

思路

相信很多人看到此题会想到贪心:让高层尽可能的小,从后往前倒叙贪心。

方面起见,我们倒叙读入,从前向后贪心使每段尽可能的小。

那么可以枚举 第一层由 a[1]\sim a[i] 构成,设从前往后到了 a[i],当前层的宽度为 w,那么就从 i 再往前后连续一段,使得这一段的长度恰好大于 w

a[i] 求前缀和 s[i],也就有了以下的代码:

for(int i = 1; i <= n and s[i]*ans <= s[n]; ++i) {
    int w = s[i], cur = i, pre, cnt = 1;
    while(s[cur] + w <= s[n]) { 
        pre = cur; 
        cur = lower_bound(s+cur+1, s+n+1, s[cur]+w) - s;
        w = s[cur] - s[pre];
        ++cnt;
    } 
    ans = max(ans, cnt);
}

对于随机数据还是表现很不错的,但随手卡一下就会挂掉。

举个例子:

6
9 8 2 1 5 5 

倒叙之后是 5\ 5\ 1\ 2\ 8\ 9,贪心划分结果为 5\ |\ 5\ |\ 1\ 2\ 8\ 9 共三层,然而正确答案是 5\ |\ 5\ 1\ 2\ |\ 8\ |\ 9 共四层。

可以看出,贪心错误之处在于 前面的贪心使得 底层过大,难以扩展。

我们大胆猜测,有一个较为贪心的结论:

构成最优解的若干种方案中,一定有一种满足底层最小,即 底层最小的方案 一定可以 构造出最高的层数

此结论由 ZKW 大神严谨证明。

一样,我们先把 a 数组翻转过来,前缀和为 s[i]

转移方程: $$f[i]=f[j]+1(\text{ $j$ 为满足 } j\in[0,i-1]\text{ 且 } g[j]\leqslant s[i]-s[j] \text{的最后一个 }j)$$ 要求最后一个 $j$ 是由于我们想要此时最后一层的宽度尽可能的小,得到 $j$ 后有 $g[i]=s[i]-s[j]$, 边界条件 $f[0]=0$。 转移复杂度 $O(n)$,总复杂度 $O(n^2)$。 - 考虑单调队列优化 转移条件为 $g[j]\leqslant s[i]-s[j]$,即 $g[j]+s[j]\leqslant s[i]$,那么就会有一个显然的结论: 若有 $k<j$ 且 $g[k]+s[k]\geqslant g[j]+s[j]$,则 $k$ 永远不可能再转移别人了,因为如果 $k$ 满足转移条件,那么 $j$ 也会满足,而 $j$ 的位置还更加靠后。 因此,我们可以维护一个 $g+s$ 值单调递增的 单调队列。 每次转移时从 单调队列中找到 最后一个 $g[j]+s[j]\leqslant s[i]$ 的位置 $j$,单调队列中 $j$ 以左的元素从队首出队。 转以后从队尾弹出 $g[j]+s[j]\geqslant g[i]+s[i]$ 的元素,然后将位置 $i$ 加入单调队列。 每个元素进出队列各一次,复杂度 $O(n)$。 ### 代码 ```cpp #include <cstdio> #include <iostream> using namespace std; const int maxn = 100005; int n, a[maxn], s[maxn], f[maxn], g[maxn]; int q[maxn], l, r; int main() { scanf("%d", &n); for(int i = n; i; --i) scanf("%d", a+i); for(int i = 1; i <= n; ++i) s[i] = s[i-1] + a[i]; for(int i = 1; i <= n; ++i) { while(l < r and s[q[l+1]]+g[q[l+1]] <= s[i]) ++l; f[i] = f[q[l]] + 1; g[i] = s[i] - s[q[l]]; while(l < r and s[q[r]]+g[q[r]] >= s[i]+g[i]) --r; q[++r] = i; } printf("%d\n", f[n]); return 0; } ```