滑动窗口(同向双指针)算法:模板与例题解析

1. 滑动窗口

核心思想

right 一直往右走(不回退)

left 只在"窗口非法"时被迫右移(也不回退)

  • 时间优化:两层for->一层while;
  • 模板(最长合法窗口)
cpp 复制代码
int left = 0, ret = 0;
unordered_map<int,int> mp;          // 计数器,视题意更换
for (int right = 0; right < n; ++right) {
    // 1. 进窗口
    mp[a[right]]++;

    // 2. 判断合法性
    while (mp[a[right]] > 1) {      // 出现重复,非法
        // 3. 出窗口
        mp[a[left]]--;
        left++;
    }

    // 4. 更新答案
    ret = max(ret, right - left + 1);
}
  • 常见题型
  1. 最长无重复子串
  2. 最长至多k个重复子串

2. 两道例题

P3143 Diamond Collector S

题目描述

奶牛 Bessie 一直喜欢闪闪发光的物体,她最近在业余时间开始了一项爱好------挖掘钻石!她收集了 NNN 颗大小各不相同的钻石(N≤50,000N \leq 50,000N≤50,000),并希望将它们中的一部分放在谷仓里的两个展示柜中展示。

由于 Bessie 希望每个展示柜中的钻石大小相对接近,她决定如果两颗钻石的大小相差超过 KKK,就不能将它们放在同一个展示柜中(如果两颗钻石的大小相差恰好为 KKK,则可以将它们一起展示在同一个展示柜中)。给定 KKK,请帮助 Bessie 确定她可以在两个展示柜中一起展示的最大钻石数量。

输入格式

输入文件的第一行包含 NNN 和 KKK(0≤K≤1090 \leq K \leq 10^90≤K≤109)。

接下来的 NNN 行每行包含一个整数,表示一颗钻石的大小。所有钻石的大小均为正数且不超过 10910^9109。

输出格式

输出一个正整数,表示 Bessie 可以在两个展示柜中一起展示的最大钻石数量。

输入 #1

复制代码
7 3
10
5
1
12
9
5
14

输出 #1

复制代码
5

题解

错误解法的局限性

一种直观但错误的思路是:先选一段「最长」的满足条件的子数组,再选一段「次长」的。但这种方法是错误的,因为第一次的选择会影响第二次的选择,两者加起来「不一定是全局最优」。

反例[1, 1, 4, 5, 6, 7, 8, 10], k = 3

  • 如果先选[4, 5, 6, 7],接下来只能选[1, 1][8, 10],总长度为 6
  • 但最优解是先选[5, 6, 7, 8],再选[1, 1, 4],总长度为 7
    这说明贪心选择局部最长段并不能得到全局最优解。
正确解法:枚举分割点

我们可以「枚举」所有可能的情况:以位置 i 为「分界点」,将数组分成左右两部分:

  • 左边:在[1, i-1]区间内,选择符合要求的「最长子串」
  • 右边:在[i, n]区间内,选择符合要求的 [ 最长子串 ]

这样我们就能枚举出所有可能的分割方式,取「左右两部分长度之和」的最大值就是最终结果。

预处理方法

为了快速查询任意区间内的最长子串长度,我们需要进行预处理:

定义预处理数组:

  • f[i]:表示[1, i]区间内符合要求的「最长子串」长度
  • g[i]:表示[i, n]区间内符合要求的「最长子串」长度

计算 f[i] 的方法

对于每个位置 i,有两种可能:

  1. 最长子串不包含 a[i],即 f[i-1]
  2. 最长子串包含 a[i],即以 a[i] 为结尾的最长子串长度
    取两者的最大值作为 f[i]

计算 g[i] 的方法 (同理):

对于每个位置 i,有两种可能:

  1. 最长子串不包含 a[i],即 g[i+1]
  2. 最长子串包含 a[i],即以 a[i] 为起始的最长子串长度
    取两者的最大值作为 g[i]
如何快速计算「以 a[i] 为结尾」的最长子串长度?

