每日c/c++题 备战蓝桥杯(洛谷P1440 求m区间内的最小值 详解(单调队列优化))

洛谷P1440 求m区间最小值:单调队列优化详解(从暴力到O(n)的蜕变)


tags: [算法, 数据结构, 滑动窗口, 洛谷, C++]

引言

在处理序列数据的区间查询问题时,暴力枚举往往难以应对大规模数据。本文以洛谷P1440为切入点,深入剖析如何通过单调队列数据结构,将时间复杂度从O(nm)优化至O(n),完美解决百万级数据下的实时查询需求。

题目解析

问题定义

给定长度为n的序列和滑动窗口大小m,要求输出每个窗口内的最小值。当m>n时输出全0。

输入格式

  • 第一行:两个整数n, m
  • 第二行:n个整数表示序列元素

输出格式

输出n-m+1行,每行一个整数表示对应窗口的最小值

数据范围

  • 1 ≤ m ≤ n ≤ 2×10⁶
  • 元素值域:1 ≤ a_i ≤ 10⁹

算法演进:从暴力到优化

暴力解法(O(nm))
cpp 复制代码
for (int i=0; i<=n-m; i++) {
    int min_val = INF;
    for (int j=i; j<i+m; j++) {
        min_val = min(min_val, a[j]);
    }
    cout << min_val << endl;
}

缺陷:当n=2×10⁶时,总操作次数达4×10¹²次,远超时限。

单调队列优化(O(n))

核心思想:维护一个双端队列,使其满足:

  1. 队列元素对应值严格单调递增
  2. 队首元素始终是当前窗口的最小值下标
  3. 自动清理失效元素(超出窗口范围或被新元素"淘汰")

代码实现与优化

完整代码(C++)
cpp 复制代码
#include <iostream>
using namespace std;

const int MAXN = 2e6 + 5;
int a[MAXN];       // 原始数组(1-based)
int deque[MAXN];    // 手动实现双端队列
int front = 0, rear = -1;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n, m;
    cin >> n >> m;
    
    // 特殊情况处理
    if (m > n) {
        for (int i=0; i<n; i++) cout << "0\n";
        return 0;
    }

    for (int i=1; i<=n; i++) cin >> a[i];

    // 初始化队列(前m个元素)
    for (int i=1; i<=m; i++) {
        while (rear >= front && a[deque[rear]] >= a[i]) rear--;
        deque[++rear] = i;
    }

    // 处理剩余窗口
    for (int i=m+1; i<=n; i++) {
        cout << a[deque[front]] << "\n";
        
        // 移除失效队首
        while (deque[front] <= i - m) front++;
        
        // 维护单调性
        while (rear >= front && a[deque[rear]] >= a[i]) rear--;
        deque[++rear] = i;
    }
    
    // 输出最后一个窗口
    cout << a[deque[front]] << "\n";

    return 0;
}
关键优化点
  1. 输入优化

    • 关闭同步流ios::sync_with_stdio(false)
    • 解除cin绑定cin.tie(nullptr)
      使输入速度提升3-5倍
  2. 数组下标处理

    • 采用1-based索引,避免处理0号元素的特殊情况
    • 显式处理m>n的边界条件
  3. 空间优化

    • 使用静态数组模拟双端队列,避免STL动态内存分配的开销
    • 相比STL deque,内存访问更连续,缓存命中率更高

算法流程详解

以输入序列[1,3,2,4,5], m=3为例:

窗口位置 队列状态(下标) 最小值 操作说明
[1,3,2] 1 → 3 → 2 1 初始化队列
[3,2,4] 2 → 4 2 移除过期1号元素,加入4
[2,4,5] 2 → 4 → 5 2 维护单调性,加入5

执行流程

  1. 初始化阶段:处理前m个元素,构建初始单调队列
  2. 滑动阶段
    • 输出当前窗口最小值(队首元素)
    • 移除窗口左边界外的元素
    • 从队尾开始移除所有≥新元素的元素,保持单调性
    • 将新元素下标加入队列

复杂度分析

指标 复杂度 说明
时间复杂度 O(n) 每个元素最多入队、出队各一次
空间复杂度 O(m) 队列长度不超过窗口大小

常见问题解决方案

  1. 队列为空检查

    通过while (rear >= front)保证队列操作安全性

  2. 重复元素处理

    使用>=而非>判断,确保相同值元素正确弹出,维护严格单调性

  3. 窗口左移边界

    deque[front] <= i - m时,队首元素已不在当前窗口

扩展应用场景

  1. 滑动窗口最大值:修改比较符号方向即可
  2. 动态规划优化:如最大矩形面积问题(LeetCode 84)
  3. 实时流处理:维护最近m条记录的统计信息

总结

单调队列通过三个核心操作:

  • 失效元素清理:保证窗口有效性
  • 单调性维护:确保最小值在队首
  • 元素淘汰机制:避免无效比较

实现了时间复杂度的质的飞跃。该算法思想是解决滑动窗口类问题的基石,掌握其精髓对算法进阶至关重要。建议配合LeetCode 239(滑动窗口最大值)进行巩固练习。

"优秀的算法工程师不是在发明新算法,而是在合适场景选择最优雅的工具。" ------ 本题解正是对这一理念的实践诠释。

相关推荐
倔强的小石头_1 小时前
【C语言指南】函数指针深度解析
java·c语言·算法
jz_ddk8 小时前
[学习] C语言数学库函数背后的故事:`double erf(double x)`
c语言·开发语言·学习
xiaolang_8616_wjl8 小时前
c++文字游戏_闯关打怪2.0(开源)
开发语言·c++·开源
夜月yeyue8 小时前
设计模式分析
linux·c++·stm32·单片机·嵌入式硬件
无小道9 小时前
c++-引用(包括完美转发,移动构造,万能引用)
c语言·开发语言·汇编·c++
FirstFrost --sy10 小时前
数据结构之二叉树
c语言·数据结构·c++·算法·链表·深度优先·广度优先
森焱森10 小时前
垂起固定翼无人机介绍
c语言·单片机·算法·架构·无人机
Tanecious.11 小时前
C++--map和set的使用
开发语言·c++
Yingye Zhu(HPXXZYY)11 小时前
Codeforces 2021 C Those Who Are With Us
数据结构·c++·算法
liulilittle12 小时前
LinkedList 链表数据结构实现 (OPENPPP2)
开发语言·数据结构·c++·链表