题意:给定一个有n个数字的序列,现要将此序列分成几段,使得每段数字的和不大于给定的数字m。并且求使得各段和的最大值最小。
思路:一个n^2的dp比较容易想到。令f[i] 表示前i个数按照题目要求的最小的和,则必然有f[i] = min(f[j] + max(a[j +1 , a[j + 2].....a[i])) ,其中j<= i,j的位置还得满足题目中m的限制。由于a数组都是大于0的,所以可以发现f必然是非递减的。设a[j + 1], a[j + 2], ...a[i]中值最大的下标为k,设x为[j + 1,k]的任意一个下标,则a[x],a[x+1],....a[i]的最大值的下标显然也是k了。由f的非递减性,f[j+1] + a[k] <= f[j+2]+a[k].....<= f[k - 1] + a[k] 。很显然,我们只要取f[j+1]+a[k]就可以了。也就是说如果某一段到当前i位置的最大值都一样,取最靠前的即可。
如何维护呢,可以联想到单调队列。维护一个递减的队列,存的是符合要求的某一段的最大值,但是可以发现,并不是队首元素就是最优,因为队列中的递减性质,队列中的所有元素都有可能导致最优解。
#include <stdio.h> #include <string.h> #define min(a,b) ((a)<(b)?(a):(b)) #define N 100005 int n; __int64 m,sum=0; int s[N],dp[N],q[N],front,rear; int main(){ int i,j,k,flag = 1; freopen("a.txt","r",stdin); scanf("%d %I64d",&n,&m); for(i = 1;i<=n;i++){ scanf("%d",&s[i]); if(s[i]>m) flag = 0; } if(!flag) printf("-1\n"); else{ dp[0] = 0; front = rear = -1; for(i = j = 1;i<=n;i++){ sum += s[i]; while(sum > m) sum-=s[j++];//找到当前段和不大于m的起点 while(front < rear && q[front+1]<j) front++; while(front < rear && s[q[rear]]<=s[i])//更新队列 rear--; q[++rear] = i; dp[i] = dp[j-1]+s[q[front+1]]; for(k = front+2;k<=rear;k++) dp[i] = min(dp[i], dp[q[k-1]]+s[q[k]]); } printf("%d\n",dp[n]); } return 0; }