滑动窗口与双调队列:幕布覆盖问题(定右缩左满分板子)改编自LeetCode 1438

今天,我们通过一道非常经典的"幕布覆盖"问题,彻底剖析双指针滑动的两种思维:"定左探右"以及"定右缩左"的标准滑动骨架,并给出两套满分模板。

一、 题目描述

【题目大意】 在一张网格图中,每一列有且仅有一个格子被小蜗涂了色,现在给你每一列被涂色的格子的高度 ai​。 现在有一块高度为 h 的长方形幕布(挂在高度 x 的时候能盖住的高度区间为 [x−h+1,x]),可以在列方向延伸(宽度可以是任意值)。 问你固定高度后,一块布最多可以覆盖住连续的多少列的格子?

【输入格式】 第一行给出两个数,代表列数 n 和高度 h。 接下来一行给出每一列被涂色的格子高度 a[i]​。

【样例输入】

复制代码
7 4
1 4 5 3 6 1 4

【样例输出】

复制代码
4

【数据范围】 1≤n≤100000,1≤h,a[i​]≤10^6。


二、 题目分析与核心不等式

这道题的物理外衣是"幕布覆盖",我们需要翻译成纯粹的数学语言。 幕布挂在高度x时,覆盖的最低点是x−h+1。 这意味着,在幕布能盖住的任意一段连续区间内,最高点与最低点的差值,最大只能是h−1。 换句话说,对于区间[l,r],合法的条件是:

max(a[l...r])−min(a[l...r])<h

(注:题目规定高度差加1不大于幕布高度,即极差≤h−1,等价于极差<h)。

题目要求我们找到满足合法条件的最长连续区间长度。为了实现O(N)的降维打击,我们需要结合滑动窗口(双指针)单调队列


三、 解法一:定左探右

这是很多初学者的第一直觉:固定左端点i,让右端点侦察兵j往未来探索。 易错点: 这种写法极易导致"试探未来污染队列"和"漏算末尾元素"。 方案**:** 我们必须利用C++ 的||短路特性,并在循环内外打上极其严密的逻辑补丁。

核心算法设计:

  1. 每次i更新时(新的一天),先把队列里<i的过期老将清理掉

  2. 侦察兵j不断往右试探。循环条件必须是极其精妙的:j<=n&&(j<i||a[maq]-a[miq]< h)

    • j<=n保证最后一个元素有机会结账。

    • j<i是空队列保护机制:当队列因左指针推进被清空时,利用||短路特性直接放行,防止后续求极差时越界。

  3. 先结账,后入队 :每次进入循环,先结算 ma=max(ma, j-i+1),确认合法后再执行入队操作,彻底杜绝"假账"。

  4. 黄金刹车片 :结账后j会自增,若j>n必须立刻break,防止后续入队越界。

解法一完整代码:

cpp 复制代码
//覆盖
//求出每个区间最大值和最小值
//最大值和最小值之差+1不大于幕布高度即可
#include <iostream>
using namespace std;
int n,h;
int a[1000010];//每一列被涂色的格子的高度
int miq[1000010];//记录最小高度的队列
int maq[1000010];//记录最大高度的队列
int mif=1,mir=0;//最小高度队首队尾
int maf=1,mar=0;//最大高度队首队尾
int ma;//最多可以覆盖多少列格子
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>h;
    for(int i=1;i<=n;i++) cin>>a[i];
    int j=0;//幕布右端
    for(int i=1;i<=n;i++){//遍历幕布左端
        //当队列非空且队首过期 就把队首清除
        while(mif<=mir&&miq[mif]<i) mif++;
        while(maf<=mar&&maq[maf]<i) maf++;
        //当幕布右端不超过边界(幕布右端未超过幕布左端)且最高点-最低点差值要小于h
        //否则幕布盖不过来
        while(j<=n&&(j<i||a[maq[maf]]-a[miq[mif]]<h)){
            //每一轮开始时统计最大格子列数
            ma=max(ma,j-i+1);
            j++;
            //防止最后一轮j越界然后访问a[j]
            if(j>n) break;
            //当最小高度队列非空且幕布右端高度小于等于队尾时
            //队尾出队
            while(mif<=mir&&a[j]<=a[miq[mir]]){
                mir--;
            }
            //j入队
            miq[++mir]=j;
            //当最大高度队列非空且幕布右端高度大于等于队尾时
            //队尾出队
            while(maf<=mar&&a[j]>=a[maq[mar]]){
                mar--;
            }
            //j入队
            maq[++mar]=j;
        }
    }
    cout<<ma;
    return 0;
}

