题目链接:POJ3017 Cut the Sequence
题目大意:
有一个长度为 (N) 的序列 (A) ,要求将 (A) 分成若干段满足每一段的数字和小于 (M) ,求“每一段的最大数字”和的最小值。
(Nleq10^5,0leq A_ileq10^6)
思路:
令 (f[i]) 为前 (i) 个数的和的最小值,先考虑朴素的转移:
[f[i]=min_{0leq j<i并且sum_{k=j+1}^i A_kleq M}left{f[j]+max_{j+1leq kleq i}{A_k}
ight}
]
时间复杂度为 (O(n^2)) ,不过注意到在所有的转移中不是所有情况都是有用的,可以发现,对于 (j) 的转移若具有优势,则必须满足下面两个条件之一:
- (A_j=max_{jleq kleq i}{A_k})
- (sum_{k=j}^i{A_k}>M) (由于 (sum_{k=j+1}^i A_kleq M),此时 (j) 为第一个转移的决策)
因为 (f[j]) 是单调递增的,以上条件可以通过调整法反证。
对于条件2,我们可以很容易 (O(n)) 地维护每一个 (i) 的第一个 (j)。
对于条件1,可以建立一个单调队列使其中的元素保证单调递增,由于 (f[j]+max_{j+1leq kleq i}{A_k}) 的大小与 (A_j) 无关,我们需要将队列中的元素对应到一个小根堆上,每次直接取堆顶转移,删除单调队列中的元素的时候同时在堆中删去此元素,小根堆可以用multiset实现,在计算插入堆的值的时候,可以发现 (max_{j+1leq kleq i}{A_k}) 的值其实就是单调队列中下一个元素的值,无需计算。(当然你硬要用RMQ我也不拦着你)
由于每个元素只进队出队一次,所以时间复杂度 (O(NlogN))。
实现细节:
- 每次循环的时候可以发现计算插入堆的值的时候单调队列中应已经有了下一个元素,所以要将队列和堆错开处理,循环到 (i) 时将 (A_i) 插入队列,到循环 (i+1) 的时候等 (A_{i+1}) 插入队列后再将 (i) 加入multiset。
- 对于满足条件2的 (j),它的 (max_{j+1leq kleq i}{A_k}) 就是队头的元素值。
- 情况1的 (j) 在转移时要判空(如 (A_i) 是前缀最大值的时候)
- 数据出现无解,当且仅当 (exists j ,A_j>M) (显然)
- 基本上要全局开long long,(M) 是 (2^{64}) 级别的。
Code:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#define N 100100
#define ll long long
using namespace std;
inline ll read(){
ll s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9')s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
return s*w;
}
ll n,m,l,r;
ll a[N],f[N],q[N];
multiset<ll> s;
int main(){
n=read(),m=read();
for(register int i=1;i<=n;i++){
a[i]=read();
if(a[i]>m){cout<<"-1";return 0;}
}
int cnt=1,l=0,r=-1;
ll tot=0;
multiset<ll>::iterator it;
for(int i=1;i<=n;i++){
tot+=a[i];
while(tot>m)tot-=a[cnt++];
while(l<=r&&q[l]<cnt){
l++,it=s.find(f[q[l-1]]+a[q[l]]);
if(l<=r&&it!=s.end())s.erase(it);
}
while(l<=r&&a[q[r]]<=a[i]){
r--,it=s.find(f[q[r]]+a[q[r+1]]);
if(l<=r&&it!=s.end())s.erase(it);
}
if(l<=r)s.insert(f[q[r]]+a[i]);
q[++r]=i;
f[i]=f[cnt-1]+a[q[l]];
if(!s.empty())f[i]=min(f[i],*s.begin());
}
cout<<f[n]<<endl;
return 0;
}