蓝书(算法竞赛进阶指南)刷题记录——BZOJ1233 干草堆(贪心+DP+单调队列)

题目:BZOJ1233.
题目大意:给定一个长度为 n n n的正整数序列 a a a,要求把序列分成若干段,使得每一段的权值和大于下一段的权值和,求最多可以分成多少段.
1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1n105.

这道题首先有一个非常神奇的贪心,就是当分的段最多的时候,必然是让前面的段的和尽量小最好.

这个结论虽然显然,但是并不容易想到利用这个结论DP.

有了这个结论后,考虑正着DP不好办,所以把序列反过来,现在分段的要求变成了每一段的和必须比下一段小.

考虑设 f [ i ] f[i] f[i]表示 a [ 1.. i ] a[1..i] a[1..i]分成若干段之后最后面一段和的最小值, g [ i ] g[i] g[i]表示最大分段数.

由于在 f [ i ] f[i] f[i]最优的时候必然有 g [ i ] g[i] g[i]最优,所以我们可以考虑直接DP出 f [ i ] f[i] f[i]的同时计算出 g [ i ] g[i] g[i].

s [ i ] s[i] s[i] a [ i ] a[i] a[i]翻转后的前缀和,可以列出方程:
f [ i ] = min ⁡ j &lt; i , f [ j ] ≤ s [ i ] − s [ j ] { s [ i ] − s [ j ] } f[i]=\min_{j&lt;i,f[j]\leq s[i]-s[j]} \{ s[i]-s[j] \}\\ f[i]=j<i,f[j]s[i]s[j]min{s[i]s[j]}

p p p f [ i ] f[i] f[i]的转移点,那么:
g [ i ] = g [ p ] + 1 g[i]=g[p]+1 g[i]=g[p]+1

然后我们就可以开心快乐的写出一个 O ( n 2 ) O(n^2) O(n2)的DP了,然而会TLE…

现在我们考虑一下如何优化这个转移过程,考虑把方程的转移条件变形:
f [ j ] ≤ s [ i ] − s [ j ] ⇔ f [ j ] + s [ j ] ≤ s [ i ] f[j]\leq s[i]-s[j]\Leftrightarrow f[j]+s[j]\leq s[i] f[j]s[i]s[j]f[j]+s[j]s[i]

容易想到 s [ i ] s[i] s[i]是单调递增的,这可以说明当前 j j j合法则接下来 j j j一定合法,而最终转移点又一定是所有合法转移点中最后面的那个,这证明转移点是具有单调性的.

还有一点,我们会发现若 k &lt; j k&lt;j k<j f [ k ] + s [ k ] ≥ f [ j ] + s [ j ] f[k]+s[k]\geq f[j]+s[j] f[k]+s[k]f[j]+s[j],则显然 k k k合法时 j j j也一定合法,而且 k k k还没有 j j j优,这样就可以及时剔除 k k k了.

容易想到维护一个单调队列,每次若队头以及队列中第二个元素都合法,则将队头出队;并同时维护队尾出队即可.

时间复杂度 O ( n ) O(n) O(n).

代码如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=100000;

int n,a[N+9];
int dp[N+9],cnt[N+9];
int q[N+9],hd,tl;

Abigail into(){
  scanf("%d",&n);
  for (int i=1;i<=n;++i)
    scanf("%d",&a[i]);
}

Abigail work(){
  reverse(a+1,a+n+1);
  for (int i=1;i<=n;++i) a[i]+=a[i-1];
  q[hd=tl=1]=0;
  for (int i=1;i<=n;++i){
    for (;hd<tl&&dp[q[hd+1]]+a[q[hd+1]]<=a[i];++hd);
    dp[i]=a[i]-a[q[hd]];cnt[i]=cnt[q[hd]]+1;
    for (;hd<=tl&&dp[q[tl]]+a[q[tl]]>=dp[i]+a[i];--tl);
    q[++tl]=i;
  }
}

Abigail outo(){
  printf("%d\n",cnt[n]);
}

int main(){
  into();
  work();
  outo();
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值