[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方程:
設
則
我們發現這個方程的時間複雜度為
,空間複雜度為
,實在是太太太大了。
我們嘗試優化這個方程,但是我們發現無法優化時空都到 的級別。這時候我們從結論入手,設列一個新的dp方程,且可以優化到 級別。
結合貪心的思路嘗試倒推:
設
為用i~n來構成的乾草堆,底層最短是多少
表示狀態f[i]的最大高度
則
由於sum[i]具有單調性,所以我們從單調性開始著手使用單調佇列優化d的套路:
在轉移方程中可以得到:
從較小的
轉移過來會更優秀,所以符合條件的
越小越好 —(1)
然後在判斷式中可以看到:
移項,把關於
,
的分別移到兩邊
那麼我們又可以得到:
對於狀態i的當前決策
,
越大 ,可以作為決策的情況就越多,這樣的j越有用。—(2)
所以我們有了兩個分別關於下標和式子的單調條件(1)和(2)
所以我們設兩個決策
,
滿足
,且
的話,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;
}