1. 程式人生 > >[BZOJ1233][Usaco2009Open]乾草堆tower(單調佇列優化)

[BZOJ1233][Usaco2009Open]乾草堆tower(單調佇列優化)

傳送門

題意搞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;
}