1. 问题背景与核心挑战
LeetCode 3567题"子矩阵的最小绝对差"是一个典型的二维数组处理问题,考察对矩阵子结构的遍历和数值计算能力。题目要求在一个给定的m×n整数矩阵中,找出所有可能的k×k子矩阵,计算每个子矩阵中最大值与最小值之差(绝对差),然后返回所有子矩阵中最小的那个绝对差。
这个问题的难点在于:
- 矩阵尺寸可能较大(LeetCode常见约束是m,n≤1000)
- 需要高效处理所有可能的子矩阵(共(m-k+1)*(n-k+1)个)
- 对每个子矩阵需要快速获取极值
- 时间复杂度优化是关键挑战
2. 暴力解法分析与优化思路
2.1 基础暴力解法
最直观的解法是四重循环暴力枚举:
- 遍历所有可能的子矩阵起始位置(i,j)
- 对于每个子矩阵,遍历其所有元素
- 记录当前子矩阵的最大值和最小值
- 计算并更新全局最小绝对差
def minDifference(matrix, k): m, n = len(matrix), len(matrix[0]) min_diff = float('inf') for i in range(m - k + 1): for j in range(n - k + 1): current_max = -float('inf') current_min = float('inf') for x in range(i, i + k): for y in range(j, j + k): current_max = max(current_max, matrix[x][y]) current_min = min(current_min, matrix[x][y]) min_diff = min(min_diff, current_max - current_min) return min_diff时间复杂度:O(mnk²) —— 当k较大时性能极差
2.2 优化方向思考
暴力解法的问题在于对每个子矩阵都重复计算极值。我们可以考虑以下优化方向:
- 预处理行极值:先计算每行中所有长度为k的滑动窗口极值
- 单调队列优化:使用双端队列高效维护滑动窗口极值
- 二维极值扩展:将一维滑动窗口极值算法扩展到二维
3. 单调队列优化实现
3.1 一维滑动窗口极值
首先我们实现一个辅助函数,使用单调队列计算一维数组所有长度为k的滑动窗口极值:
def sliding_window_extremes(arr, k, is_max=True): q = collections.deque() result = [] for i, num in enumerate(arr): # 维护队列单调性 while q and ((is_max and arr[q[-1]] <= num) or (not is_max and arr[q[-1]] >= num)): q.pop() q.append(i) # 移除超出窗口的元素 if q[0] <= i - k: q.popleft() # 窗口形成后记录结果 if i >= k - 1: result.append(arr[q[0]]) return result3.2 二维扩展实现
利用上述一维算法,我们可以分两步处理二维矩阵:
- 对每行计算所有长度为k的滑动窗口极值
- 对第一步结果的每一列,再次计算长度为k的滑动窗口极值
import collections def minDifference(matrix, k): if not matrix or k == 1: return 0 m, n = len(matrix), len(matrix[0]) # 第一步:处理每行的滑动窗口最大值和最小值 row_max = [[0] * (n - k + 1) for _ in range(m)] row_min = [[0] * (n - k + 1) for _ in range(m)] for i in range(m): row = matrix[i] row_max[i] = sliding_window_extremes(row, k, is_max=True) row_min[i] = sliding_window_extremes(row, k, is_max=False) # 第二步:处理列的滑动窗口最大值和最小值 min_diff = float('inf') for j in range(n - k + 1): # 提取当前列的所有行极值 col_max = [row_max[i][j] for i in range(m)] col_min = [row_min[i][j] for i in range(m)] # 计算当前列的滑动窗口极值 window_max = sliding_window_extremes(col_max, k, is_max=True) window_min = sliding_window_extremes(col_min, k, is_max=False) # 计算最小绝对差 for x in range(len(window_max)): current_diff = window_max[x] - window_min[x] min_diff = min(min_diff, current_diff) return min_diff时间复杂度:O(m*n) —— 每个元素被处理常数次
4. 算法正确性验证
让我们用一个简单例子验证算法正确性:
输入矩阵:
[ [1, 3, 5], [4, 2, 6], [7, 8, 9] ] k = 2手动计算所有2×2子矩阵的绝对差:
- 左上角子矩阵 [[1,3],[4,2]]:max=4, min=1 → diff=3
- 右上角子矩阵 [[3,5],[2,6]]:max=6, min=2 → diff=4
- 左下角子矩阵 [[4,2],[7,8]]:max=8, min=2 → diff=6
- 右下角子矩阵 [[2,6],[8,9]]:max=9, min=2 → diff=7
最小绝对差应为3,与算法输出一致。
5. 性能对比与复杂度分析
5.1 时间复杂度对比
| 方法 | 时间复杂度 | 适用场景 |
|---|---|---|
| 暴力解法 | O(mnk²) | 小矩阵(k≤3) |
| 单调队列优化 | O(m*n) | 大矩阵 |
5.2 空间复杂度分析
优化算法需要额外存储:
- 行最大值矩阵:O(m*(n-k+1))
- 行最小值矩阵:O(m*(n-k+1))
- 列极值数组:O(m)
总空间复杂度:O(m*n) —— 与输入矩阵同阶
6. 实际编码注意事项
6.1 边界条件处理
在实际编码中需要特别注意:
- k=1时直接返回0(单个元素的绝对差为0)
- 矩阵为空或k大于矩阵尺寸时的处理
- 矩阵元素全相同时的快速返回
6.2 Python实现优化技巧
- 使用列表推导式:替代显式循环提高代码简洁性
- 提前分配空间:避免动态扩展列表带来的性能损耗
- 利用内置函数:max()/min()在k很小时可能比单调队列更快
- 输入验证:添加类型检查和范围验证
优化后的完整实现:
import collections from typing import List def minDifference(matrix: List[List[int]], k: int) -> int: if not matrix or not matrix[0] or k <= 0: return 0 if k == 1: return 0 m, n = len(matrix), len(matrix[0]) if k > m or k > n: return 0 def sliding_extremes(arr, k, is_max): q = collections.deque() res = [] for i, num in enumerate(arr): while q and ((is_max and arr[q[-1]] <= num) or (not is_max and arr[q[-1]] >= num)): q.pop() q.append(i) if q[0] <= i - k: q.popleft() if i >= k - 1: res.append(arr[q[0]]) return res # 预处理行极值 row_max = [sliding_extremes(row, k, True) for row in matrix] row_min = [sliding_extremes(row, k, False) for row in matrix] min_diff = float('inf') # 处理列极值 for j in range(n - k + 1): col_max = [row_max[i][j] for i in range(m)] col_min = [row_min[i][j] for i in range(m)] w_max = sliding_extremes(col_max, k, True) w_min = sliding_extremes(col_min, k, False) for x in range(len(w_max)): min_diff = min(min_diff, w_max[x] - w_min[x]) return min_diff7. 同类问题扩展
这种滑动窗口极值问题有很多变种:
- 最大子矩阵和:使用类似思想结合Kadane算法
- 统计特殊子矩阵数量:如全1子矩阵计数
- 子矩阵平均值:可以预处理前缀和数组
- 更高维度的扩展:如三维矩阵中的子立方体处理
对于面试准备,建议同时掌握:
- 一维滑动窗口极值(LeetCode 239)
- 二维前缀和计算(LeetCode 304)
- 单调队列的其他应用场景
提示:在面试中遇到类似问题时,可以先从暴力解法开始,然后逐步引导到优化思路,展示你的问题分析和算法优化能力。