题目
分析
前缀和
看到要不断地求一个区间的和,是使用“前缀和”的强烈提示。只要求出前缀和,就可以在\(O(1)\)的时间里求出任意区间\([l, r]\)的和:\(prefix\_sum[r]-prefix\_sum[l-1]\)。
for (int i = 1; i <= n; i++)
{
cin >> a[i];
prefix_sum[i] = prefix_sum[i - 1] + a[i];
}
单调队列
同时,求区间极值的问题,也是使用“单调队列”的强烈提示。它的基本算法如下:
结合流程图进一步说明如下:
while (!q.empty() && i - q.front() > m)
{
q.pop_front();
}
这段代码是为了保证单调队列中所有的“位置”都是和当前考虑的位置i
在大小为m
的区间里。如果i=q.front()>=m
就说明q.front()
所在的位置已经超出了,不用再考虑。
if (!q.empty())
{
max_sum = max(max_sum, prefix_sum[i] - prefix_sum[q.front()]);
}
这段代码是计算以当前位置结尾的最大子段和。注意,这里只减了一次,而m
是个区间。为什么不需要循环处理呢?我们继续看代码。
//第三段代码
while (!q.empty() && prefix_sum[q.back()] >= prefix_sum[i])
{
q.pop_back();
}
这段代码维护单调队列,保证队列中的元素对应的前缀和是单调递增的。
我们回头来讨论m
虽然定义了一个区间,但我们不用循环处理的问题。
考虑两种情况。
第一种情况,元素都是正数,因此前缀和都是递增的。例如:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
元素 | - | 1 | 2 | 4 |
前缀和 | 0 | 1 | 3 | 7 |
这种情况下,要想获得一个更大的区间和,在右边界确定(i
)的前提下,左边界越小越好。也就是说,这样才能加上更多的正数,所以和会更大。
第二种情况,元素是正负交错的,因此前缀和不再严格递增。例如:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
元素 | - | 1 | -2 | 4 |
前缀和 | 0 | 1 | -1 | 3 |
这种情况下,要想获得一个更大的区间和,在右边界确定(i
)的前提下,左边界越大越好。也就是说,这样才能避免加上负数,所以和会更大。
体现在代码上就是上面的第三段代码。
答案
思考
本题的关键有两个:前缀和以及(前缀和)的单调递增队列,从而保证了只要用队首下标进行计算,不用考虑m
所蕴含的区间,提升了效率,整个算法复杂度降低为\(O(n)\)。