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);
}
- 常见题型
- 最长无重复子串
- 最长至多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,有两种可能:
- 最长子串不包含 a[i],即
f[i-1] - 最长子串包含 a[i],即以 a[i] 为结尾的最长子串长度
取两者的最大值作为f[i]
计算 g[i] 的方法 (同理):
对于每个位置 i,有两种可能:
- 最长子串不包含 a[i],即
g[i+1] - 最长子串包含 a[i],即以 a[i] 为起始的最长子串长度
取两者的最大值作为g[i]
如何快速计算「以 a[i] 为结尾」的最长子串长度?
使用「滑动窗口」技术:
- 维护一个滑动窗口,保证窗口内最大值与最小值的差 ≤ k
- 当窗口扩展时,如果差值超过 k,则收缩左边界
- 对于每个右端点 right,窗口长度
right-left+1就是「以 a[right] 为结尾」的最长子串长度 - 一次滑动窗口遍历,可以预处理出所有位置的信息
滑动窗口是两指针不回退,一个指针一步一步向前走,一个指针违反限制就收缩若干格,因为要保证i-n都有数据,因此要用一步步的那个指针处理数据,也因此g[i]和f[i]要分别使用双指针
整体算法流程
- 排序:先对数组排序,使得可以使用双指针
- 正向预处理:使用滑动窗口计算 f[i],表示前 i 个元素的最优解
- 反向预处理:同样使用滑动窗口(反向)计算 g[i],表示从 i 开始的最优解
- 枚举分割点 :遍历所有可能的分割点 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;
}