%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 滑动窗口
滑动窗口 **==右边界==每次向右移动一步**,相当于在是 **==枚举/搜索==以==每个位置 `i` 作为右边界==的情况**,在每种情况下再去考虑**是否需要收缩左边界**。
其要求保证 **==左边界==只可能==向右收缩==,而不会向左回头**,从而 **减少了搜索空间**。
算法只遍历了数组两次,而**不用枚举所有可能的区间**,把 $O(n^2)$ 的时间复杂度降到了 $O(n)$。
---
滑动窗口在实现上属于 "同向双指针" 的一种,强调 "窗口" 的概念,根据条件对窗口进行 "**右扩展、左收缩、滑动**"。
由此确认**所有**符合条件的**连续区间段(每个区间都要是最大化的、不可再扩张的区间)**
## 滑动窗口适用的条件
能够应用滑动窗口的**前提条件**如下:
> [!summary]
> 对于**任意两个分别以 `l1`、`l2` 为左边界的==满足目标条件的极大窗口== `[l1, r1]` 与 `[l2, r2]`**,
> **如果当 `l1 < l2` 时,一定有 `r1 <= r2` 成立**,则**可应用滑动窗口方法**。
>
上述条件成立时,意味着**当滑窗==右边界==枚举到 `r2`** 时,**已然搜索完了 "以`l1` 为左端的所有窗口"**,故 **==可收缩左边界 `l1`==**,而不用再回头。
![[Excalidraw-Solution/滑动窗口_0.excalidraw.svg|341]]
%%[[Excalidraw-Solution/滑动窗口_0.excalidraw.md|🖋 Edit in Excalidraw]]%%
<br>
<br>
# 变长的滑动窗口
变长滑动窗口的思路:
1. **每次==右边界==向右==扩展一步==**;
2. **检查当前窗口状态**,根据窗口是否满足条件**判断==是否需要收缩左边界==**;
3. **处理当前窗口**;
4. 重复上述过程。
> [!caution] 对于==窗口 `size=0`== 的情况,需要进行==特判==,单独记录
模版伪代码如下(仅示意**常规流程**,**并不绝对**):
```cpp
// 下面的循环中, 窗口size至少都为1.
// 因此需要特判窗口size为0的情况.
// 循环不变量: 每次进入循环前/循环结束后, 区间[i, j)表示当前窗口, j始终指向待处理位置
int i = 0;
for (int j = 0; j < n; ++j) {
// 扩展右边界, 纳入新元素
include(nums[j]);
// 收缩左边界
while (i <= j && ...) {
// ? 处理当前区间[i, j]
process(i, j);
++i;
}
// ? 处理当前区间[i, j]
process(i, j);
}
```
![[Excalidraw-Solution/滑动窗口_1.excalidraw.svg|377]]
%%[[Excalidraw-Solution/滑动窗口_1.excalidraw.md|🖋 Edit in Excalidraw]]%%
<br><br>
# 定长的滑动窗口
区分两种场景:
- (1)**窗口状态** 需 **全量更新** 的情况——例如,对每个窗口,需要**遍历窗口完内所有元素**才能确认状态,本质上是 "**枚举各个窗口**", $O(n*k)$。
- (2)**窗口状态** 可 **差量更新** 的情况——即窗口状态可通过 "**移除左端旧元素**" & "**加入右端新元素**" 直接得到,**只需处理==边界==**,即真正意义上的 "**滑动**",$O(n)$。
- 写法一:**窗口从 0 开始扩展**,**达到最大 size 后不变**,随后每次向后滑动;❇️
- 写法二:**初始时==固定窗口 size== 并==根据窗口内状态完成初始化**==,随后每次向后滑动。
## 场景一:窗口状态需全量更新
枚举窗口**左边界**:`for (int i = 0; i + w <= n; ++i)` ;
```cpp
int w = size;
for (int i = 0; i + w <= n; ++i) { // 枚举窗口左边界
for (int j = 0; j < w; ++j) {
process_v(nums[i+j]); // 处理窗口内单项元素
}
process(i, i + w -1); //处理整个窗口`[i, i + w - 1]`;
}
```
<br>
## 场景二:每次移动后纳入右边界、移除左边界,只需对边界进行处理
### 写法一 ❇️
> [!caution] 窗口 size 从 0 开始扩展,必须**等==窗口大小满足题目要求==时, 才记录窗口内状态**!
不足:**每次窗口移动都要==额外==做两次条件判断**。
```cpp
int w = size;
for (int i = 0; i < n; ++i) {
// 窗口右边界纳入新元素
include(nums[i]);
// 移除当前窗口之外的元素(旧的左边界)
if (i >= w - 1) {
exclude(nums[i-w]);
}
// 处理当前窗口 [i-w+1, i]
if (i >= w) {
process(i-w+1, i);
}
}
```
### 写法二
#### 循环不变量定义一:**窗口状态**对应 `[i-w+1, i)` 表示的**大小为 `w-1`** 的窗口 ❇️
变量 `i` **标识==右边界==**,分成两段:
- `for (int i = 0; i < w - 1; ++i)`
- `for (int i = w - 1; i < n; ++i)`
这一写法下,代码不会有过多的重复/冗余逻辑。
```cpp
int w = size;
// 初始化, 扩展窗口, 这里只纳入了`w-1`项元素
for (int i = 0; i < w - 1; ++i) {
include(nums[i]);
}
// 处理定长窗口
// 循环不变量: 每次进入循环前/结束循环后, 区间[i-w+1, i)表示大小为`w-1`的窗口
for (int i = w - 1; i < n; ++i) {
// 纳入新元素, 使得窗口大小变为`w`
include(nums[i]);
// 处理大小为`w`的窗口[i-w+1, i]
process(i-w+1, i);
// 收缩左边界, 使[i-w+1, i]表示大小为`w-1`的窗口
exclude(nums[i-w+1]);
}
```
算法流程示意图:
![[Excalidraw-Solution/滑动窗口_2.excalidraw.svg|559]]
%%[[Excalidraw-Solution/滑动窗口_2.excalidraw.md|🖋 Edit in Excalidraw]]%%
<br>
#### 循环不变量定义二:窗口状态对应 `[i-w, i)` 表示的**大小为 `w`** 的窗口
```cpp
int w = size;
// 初始化, 扩展窗口, 得到size=w的窗口
for (int i = 0; i < w; ++i) {
include(nums[i]);
}
// 处理首个长度为`w`的定长窗口
process(0, w-1);
// 滑动窗口
// 循环不变量: 每次进入循环前/结束循环后, 区间[i-w, i)表示大小为`w`的窗口
for (int i = w; i < n; ++i) {
// 纳入新元素
include(nums[i]);
// 收缩左边界, 使i、j表示大小为`w`的窗口[i, j]
exclude(nums[i-w]);
// 处理大小为`w`的窗口[i, j]
process(i-w+1, i);
}
```
算法流程示意图:
![[Excalidraw-Solution/滑动窗口_3.excalidraw.svg|600]]
%%[[Excalidraw-Solution/滑动窗口_3.excalidraw.md|🖋 Edit in Excalidraw]]%%
<br><br>
# 参考资料
# Footnotes