使用「滑动窗口」技术:

  • 维护一个滑动窗口,保证窗口内最大值与最小值的差 ≤ k
  • 当窗口扩展时,如果差值超过 k,则收缩左边界
  • 对于每个右端点 right,窗口长度right-left+1就是「以 a[right] 为结尾」的最长子串长度
  • 一次滑动窗口遍历,可以预处理出所有位置的信息
    滑动窗口是两指针不回退,一个指针一步一步向前走,一个指针违反限制就收缩若干格,因为要保证i-n都有数据,因此要用一步步的那个指针处理数据,也因此g[i]和f[i]要分别使用双指针
整体算法流程
  1. 排序:先对数组排序,使得可以使用双指针
  2. 正向预处理:使用滑动窗口计算 f[i],表示前 i 个元素的最优解
  3. 反向预处理:同样使用滑动窗口(反向)计算 g[i],表示从 i 开始的最优解
  4. 枚举分割点 :遍历所有可能的分割点 i,计算 f[i-1] + g[i] 的最大值
cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 5e4 + 10;  // 最大数据范围
int n, k;               // n: 元素个数, k: 最大差值限制
int a[N];               // 存储原始数组

int f[N], g[N];         // f[i]: 前i个元素中满足条件的最长子数组长度
                        // g[i]: 从第i个元素到末尾满足条件的最长子数组长度

int main()
{
    cin >> n >> k;  // 读入元素个数和差值限制
    
    // 读入数组
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    // 对数组进行排序,方便使用双指针
    sort(a + 1, a + 1 + n);
    
    // 预处理 f[i]: 以i为右端点,满足条件的最大连续子数组长度
    // 使用双指针维护滑动窗口
    for(int left = 1, right = 1; right <= n; right++)
    {
        // 当当前窗口的最大最小值差超过k时,收缩左指针
        while(a[right] - a[left] > k) left++;
        
        // f[right]表示前right个元素中满足条件的最大长度
        // 要么包含当前right(即right-left+1),要么不包含(f[right-1])
        f[right] = max(f[right - 1], right - left + 1);
    }
    
    // 预处理 g[i]: 以i为左端点,满足条件的最大连续子数组长度
    // 同样使用双指针,从后往前扫描
    for(int left = n, right = n; left >= 1; left--)
    {
        // 当当前窗口的最大最小值差超过k时,收缩右指针
        while(a[right] - a[left] > k) right--;
        
        // g[left]表示从left开始到末尾满足条件的最大长度
        // 要么包含当前left(即right-left+1),要么不包含(g[left+1])
        g[left] = max(g[left + 1], right - left + 1);
    }
    
    // 寻找最优分割点,将数组分成两部分,使得两部分长度之和最大
    int ret = 0;
    for(int i = 2; i <= n; i++)  // i表示分割点(第二部分从i开始)
    {
        // 前i-1个元素的最优解 + 从i开始的最优解
        ret = max(ret, f[i - 1] + g[i]);
    }
    
    cout << ret << endl;  // 输出结果
    return 0;
}

P10710 School Photo

题目描述

Zane 是 NOI 学校的校长。NOI 学校有 nnn 个班,每个班有 sss 名同学。第 iii 个班中的第 jjj 名同学的身高是 ai,ja_{i,j}ai,j。

现在 Zane 想从每个班上选出一名同学拍照,使得这 nnn 名同学中最高的同学和最低的同学的身高差最小。

请你输出这个最小值。

输入格式

第一行,两个整数 n,sn,sn,s;

接下来 nnn 行,每行 sss 个整数,表示 aaa。

输出格式

一行一个整数表示答案。

输入 #1

复制代码
2 3
2 1 8
5 4 7

输出 #1

复制代码
1

输入 #2

复制代码
3 3
3 1 4
2 7 18
9 8 10

输出 #2

复制代码
4

说明/提示

【样例 #2 解释】

选择 a1,3,a2,2,a3,2a_{1,3},a_{2,2},a_{3,2}a1,3,a2,2,a3,2,答案为 8−4=48-4=48−4=4。

【数据范围】

Subtask\text{Subtask}Subtask 分值 特殊性质
000 000 样例
111 111111 n=2n=2n=2
222 222222 n,s≤100n,s\le100n,s≤100
333 999 n,s≤250n,s\le250n,s≤250
444 333333 n,s≤500n,s\le500n,s≤500
555 252525

