📌 专栏:信息学竞赛基础算法专题
💡 适用人群:NOIP/CSP-J/S 入门-提高组选手、算法零基础刷题党
🔥 算法定位:信奥高频必考、双指针核心算法、区间问题最优解
一、算法前言
在信息学竞赛中,**滑动窗口(又称尺取法)**是解决连续区间问题的核心利器,几乎覆盖数组、字符串、子序列、区间最值等高频题型。
传统暴力枚举区间的时间复杂度为 O(n2)O(n^2)O(n2),面对题目 n≤105n \le 10^5n≤105 的数据范围会直接超时。而滑动窗口通过双指针同向移动 的核心思想,仅需一次遍历即可完成求解,时间复杂度优化至 O(n)O(n)O(n),是竞赛中性价比极高的贪心类算法。
本文将从核心原理、两大题型分类、通用C++模板、竞赛经典例题、高频易错点五个维度,带你彻底吃透滑动窗口,适配所有信奥基础区间题型。
二、滑动窗口核心原理
2.1 核心思想
借助两个指针 left(左边界) 和 right(右边界) 模拟一个动态窗口:
-
right指针:不断右移,扩大窗口,纳入新元素
-
left指针:在窗口不合法/满足最优条件时右移,缩小窗口
-
窗口状态:全程维护合法区间,实时更新最优答案
核心特性:双指针单向移动,不回头,因此每个元素仅被遍历一次,保证线性复杂度。
2.2 适用场景
满足两个条件即可使用滑动窗口:
-
求解连续区间/子数组/子串问题
-
窗口的合法性具有单调性(窗口扩大不合法、缩小合法,或反之)
✅ 常见题型:区间最值、无重复子串、最小覆盖子串、定长区间求和、最多翻转区间等
❌ 不适用:非连续区间、区间无单调性的问题
三、滑动窗口两大竞赛题型
信奥中所有滑动窗口题目,均可分为定长窗口 和不定长窗口两类,对应两套通用模板,覆盖99%基础题型。
3.1 定长滑动窗口(窗口大小固定k)
题型特征:题目明确限定区间长度为固定值k,求该长度下的最优解(最大值、最小值、最大和等)
解题逻辑:
-
先构建初始大小为k的窗口
-
右指针右移纳入新元素
-
左指针同步右移,移除窗口过期元素
-
每次窗口成型后更新答案
3.2 不定长滑动窗口(窗口大小动态变化)
题型特征 :无固定区间长度,求满足条件的最大/最小合法区间
细分两类:
-
最大合法窗口:窗口合法时持续扩大,不合法时收缩左边界(如最长无重复子串)
-
最小合法窗口:窗口合法时持续收缩左边界找最优解,不合法时扩大右边界(如最小覆盖子串)
四、C++竞赛通用模板(可直接套用)
以下模板经过竞赛实战验证,代码简洁、无冗余,适配CSP/NOIP考场,可直接复制修改使用。
4.1 定长窗口通用模板
cpp
// n:数组长度 k:固定窗口大小 a[]:目标数组
int left = 0;
int ans = 0; // 根据题目初始化最值
for(int right = 0; right < n; right++){
// 1. 纳入右侧新元素,更新窗口状态
// ... 自定义状态更新(求和、计数、去重等)
// 2. 窗口长度超过k,收缩左边界
while(right - left + 1 > k){
// 移除左侧过期元素,更新窗口状态
// ... 自定义状态回滚
left++;
}
// 3. 窗口成型,更新最优答案
if(right - left + 1 == k){
ans = max(ans, 窗口状态); // 或min、sum等
}
}
cout << ans << endl;
4.2 不定长-最大合法窗口模板
cpp
int left = 0;
int ans = 0;
for(int right = 0; right < n; right++){
// 1. 纳入右元素,更新状态
// ...
// 2. 窗口不合法,持续收缩左边界
while(窗口不合法条件){
// 移除左元素,回滚状态
// ...
left++;
}
// 3. 更新最大窗口答案
ans = max(ans, right - left + 1);
}
cout << ans << endl;
4.3 不定长-最小合法窗口模板
cpp
int left = 0;
int ans = INT_MAX; // 最小值问题初始化为极大值
for(int right = 0; right < n; right++){
// 1. 纳入右元素,更新状态
// ...
// 2. 窗口合法时,持续收缩找最小区间
while(窗口合法条件){
ans = min(ans, right - left + 1);
// 移除左元素,回滚状态
// ...
left++;
}
}
if(ans == INT_MAX) cout << 0 << endl; // 无合法区间
else cout << ans << endl;
五、竞赛经典例题精讲(C++ AC代码)
精选3道信奥入门-提高组高频真题,覆盖三类窗口题型,手把手拆解思路。
例题1:洛谷P1886 滑动窗口(定长窗口+单调队列)
题目描述:给定长度为n的序列,求所有长度为k的连续窗口的最大值、最小值。(经典模板题)
解题思路 :定长窗口结合单调队列,维护队列单调性,剔除窗口过期元素,O(n)O(n)O(n) 求解区间最值。
cpp
#include <iostream>
#include <deque>
using namespace std;
const int N = 1e6 + 10;
int a[N];
deque<int> q; // 存储数组下标
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
for(int i = 0; i < n; i++) cin >> a[i];
// 求最小值(单调递增队列)
for(int i = 0; i < n; i++){
// 移除窗口外过期元素
while(!q.empty() && q.front() <= i - k) q.pop_front();
// 维护递增单调性
while(!q.empty() && a[i] <= a[q.back()]) q.pop_back();
q.push_back(i);
// 窗口成型输出答案
if(i >= k - 1) cout << a[q.front()] << " ";
}
cout << endl;
q.clear();
// 求最大值(单调递减队列)
for(int i = 0; i < n; i++){
while(!q.empty() && q.front() <= i - k) q.pop_front();
while(!q.empty() && a[i] >= a[q.back()]) q.pop_back();
q.push_back(i);
if(i >= k - 1) cout << a[q.front()] << " ";
}
return 0;
}
例题2:最长无重复字符子串(最大不定长窗口)
题目描述:给定字符串,找出不含重复字符的最长子串长度。
解题思路:窗口内字符无重复为合法条件,出现重复则收缩左边界,动态更新最大窗口长度。
cpp
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main(){
string s;
cin >> s;
int vis[128] = {0}; // 记录字符出现次数
int left = 0, maxLen = 0;
int n = s.size();
for(int right = 0; right < n; right++){
char c = s[right];
vis[c]++;
// 存在重复字符,收缩左边界
while(vis[c] > 1){
vis[s[left]]--;
left++;
}
// 更新最大长度
maxLen = max(maxLen, right - left + 1);
}
cout << maxLen << endl;
return 0;
}
例题3:最小覆盖子串(最小不定长窗口)
题目描述:给两个字符串s、t,找出s中包含t所有字符的最小子串。
解题思路:右指针扩窗口至包含所有目标字符,再收缩左边界找最小合法区间。
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <climits>
using namespace std;
int main(){
string s, t;
cin >> s >> t;
int window[128] = {0}, target[128] = {0};
int cnt = 0; // 记录匹配的字符种类
int left = 0, minLen = INT_MAX;
int n = s.size(), m = t.size();
// 初始化目标字符计数
for(char c : t) target[c]++;
for(int right = 0; right < n; right++){
char c = s[right];
window[c]++;
// 该字符匹配完成,种类+1
if(target[c] && window[c] == target[c]) cnt++;
// 完全匹配,收缩左边界找最小区间
while(cnt == m){
minLen = min(minLen, right - left + 1);
char lc = s[left];
if(target[lc] && window[lc] == target[lc]) cnt--;
window[lc]--;
left++;
}
}
if(minLen == INT_MAX) cout << "" << endl;
else cout << s.substr(left - minLen, minLen) << endl;
return 0;
}
六、竞赛高频易错点避坑指南
滑动窗口代码简单,但细节极易丢分,总结考场高频错误:
6.1 边界判断错误
窗口长度计算为 right - left + 1,切勿遗漏 +1,这是新手最常见失分点。
6.2 状态更新顺序错误
必须遵循:先扩窗口、再判合法、最后更新答案,顺序颠倒会导致窗口状态错乱。
6.3 最值初始化不当
求最大值初始化为0,求最小值必须初始化为 INT_MAX,同时需判断无合法区间的特判情况。
6.4 数据范围超时
处理 n≥105n \ge 10^5n≥105 数据时,必须添加 ios::sync_with_stdio(false);cin.tie(0); 关闭流同步,避免TLE。
6.5 单调队列过期元素未清除
定长最值窗口中,必须优先清除队头过期下标,再维护队列单调性,否则答案错误。
七、刷题适配总结
-
固定区间长度 → 直接套用定长窗口模板(区间和、区间最值、固定子串)
-
求最长合法区间 → 最大不定长窗口(无重复、最多翻转、最大和区间)
-
求最短合法区间 → 最小不定长窗口(覆盖子串、最小达标区间)
-
区间最值进阶 → 滑动窗口+单调队列,解决1e6级大数据问题
八、文末总结
滑动窗口是信息学竞赛中性价比最高的基础算法,逻辑简单、代码短小、效率极高,是冲刺CSP-J/S、NOIP初赛、复赛的必备知识点。
掌握两套核心逻辑+三套通用模板,即可覆盖绝大多数区间类题型,后续刷题只需根据题目条件修改窗口合法判断逻辑即可。
后续会持续更新信奥算法专题:前缀和、差分、二分、单调队列、DP基础,感兴趣可以点赞+关注,持续刷题不迷路!
💖 原创不易,转载请注明出处
✨ 关注博主,持续更新信息学竞赛干货、真题解析、刷题技巧