一.什么是滑动窗口
滑动窗口(Sliding Window)是一种在数组、列表、字符串上常用的算法技巧,用于高效地处理固定长度或可变长度的连续子序列(子数组/子串)的问题。
核心思想:
维护"一个窗口"(即一段连续的区间),通过移动窗口的左右边界,避免重复计算,从而优化时间复杂度。
二.经典应用场景
1.固定窗口大小(最常见)
问题:给定数组,求每个长度为k的连续子数组的最大值(或最小值,和等)
python
for i in range(n-k+1):
# 因为每组是k个元素,最后一组的最后元素索引是i+k(i,i+1,i+2,...,i+k-1,刚好k个元素),
# i+k作为切片的末索引,因为切片中是切到末索引前一个索引(即i+k-1)
# 所以i+k-1 = n-1 则 i要取到n-k,则range()中的参数应该为n-k+1
max_val = max(arr[i:i+k])
如:
python
arr = [1,2,3,4,5,6,7,8,9,10]
n = len(arr)
k = 3
for i in range(n-k+1):
# print(i,end=' ')
max_val = max(arr[i:i+k])
print(max_val)
"""
输出:
3
4
5
6
7
8
9
10
"""
这个模板属于暴力解法,性能比较低效
可以使用单调队列来加以优化
先来复习一下队列的知识
队列是一种常用的数据结构,特点是元素"先进先出";只能在队尾插入,队头删除;可以用排队买票来类比
队列的实现方式可以手写 ,也可以使用Python的内置库:collections.queue (collections库提供了许多增强版的数据容器类型,主要用于代替或扩展内置的list、dict、tuple、set等,让代码更简洁,高效,语义清晰)、queue.Queue(线程安全,用于多线程,普通算法题不要用这个,因为这个慢且不支持索引/切片)
双端队列:两端都可以进行插入和删除的队列
collections.deque,所有操作的时间复杂度都是O(1),(底层用双向链表和循环数组实现)
python
from collections import deque
def sliding_window_max(arr,k):
"""返回arr中每个长度为k的滑动窗口的最大值列表"""
dq = deque() # 队列用来存储索引
res = []
for i in range(len(arr)):
# 移除超出窗口(窗口长度k)的索引
if dq and dq[0] == i-k: # 如何判断刚好是长度为k的窗口?当循环到索引i时,dq中要只需有i-k+1,...,i-2,i-1,i这k个元素就行,但如果有i-k,就要移除
# 可以改成dq[0] < i-k+1吗?可以(小于窗口左边界i-k+1就要删除),但每次i只增加1,最多只有1个索引会过期,原写法效率会高一点(只判断一次)
dq.popleft() # 从队列的左端移除
# 维护单调递减队列
while dq and arr[dq[-1]] < arr[i]: # 新的元素要小于等于dq末尾索引对应的元素才能入队
dq.pop()
dq.append(i)
if i >= k-1: # 如果遍历到i大于等于k-1(为什么是k-1,因为range从0开始,0,1,2,...,k-1,刚好k个元素)时,说明窗口长度已经达标了
res.append(arr[dq[0]]) # 由于维护的是递减队列,队首就是最大的元素的索引
return res
res = sliding_window_max([1,2,3,4,5,4,7,11,9,10],3)
print(res) # [3, 4, 5, 5, 7, 11, 11, 11]
蓝桥云课3521题:
python
n, m, a, b = map(int, input().split())
matrix = []
for _ in range(n):
matrix.append(list(
map(int, input().split())
))
res = 0
start_row, end_row = 0, a
start_col, end_col = 0, b
while end_row <= n: # 这一层while循环要循环多少次?end_row最开始是a,最后取值为n,从a到n一共有n-a+1个数,那么这层while循环就要循环n-a+1次
temp = matrix[start_row:end_row]
start_row += 1
end_row += 1
while end_col <= m: # 这层while循环要循环m-b+1次
submatrix = [sub[start_col:end_col] for sub in temp]
start_col += 1
end_col += 1
# print(submatrix)
min_val = min(min(row) for row in submatrix) # 求子矩阵的最小值:对每一行的b个元素求最小值要比较b-1次,有a行那么就有a个最小值,那么还要比较a-1次。共要比较(a-1)*(b-1)次
max_val = max(max(row) for row in submatrix) # 求子矩阵的最大值:同理要比较(a-1)*(b-1)次
# 因此
value = min_val * max_val
res += value
start_col, end_col = 0, b # 重置
print(res % 998244353 )
### 这个算法的时间复杂度为:O(mnab)?还是O((m-b+1)*(n-a+1)*(a-1)*(b-1))
"""
现代CPU每秒可执行越10^9次操作
综合多种因素,可以认为Python每秒可以执行10^7次操作
"""
只通过了60%的测试用例,超时了。
python
from collections import deque
def sliding_window_max(arr, k):
"""返回arr中每个长度为k的滑动窗口的最大值列表"""
dq = deque()
res = []
for i in range(len(arr)):
if dq and dq[0] == i-k:
dq.popleft()
while dq and arr[dq[-1]] <= arr[i]:
dq.pop()
dq.append(i)
if i >= k-1:
res.append(arr[dq[0]])
return res
def sliding_window_min(arr, k):
"""返回arr中每个长度为k的滑动窗口的最小值列表"""
dq = deque()
res = []
for i in range(len(arr)):
if dq and dq[0] == i-k:
dq.popleft()
# 维护一个单调递增队列
while dq and arr[dq[-1]] >= arr[i]:
dq.pop()
dq.append(i)
if i >= k-1:
res.append(arr[dq[0]])
return res
n, m, a, b = map(int, input().split())
matrix = []
for _ in range(n):
matrix.append(list(
map(int, input().split())
))
row_mins, row_maxs = [], []
for i in range(n):
# 对每一行的滑动窗口求最小值,最大值
row_mins.append(sliding_window_min(matrix[i], b)) # n行m-b+1列(从b-1到m-1,有m-1-(b-1)+1=m-b+1个)
row_maxs.append(sliding_window_max(matrix[i], b)) # n行m-b+1列
# print(row_mins)
# print(row_maxs)
total = 0
MOD = 998244353
cols = len(row_mins[0]) # m-b+1
for j in range(cols):
# 提取第j列的所有行的窗口的最小值,最大值
col_mins_val = [row_mins[i][j] for i in range(n)] # n行n列
col_maxs_val = [row_maxs[i][j] for i in range(n)]
# 对列方向做高度为a的滑动窗口
win_mins = sliding_window_min(col_mins_val, a)
win_maxs = sliding_window_max(col_maxs_val, a) # n行n-a+1列
for idx in range(len(win_mins)):
total = (total + win_mins[idx]*win_maxs[idx]) % MOD
print(total)