题意
给定n个数字,将它们分成连续的若干个区间。满足后一个区间中的数字之和一定不大于前一个区间中的数字之和。求最多可以分成多少个区间。
思路
显然,本题可以用动态规划进行求解。
定义 \(f[i]\)表示最后一个区间的右端点为第 \(i\) 个数的时候所能分成的最大区间数。
但是只靠 \(f[i]\)数组无法满足题目中的限制条件。于是需要定义 \(g[i]\) 表示在最优的状态下最后一个区间内的数字和。同时定义一个 \(s[i]\) 表示前 \(i\) 个数字之和。(这两个数组的具体用法下面状态转移方程会提到)
有一个很显然的贪心性质,每一个区间的数字之和越小越好。
但是如果直接正向枚举。会发现一个很奇怪的问题。由于需要满足上面的贪心性质,所以第一个区间就会特别小,而区间数字之和又要满足非严格单调递减,于是就可能会出现即使一个数字单独成为一个区间还是不合法的尴尬局面。于是可以考虑倒序枚举,就可以避免下一个区间放不下一个数字的局面。
于是就可以列出状态转移方程:
设 \(0 \leq j \leq i\) ,若满足 \(g[j] \leq s[i] - s[j]\) ,那么就有
\(f[i]= max(f[j]+1)\) 。
这个转移方程还是比较好理解的。但是 \(O(n^2)\) 的时间复杂度显然无法直接通过本题。于是需要用到单调队列优化转移方程。
观察之前的状态转移方程的条件 \(g[j] \leq s[i] - s[j]\) ,移项,得 \(g[j]+s[j] \leq s[i]\)。而不等式的左边为定值,于是就可以对不等式左边的值进行单调队列优化。
有一个很显然的性质,如果 \(k \le j\) 且 \(s[k]+g[k] \geq s[j]+g[j]\) 。那么 \(k\) 就一定无法成为最优状态。因为对于 \(i\) 来说。\(i\) 所在的这一个区间内的数字之和一定要在满足不小于前一个区间的数字和的前提下越小越好。 如果 \(k\) 比 \(j\) 小,但是它所在的那一段区间长度反而比 \(j\) 还要长。那么肯定将区间分成了更少的部分,显然违背了之前提到的贪心性质。所以就不可能成为最优解了。于是维护一个满足 \(g[i]+s[i]\) 单调递增(也就是队头为区间最小值)的队列。(队尾出队判断)
同时,由于本题没有对区间长度进行限制,那么根据贪心性质,肯定是在满足 \(g[j]+s[j] \leq s[i]\) 的前提下 \(j\) 越靠近右边越好。(队头出队判断)
于是就可以做到 \(O(n)\) 时间内通过本题。
code:
#include<iostream>
#include<cstdio>
#include<deque>
using namespace std;
const int M=1e6+10;
deque<int> q;
int n,w[M],sum[M],f[M],g[M];
int calc(int k)
{
return sum[k]+g[k];
}
int main()
{
scanf("%d",&n);
for(int i=n;i>=1;i--) scanf("%d",&w[i]);
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+w[i];
int last=0;
for(int i=1;i<=n;i++)
{
while(q.size()&&sum[i]>=g[q.front()]+sum[q.front()])
{
last=q.front();//由于本人比较懒,不想写模拟双向队列判断出队。所以需要一个last来记录最后一个满足s[i]+g[i]<=s[i] 的下标
q.pop_front();
}
g[i]=sum[i]-sum[last];
f[i]=f[last]+1;
while(q.size()&&calc(q.back())>calc(i)) q.pop_back();
q.push_back(i);
}
printf("%d\n",f[n]);
return 0;
}