四、 解法二:定右缩左

相比于第一种防御性编程,竞赛中更推崇的标程写法是转换视角:"只处理当下,绝不试探未来"。

核心算法设计:

  1. 让右边界r像推土机一样每天无脑往前走,新元素无条件入队

  2. 每次入队后,回头查账。如果发现极差≥h(超标了),我们就强迫左边界l往前收缩

  3. 收缩时,把下标<l的过期老将踢出。

  4. 终极防爆边界 :循环收缩的前提加上l<r。当l追上r时(区间仅剩一个元素),极差为0,绝对合法。这一步强行刹车,保证队列永远不为空,彻底斩断越界风险。

  5. 存活出审查循环的 [l,r] 绝对合法,每日结算一次。

解法二完整代码:

cpp 复制代码
//覆盖 第二种做法
//固定右端点,移动左端点
#include <iostream>
using namespace std;
int n,h;
int a[1000010];//每一列被涂色的格子的高度
int miq[1000010];//记录最小高度的队列
int maq[1000010];//记录最大高度的队列
int mif=1,mir=0;//最小高度队首队尾
int maf=1,mar=0;//最大高度队首队尾
int ma;//最多可以覆盖多少列格子

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>h;
    for(int i=1;i<=n;i++) cin>>a[i];
    int l=1;//左端
    for(int r=1;r<=n;r++){//遍历右端
        //当最小高度队列非空且右端点高度小于等于最小高度队尾时
        //队尾出队
        while(mif<=mir&&a[r]<=a[miq[mir]]){
            mir--;
        }
        //右端点入队
        miq[++mir]=r;
        //当最大高度队列非空且右端点高度大于等于最大高度队尾时
        //队尾出队
        while(maf<=mar&&a[r]>=a[maq[mar]]){
            mar--;
        }
        //右端点入队
        maq[++mar]=r;
        //l不超过边界且判断接下来检查最大高度-最小高度如果大于等于h
        //代表幕布不能完全盖住,就把幕布左端往右锁
        while(l<r&&a[maq[maf]]-a[miq[mif]]>=h){
            //如果不小于h就把l往右移动
            l++;
            //当队列非空且队首过期时
            //就清理过期的
            while(mif<=mir&&miq[mif]<l) mif++;
            while(maf<=mar&&maq[maf]<l) maf++;
        }
        //接下来肯定是符合要求的
        ma=max(ma,r-l+1);
    }
    cout<<ma;
    return 0;
}

五、 时空复杂度与易错点总结

  • 时间复杂度: O(N)。两个解法中,每个元素最多入队一次、出队一次。均摊下来是严格的线性时间。

  • 空间复杂度: O(N)。需要开辟原数组和两个单调队列。

避坑指南:

  1. 队列里永远只存下标。 绝不存具体的值,否则清理过期元素时无法对比位置。

  2. 警惕-1+1 数组如果是1-indexed,左边界l必须从 1 开始。如果从0开始,答案会永远比正确结果多 1。

  3. && 的短路求值: 判空条件(如 mif<=mir)必须放在逻辑与&&的最前面,利用短路特性充当安全带。

相关推荐
CoovallyAIHub2 小时前
ICLR 2026 | MedAgent-Pro:用 Agent 工作流模拟临床医生的循证诊断过程
深度学习·算法·计算机视觉
实心儿儿2 小时前
算法7:两个数组的交集
算法·leetcode·职场和发展
我可能是个假开发2 小时前
算法-回溯
算法
WolfGang0073212 小时前
代码随想录算法训练营 Day14 | 二叉树 part04
数据结构·算法
爱丽_2 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
仍然.2 小时前
算法题目---前缀和
算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
大熊背2 小时前
双目拼接摄像机中简单的亮度差校正原理
人工智能·算法·双目拼接·亮度差消除