https://vjudge.net/problem/POJ-3017
题目
给一个长度为$N$的序列,你需要把它切成几段,每一段的和不能超过$M$,求一种切法,使每一段的最大值的和最小。
$Nleqslant100000$,$M$不会爆long long,序列中的数在$[0,1000000]$
题解
$dp[i]=min{dp[j]+max{a[j+1],cdots,a[i]} | s[i]-s[j]leqslant M}$
时间复杂度$mathcal{O}(n^2)$
假设最优的转移是$j$,那么想必要条件
$dp[j]+max{a[j+1],cdots,a[i]}<dp[j+1]+max{a[j+2],cdots,a[i]}$
因为$dp[i]$单调递增,所以必要条件是$j==N$或$a[j+1]leqslantmax{a[j+2],cdots,a[i]}$即$a[j+1] emax{a[j+1],cdots,a[i]}$
$dp[j]+max{a[j+1],cdots,a[i]}<dp[j-1]+max{a[j],cdots,a[i]}$
必要条件是$s[i]-s[j-1]>M$或$a[j]geqslantmax{a[j+1],cdots,a[i]}$,即$a[j]=max{a[j],cdots,a[i]}$
第二个必要条件好用一些,可以利用单调队列优化,可以在$mathcal{O}(1)$时间得到每个$i$必要的决策
由于要求值最小的决策,可以使用multiset,时间复杂度$mathcal{O}(nlog n)$
AC代码
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<vector> #include<set> #define REP(i,a,b) for(register int i=(a); i<(b); i++) #define REPE(i,a,b) for(register int i=(a); i<=(b); i++) #define PERE(i,a,b) for(register int i=(a); i>=(b); i--) using namespace std; typedef long long ll; #define MAXN 100007 int N; ll M; int a[MAXN]; ll dp[MAXN]; int ma[MAXN]; multiset<ll> ms; typedef multiset<ll>::iterator msi; int main() { scanf("%d%lld", &N, &M); REPE(i,1,N) { scanf("%d", &a[i]); if(a[i]>M) {puts("-1"); return 0;} } dp[0]=0; int l1=1,l2=0,r2=0; ll ss=0; REPE(i,1,N) { ss+=a[i]; while(ss>M) ss-=a[l1++]; //l1<i while(l2<r2 && ma[l2]<l1) { int x=ma[l2]; if(l2+1<r2) { msi it=ms.find(dp[x]+a[ma[l2+1]]); if(it!=ms.end()) ms.erase(it); } l2++; } while(l2<r2 && a[ma[r2-1]]<a[i]) { int x=ma[r2-1]; if(r2-2>=l2) { msi it=ms.find(dp[ma[r2-2]]+a[x]); if(it!=ms.end()) ms.erase(it); } r2--; } if(r2>l2) { ms.insert(dp[ma[r2-1]]+a[i]); } ma[r2++]=i; dp[i]=dp[l1-1]+a[ma[l2]]; if(!ms.empty()) { dp[i]=min(dp[i], *ms.begin()); } } printf("%lld ", dp[N]); }