单调队列优化DP——AcWing 135. 最大子序和

单调队列优化DP

定义

单调队列优化DP是一种在动态规划(Dynamic Programming, DP)中应用的数据结构优化方法。它利用单调队列(Monotonic Queue)这一数据结构来高效维护一个区间内的最值(通常是最大值或最小值),从而减少DP状态的计算量,提升算法效率。单调队列内部保持元素的单调性(递增或递减),并能快速地在队列头部获取区间最值,同时在队列尾部进行元素的插入与删除以维持单调性。

运用情况

  1. 区间最大/最小值问题:如求一个序列中所有长度为k的子数组中的最大值最小化(或最小值最大化)问题。
  2. 滑动窗口问题:如寻找一个长度为k的子序列,使其和最大。
  3. 动态规划状态优化:在某些动态规划问题中,状态转移依赖于前i个元素中的最大值或最小值,这时可以使用单调队列来避免直接遍历前i个元素。

注意事项

  1. 维护单调性:确保队列中的元素按照所需的单调性排列(递增或递减),这是单调队列的基础。
  2. 队列更新策略:当新元素加入时,及时从队列尾部移除不再影响区间最值的元素,保持队列大小不会无限增长。
  3. 边界处理:正确处理队列的初始化和结束条件,避免越界错误。
  4. 空间效率:虽然单调队列可以显著提高时间效率,但也要注意其对空间的占用,特别是在大规模数据处理时。

解题思路

  1. 明确问题:识别问题中涉及的区间最值查询需求,判断是否可以通过维护单调性来优化。
  2. 状态定义:定义DP状态,明确状态转移方程。
  3. 引入单调队列:设计队列的插入与删除规则,确保队列始终保持所需单调性,并能快速提供区间最值。
  4. 状态转移优化:利用单调队列查询区间最值代替遍历,优化DP状态的计算。
  5. 实现细节:编写代码实现,特别注意队列的维护逻辑,确保在每个DP状态转移时正确更新队列。
  6. 验证与调试:检查边界条件和特殊情况,确保算法的正确性和效率。

AcWing 135. 最大子序和

题目描述

135. 最大子序和 - AcWing题库

运行代码

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;
}

代码思路

  1. 输入与预处理:

    • 首先读入两个整数n和m,分别代表序列的长度和需要考虑的子数组长度。
    • 接着读入一个长度为n的整数序列a[],并通过累加生成一个新的序列,使得a[i]表示原序列前i个元素的和。这种预处理是为了方便计算任意子数组的和。
  2. 单调队列设计:

    • 定义一个单调递减的双端队列q[],用来存储序列a[]的索引。队列中的元素索引对应的a值是递减的,这样队头元素总是队列中对应a值最大的位置。
    • 初始化队列的头指针hh和尾指针tt为0。
  3. 遍历与优化:

    • 遍历更新序列a[]的每一个元素i时,首先检查队列中的最前面的索引是否超出了当前子数组长度m的范围,如果超出了则从队列头部弹出索引,保证队列中的都是可能参与形成长度为m的子数组的索引。
    • 计算当前子数组a[q[hh]]到a[i]的和,并更新结果res为这个和与当前最大差值中的较大者。这样做的目的是找到最大的子数组和,因为我们要找的是这些和中的最小值,所以用负数表示并取最大值来间接实现。
    • 维护单调队列的性质:如果新加入的a[i]比队列尾部的元素小(即a[q[tt]] >= a[i]),说明队尾元素不可能再成为未来更优解的一部分,因此将其从队列中移除。然后将当前索引i加入队列尾部。
  4. 输出结果:遍历结束后,变量res存储了所有长度为m的子数组和的最大值的负数形式,输出其正值即为所求的最小值。

改进思路

  1. 明确注释:添加详细的注释,尤其是对于算法核心逻辑部分,可以帮助阅读者更快理解代码意图。

  2. 变量命名清晰化 :虽然hh, tt, q[]等变量在竞赛编程中较为常见且简短,但对于长期维护的项目,使用更具描述性的变量名如queueHead, queueTail, indicesQueue[]可能会更好。

  3. 常量定义 :将负无穷大(-3E9)这样的magic number定义为常量,提高代码可读性和可维护性。例如,定义const long long INF = -3E9;

  4. 异常处理:考虑增加对输入数据的合法性检查,比如n和m是否合理,输入数组是否有非法值等。

  5. 优化输出格式:对于输出结果,根据实际需要可能要调整格式,比如添加单位、保留小数等。

相关推荐
浮生如梦_31 分钟前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
励志成为嵌入式工程师2 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
wheeldown3 小时前
【数据结构】选择排序
数据结构·算法·排序算法
观音山保我别报错5 小时前
C语言扫雷小游戏
c语言·开发语言·算法
TangKenny6 小时前
计算网络信号
java·算法·华为
景鹤6 小时前
【算法】递归+深搜:814.二叉树剪枝
算法
iiFrankie6 小时前
SCNU习题 总结与复习
算法
Dola_Pan7 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法