[BZOJ1233][Usaco2009Open]干草堆tower(单调队列优化)

版权声明:本文为博主原创文章,转载请附上原博客链接。 https://blog.csdn.net/CABI_ZGX/article/details/83054311

传送门

题意搞skr人…,其实就是堆方块:
有n(n<=100000)个干草,每堆有个宽度,现在要且分成若干段,把每一段的干草按顺序堆起来形成一个多层的干草堆(所以下标越小的干草堆放在越下面)且宽度要逐层非严格递减(上面一层的宽度<=下面一层的宽度),求最多可以放多少层。


好神啊这题。。

题意看起来不复杂,所以我们很容易想到一个贪心
从上往下倒着考虑,假设我们已经知道上面一层的宽度为w,那么下面一层的最优的策略一定是的大于等于w的宽度和中最靠近w的。

想出贪心之后我们需要证明他的正确性,但是也可以证明贪心是错的:
在bzoj该题的Discuss下id为thy的大佬已经举出了一个例子证明贪心是错的:

ex:
16
6 1 1 1 6 1 1 1 1 6 1 1 1 1 1 6
贪心:
6
111116
111161116
最优:
6111
1161
1116
1116
为什么是错的呢?
因为第i层可以帮第i+1层分担一些宽度,使得i+2层压力更小

但是我们可以在证明贪心是错误的过程中模糊地猜到一个结论
假如该问题的一个解是最优解,那么它的最底层宽度一定最小。

这个结论有没有用我们现在还不知道,但是zory左老师有言:

每一个题目肯定有每一个题目的特质,肯定要抓住这个特质来解题。

为啥,因为最底层最小可以让干草堆叠的尽量的高嘛…

然后我们往dp去想,设列一个初级dp方程:
f [ i ] [ j ] f[i][j] 表示用前i包干草,且当前层用的是第j包到第i包所能达到的最大高度。
f [ i ] [ j ] = m a x { f [ j 1 ] [ k ] } + 1 ( s u m [ i ] s u m [ j 1 ] s u m [ j 1 ] s u m [ k 1 ] , k &lt; j i ) f[i][j]=max\lbrace f[j-1][k]\rbrace+1(sum[i]-sum[j-1] \leq sum[j-1]-sum[k-1],k&lt;j\leq i)
我们发现这个方程的时间复杂度为 O ( N 3 ) O(N^3) ,空间复杂度为 O ( N 2 ) O(N^2) ,实在是太太太大了。

我们尝试优化这个方程,但是我们发现无法优化时空都到 O ( N ) O(N) 的级别。这时候我们从结论入手,设列一个新的dp方程,且可以优化到 O ( N ) O(N) 级别。

结合贪心的思路尝试倒推

f [ i ] f[i] 为用i~n来构成的干草堆,底层最短是多少
g [ i ] g[i] 表示状态f[i]的最大高度

f [ i ] = m i n { s u m [ j 1 ] s u m [ i 1 ] } ( i &lt; j n ,     f [ j ] s u m [ j 1 ] s u m [ i 1 ] ) f[i]=min\lbrace sum[j-1]-sum[i-1]\rbrace(i&lt;j\leq n,\ \ \ f[j] \leq sum[j-1]-sum[i-1])
g [ i ] = g [ j ] + 1 g[i]=g[j]+1

由于sum[i]具有单调性,所以我们从单调性开始着手使用单调队列优化d的套路:
在转移方程中可以得到:
f [ i ] f[i] 从较小的 j j 转移过来会更优秀,所以符合条件的 j j 越小越好 —(1)

然后在判断式中可以看到:
f [ j ] s u m [ j 1 ] s u m [ i 1 ] f[j] \leq sum[j-1]-sum[i-1]
移项,把关于 i i j j 的分别移到两边
s u m [ i 1 ] s u m [ j 1 ] f [ j ] sum[i-1] \leq sum[j-1]-f[j]
那么我们又可以得到:
对于状态i的当前决策 j j s u m [ j 1 ] f [ j ] sum[j−1]−f[j] 越大 ,可以作为决策的情况就越多,这样的j越有用。—(2)

所以我们有了两个分别关于下标和式子的单调条件(1)和(2)
所以我们设两个决策 j j k k 满足 k &gt; j k&gt;j ,且 s u m [ k 1 ] f [ k ] s u m [ j 1 ] j f [ j ] sum[k-1]-f[k] \leq sum[j-1]-jf[j] 的话,k就是无用状态可以删去。

用单调队列维护即可,注意由于我们是倒推所以单调队列的head和tail要反过来(因为我们默认head<=tail)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
typedef long long ll;
const int N=1e5+10;
int a[N],sum[N];
int list[N],head,tail;
int f[N],g[N];
int main()
{
    int n;scanf("%d",&n);
    sum[0]=0;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    list[1]=n+1; head=tail=1; //注意由于倒序所以head和tail反过来 
    for(int i=n;i>=0;i--)
    {
        while(head<tail && f[list[head+1]]<=sum[list[head+1]-1]-sum[i-1]) head++;
        int j=list[head];
        f[i]=sum[j-1]-sum[i-1];
        g[i]=g[j]+1;
        while(head<=tail && sum[i-1]-f[i]>=sum[list[tail]-1]-f[list[tail]]) tail--;
        list[++tail]=i;
         
    }
    printf("%d\n",g[1]);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/CABI_ZGX/article/details/83054311