对于 100%100\%100% 的数据,1≤n,s≤1000,1≤ai,j≤1091\le n,s \le 1000,1\le a_{i,j} \le 10^91≤n,s≤1000,1≤ai,j≤109。

题解

问题理解

我们有:

  • n 个班级
  • 每个班级有 s 个学生
  • 要从每个班选一个学生
  • 要最小化:选出的n个学生中最高和最矮的身高差

关键转化

如果我们把所有学生按身高排序,问题转化为:

在排序后的数组中,找一个最短的连续子数组,这个子数组包含所有n个班级的学生。

**思路

  • 将所有⼈的⾝⾼与班级绑定,从⼩到⼤排序;
  • 问题就变成在⼀个数组中,挑出连续的⼀段,使⾥⾯的学⽣包含所有班级。在所有的挑法中,找出最⼤⾝⾼ - 最⼩⾝⾼的最⼩值。
  • 可以⽤滑动窗⼝来解决。

**两变量

  • cnt[i]:记录当前窗口中班级 i 的学生数量
  • kind:记录当前窗口中包含的不同班级数量
cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010, M = N * N;  // N:最大班级数,M:最大学生总数

int n, s, m;  // n:班级数,s:每班人数,m:总人数
struct node
{
    int h, id;  // h:身高,id:班级编号(1~n)
}a[M];
int cnt[N];     // 计数器,记录当前窗口中每个班级的学生数

// 比较函数:按身高升序排序
bool cmp(node& x, node& y)
{
    return x.h < y.h;
}

int main()
{
    // 读入数据
    cin >> n >> s;
    
    // 读入所有学生信息
    for(int i = 1; i <= n; i++)  // i是班级编号
    {
        for(int j = 1; j <= s; j++)  // j是班级内学生序号
        {
            m++;  // 总人数+1
            cin >> a[m].h;      // 读入身高
            a[m].id = i;        // 记录班级编号
        }
    }
    
    // 将所有学生按身高从小到大排序
    sort(a + 1, a + 1 + m, cmp);
    
    // 初始化答案为无穷大
    int ret = 1e9;
    
    // 滑动窗口(双指针)
    // l:窗口左指针,r:窗口右指针,kind:窗口中包含的不同班级数
    for(int l = 1, r = 1, kind = 0; r <= m; r++)
    {
        // 将a[r]加入窗口
        cnt[a[r].id]++;
        
        // 如果这个班级之前不在窗口中(cnt从0变成1)
        // 则窗口中包含的班级种类数+1
        if(cnt[a[r].id] == 1) kind++;
        
        // 当窗口包含所有班级时,尝试缩小窗口
        while(kind == n)
        {
            // 更新答案:当前窗口的身高差
            ret = min(ret, a[r].h - a[l].h);
            
            // 将a[l]移出窗口
            cnt[a[l].id]--;
            
            // 如果这个班级的学生全部移出窗口(cnt从1变成0)
            // 则窗口中包含的班级种类数-1
            if(cnt[a[l].id] == 0) kind--;
            
            // 左指针右移,缩小窗口
            l++;
        }
    }
    
    // 输出最小身高差
    cout << ret << endl;
    
    return 0;
}
相关推荐
Brilliantwxx1 小时前
【算法题】基础计算器的不同实现方式
c++·算法
Sunsets_Red1 小时前
P12375 「LAOI-12」MST? 题解
c++·算法·洛谷·信息学·oier·洛谷题解
He BianGu1 小时前
【笔记】在WPF中在IValueConverter 时“无法返回有效值该怎么做”
笔记·wpf
白小沫1 小时前
TortoiseSVN 的快速安装与常用操作
经验分享·笔记
雪度娃娃1 小时前
多用户任务管理器
c++·个人开发
_深海凉_1 小时前
LeetCode热题100-二叉树的直径
算法·leetcode·职场和发展
shylyly_1 小时前
大小端字节序
数据结构·算法·联合体·大小端字节序·字节序判断
mmz12072 小时前
深度优先搜索DFS3(c++)
c++·算法·深度优先
水蓝烟雨2 小时前
3373. 连接两棵树后最大目标节点数目 II
算法·leetcode