单调队列优化DP
定义
单调队列优化DP是一种在动态规划(Dynamic Programming, DP)中应用的数据结构优化方法。它利用单调队列(Monotonic Queue)这一数据结构来高效维护一个区间内的最值(通常是最大值或最小值),从而减少DP状态的计算量,提升算法效率。单调队列内部保持元素的单调性(递增或递减),并能快速地在队列头部获取区间最值,同时在队列尾部进行元素的插入与删除以维持单调性。
运用情况
- 区间最大/最小值问题:如求一个序列中所有长度为k的子数组中的最大值最小化(或最小值最大化)问题。
- 滑动窗口问题:如寻找一个长度为k的子序列,使其和最大。
- 动态规划状态优化:在某些动态规划问题中,状态转移依赖于前i个元素中的最大值或最小值,这时可以使用单调队列来避免直接遍历前i个元素。
注意事项
- 维护单调性:确保队列中的元素按照所需的单调性排列(递增或递减),这是单调队列的基础。
- 队列更新策略:当新元素加入时,及时从队列尾部移除不再影响区间最值的元素,保持队列大小不会无限增长。
- 边界处理:正确处理队列的初始化和结束条件,避免越界错误。
- 空间效率:虽然单调队列可以显著提高时间效率,但也要注意其对空间的占用,特别是在大规模数据处理时。
解题思路
- 明确问题:识别问题中涉及的区间最值查询需求,判断是否可以通过维护单调性来优化。
- 状态定义:定义DP状态,明确状态转移方程。
- 引入单调队列:设计队列的插入与删除规则,确保队列始终保持所需单调性,并能快速提供区间最值。
- 状态转移优化:利用单调队列查询区间最值代替遍历,优化DP状态的计算。
- 实现细节:编写代码实现,特别注意队列的维护逻辑,确保在每个DP状态转移时正确更新队列。
- 验证与调试:检查边界条件和特殊情况,确保算法的正确性和效率。
AcWing 135. 最大子序和
题目描述
运行代码
cpp
#include <iostream>
using namespace std;
const int N = 3E5 + 10;
int n, m;
int a[N], q[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> a[i], a[i] += a[i - 1];
long long res = -3E9;
int hh = 0, tt = 0;
for(int i = 1; i <= n; i ++ )
{
if(i - q[hh] > m) hh ++;
res = max(res, 1ll * a[i] - a[q[hh]]);
while(hh <= tt && a[i] <= a[q[tt]]) tt --;
q[ ++ tt] = i;
}
cout << res;
}
代码思路
-
输入与预处理:
- 首先读入两个整数n和m,分别代表序列的长度和需要考虑的子数组长度。
- 接着读入一个长度为n的整数序列a[],并通过累加生成一个新的序列,使得a[i]表示原序列前i个元素的和。这种预处理是为了方便计算任意子数组的和。
-
单调队列设计:
- 定义一个单调递减的双端队列q[],用来存储序列a[]的索引。队列中的元素索引对应的a值是递减的,这样队头元素总是队列中对应a值最大的位置。
- 初始化队列的头指针hh和尾指针tt为0。
-
遍历与优化:
- 遍历更新序列a[]的每一个元素i时,首先检查队列中的最前面的索引是否超出了当前子数组长度m的范围,如果超出了则从队列头部弹出索引,保证队列中的都是可能参与形成长度为m的子数组的索引。
- 计算当前子数组a[q[hh]]到a[i]的和,并更新结果res为这个和与当前最大差值中的较大者。这样做的目的是找到最大的子数组和,因为我们要找的是这些和中的最小值,所以用负数表示并取最大值来间接实现。
- 维护单调队列的性质:如果新加入的a[i]比队列尾部的元素小(即a[q[tt]] >= a[i]),说明队尾元素不可能再成为未来更优解的一部分,因此将其从队列中移除。然后将当前索引i加入队列尾部。
-
输出结果:遍历结束后,变量res存储了所有长度为m的子数组和的最大值的负数形式,输出其正值即为所求的最小值。
改进思路
-
明确注释:添加详细的注释,尤其是对于算法核心逻辑部分,可以帮助阅读者更快理解代码意图。
-
变量命名清晰化 :虽然
hh
,tt
,q[]
等变量在竞赛编程中较为常见且简短,但对于长期维护的项目,使用更具描述性的变量名如queueHead
,queueTail
,indicesQueue[]
可能会更好。 -
常量定义 :将负无穷大(-3E9)这样的magic number定义为常量,提高代码可读性和可维护性。例如,定义
const long long INF = -3E9;
-
异常处理:考虑增加对输入数据的合法性检查,比如n和m是否合理,输入数组是否有非法值等。
-
优化输出格式:对于输出结果,根据实际需要可能要调整格式,比如添加单位、保留小数等。