注意思路!多看几遍!
很巧妙的一道题。不再是决策点以dp值中一部分含j项为维护对象,而是通过维护条件来获取决策。
首先有个贪心策略,让底层的宽度尽可能小,才能让高度尽可能高。所以应该倒着dp,表示堆$i$~$n$的最高高度$f[i]$,同时这种最值应来源于之后的j,要在设一个$g[i]$表示以i为底层,最窄的宽度。这个的话真的只可意会啊。注意$g[i]$没人告诉你是单调的,$g[i]$之后一个不合法的决策都可能有$g[j]>g[i]$,所以单调性问题还当谨慎考虑。
所以dp方程就能出来了
$g[i]=min(sum[j-1]-sum[i-1]) $ $ g[j]<=sum[j-1]-sum[i-1]$
$f[i]=f[j]+1$
然后这个是$O(n^2)$的,考虑优化。从min内可以看出,j越小越好。那瓶颈就在于后面的那个约束条件怎么用,就是我在保证条件的情况下取j最小。
转化条件:$g[j]<=sum[j-1]-sum[i-1]$移项得$sum[j-1]-g[j]≥sum[i-1]$,而$sum[i-1]$是单调减的,那我要之前的j得插入$sum[j-1]-g[j]$,在dp到i时去把决策中大于等于$sum[i-1]$的找一个最小j。
于是单调队列维护j单调递减,$sum[j-1]-g[j]$单调减元素。假设dp完i后,我将目前i要向单调队列队尾比较。如果$sum[j-1]-g[j]>=sum[q[r]-1]-g[q[r]]$也就是j可以发挥与队尾同等的作用(甚至更优)的话,而我又渴求j尽可能小,那队尾就没什么卵用可以弹出了。直到不满足上述比较,就在队尾插入。这样保证$sum[j-1]-g[j]$单调减,我要取合法决策,便是队列头开始的一段大于等于$sum_i$的。注意,我要j最小,那队头满足条件的若干个决策,只要j最小的,那我可以依据j单调性将队头不断pop(反正之后$sum[i-1]$只会更小,现在留着也没用),直到最后一个满足条件的,就是最小的j。还是有点难以理解,贴个图。
然后每次就可以取到最小满足条件j了。
这个故事告诉我们单调队列的队头并不就一定是检查队头合法性的(范围之类的),也可以排除不优决策,这依赖于队头和队头下一个元素间的相互比较,要结合单调性予以考虑。
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<cstring> 5 #include<cmath> 6 using namespace std; 7 typedef long long ll; 8 template<typename T>inline char MIN(T&A,T B){return A>B?A=B,1:0;} 9 template<typename T>inline char MAX(T&A,T B){return A<B?A=B,1:0;} 10 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 11 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 12 template<typename T>inline T read(T&x){ 13 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 14 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 15 } 16 const int N=100000+7; 17 int sum[N],f[N],g[N],q[N]; 18 int n,l,r; 19 20 int main(){//freopen("test.in","r",stdin);//freopen("tmp.out","w",stdout); 21 read(n);for(register int i=1;i<=n;++i)sum[i]=read(sum[i])+sum[i-1]; 22 l=1,r=1;q[l]=n+1; 23 for(register int i=n;i;--i){ 24 while(l<r&&sum[q[l+1]-1]-g[q[l+1]]>=sum[i-1])++l; 25 g[i]=sum[q[l]-1]-sum[i-1],f[i]=f[q[l]]+1; 26 while(l<=r&&sum[q[r]-1]-g[q[r]]<=sum[i-1]-g[i])--r; 27 q[++r]=i; 28 } 29 printf("%d ",f[1]); 30 return 0; 31 }