1. 题目解析:理解子矩阵最小绝对差问题
这道LeetCode中等难度题目(编号3567)要求我们处理一个二维矩阵,找出所有可能的子矩阵,并计算这些子矩阵中最小元素与最大元素的绝对差,最终返回所有子矩阵中最小的那个绝对差值。
理解题目有几个关键点:
- 子矩阵是指原矩阵中任意连续的行和列组成的矩形区域
- 对于每个子矩阵,我们需要找出其中的最小值和最大值
- 计算这两个值的绝对差
- 最终返回所有子矩阵中最小的那个绝对差
举个例子,给定矩阵:
code复制[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
其中一个2x2的子矩阵是:
code复制[1, 2]
[4, 5]
这个子矩阵的最小值是1,最大值是5,绝对差是4。我们需要计算所有可能的子矩阵的这种差值,找出其中最小的那个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法思路与实现
最直观的解法就是暴力枚举所有可能的子矩阵,然后计算每个子矩阵的最小值和最大值,最后找出最小的绝对差。
2.1 暴力解法的实现步骤
- 遍历所有可能的子矩阵起始点(i,j)
- 对于每个起始点,遍历所有可能的子矩阵大小(k,l)
- 对于每个子矩阵,找出最小值和最大值
- 计算它们的绝对差
- 记录所有绝对差中的最小值
Python实现代码:
python复制def minAbsoluteDifference(grid):
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
min_diff = float('inf')
for i in range(rows):
for j in range(cols):
# 子矩阵的右下角
for k in range(i, rows):
for l in range(j, cols):
# 找出子矩阵中的最小和最大值
min_val = float('inf')
max_val = -float('inf')
for x in range(i, k+1):
for y in range(j, l+1):
min_val = min(min_val, grid[x][y])
max_val = max(max_val, grid[x][y])
# 计算绝对差
current_diff = abs(max_val - min_val)
min_diff = min(min_diff, current_diff)
return min_diff
2.2 暴力解法的时间复杂度分析
这个暴力解法的时间复杂度非常高:
- 外层四个循环遍历所有子矩阵:O(n²m²)
- 内层两个循环找出子矩阵的最小最大值:O(nm)
- 总时间复杂度:O(n³m³)
对于较大的矩阵(比如100x100),这个解法显然会超时。因此我们需要寻找更优化的解法。
3. 优化思路:单调队列与动态规划
为了优化这个解法,我们需要避免重复计算子矩阵的最小值和最大值。这里可以考虑使用单调队列或者动态规划的方法。
3.1 基于单调队列的优化
我们可以分别计算每个可能子矩阵的最小值和最大值,然后计算它们的差值。使用单调队列可以在O(nm)时间内预处理出每个子矩阵的最小值和最大值。
具体步骤:
- 预处理每行的滑动窗口最小值和最大值
- 预处理每列的滑动窗口最小值和最大值
- 组合行列预处理结果得到子矩阵的最小最大值
Python实现:
python复制from collections import deque
def minAbsoluteDifference(grid):
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
min_diff = float('inf')
# 预处理每行的滑动窗口最小值和最大值
row_mins = [[0]*cols for _ in range(rows)]
row_maxs = [[0]*cols for _ in range(rows)]
for i in range(rows):
q_min = deque()
q_max = deque()
for j in range(cols):
# 维护最小值队列
while q_min and grid[i][q_min[-1]] >= grid[i][j]:
q_min.pop()
q_min.append(j)
# 维护最大值队列
while q_max and grid[i][q_max[-1]] <= grid[i][j]:
q_max.pop()
q_max.append(j)
# 移除超出窗口的元素
while q_min[0] <= j - cols:
q_min.popleft()
while q_max[0] <= j - cols:
q_max.popleft()
row_mins[i][j] = grid[i][q_min[0]]
row_maxs[i][j] = grid[i][q_max[0]]
# 预处理每列的滑动窗口最小值和最大值
col_mins = [[0]*cols for _ in range(rows)]
col_maxs = [[0]*cols for _ in range(rows)]
for j in range(cols):
q_min = deque()
q_max = deque()
for i in range(rows):
# 维护最小值队列
while q_min and row_mins[q_min[-1]][j] >= row_mins[i][j]:
q_min.pop()
q_min.append(i)
# 维护最大值队列
while q_max and row_maxs[q_max[-1]][j] <= row_maxs[i][j]:
q_max.pop()
q_max.append(i)
# 移除超出窗口的元素
while q_min[0] <= i - rows:
q_min.popleft()
while q_max[0] <= i - rows:
q_max.popleft()
col_mins[i][j] = row_mins[q_min[0]][j]
col_maxs[i][j] = row_maxs[q_max[0]][j]
# 计算当前绝对差
current_diff = abs(col_maxs[i][j] - col_mins[i][j])
min_diff = min(min_diff, current_diff)
return min_diff
3.2 动态规划优化思路
另一种优化方法是使用动态规划来预处理子矩阵的最小值和最大值。我们可以定义:
- min_dp[k][i][j]:以(i,j)为右下角,大小为k的子矩阵的最小值
- max_dp[k][i][j]:以(i,j)为右下角,大小为k的子矩阵的最大值
然后可以通过递推关系式来计算这些值:
code复制min_dp[k][i][j] = min(grid[i][j], min_dp[k-1][i-1][j], min_dp[k-1][i][j-1], min_dp[k-1][i-1][j-1])
max_dp[k][i][j] = max(grid[i][j], max_dp[k-1][i-1][j], max_dp[k-1][i][j-1], max_dp[k-1][i-1][j-1])
这种方法的时间复杂度可以降到O(n²m),对于中等大小的矩阵已经足够高效。
4. 最优解法:四维动态规划
结合上述思路,我们可以设计一个更优的四维动态规划解法,直接计算所有可能子矩阵的最小绝对差。
4.1 状态定义
定义两个四维数组:
- min_dp[i1][j1][i2][j2]:从(i1,j1)到(i2,j2)子矩阵的最小值
- max_dp[i1][j1][i2][j2]:从(i1,j1)到(i2,j2)子矩阵的最大值
初始状态:
- 当i1==i2且j1==j2时,min_dp和max_dp都等于grid[i1][j1]
4.2 状态转移方程
对于一般情况:
code复制min_dp[i1][j1][i2][j2] = min(
grid[i2][j2],
min_dp[i1][j1][i2-1][j2],
min_dp[i1][j1][i2][j2-1],
min_dp[i1][j1][i2-1][j2-1]
)
max_dp[i1][j1][i2][j2] = max(
grid[i2][j2],
max_dp[i1][j1][i2-1][j2],
max_dp[i1][j1][i2][j2-1],
max_dp[i1][j1][i2-1][j2-1]
)
4.3 实现代码
Python实现:
python复制def minAbsoluteDifference(grid):
if not grid or not grid[0]:
return 0
rows = len(grid)
cols = len(grid[0])
# 初始化DP表
min_dp = [[[[0 for _ in range(cols)] for __ in range(rows)] for ___ in range(cols)] for ____ in range(rows)]
max_dp = [[[[0 for _ in range(cols)] for __ in range(rows)] for ___ in range(cols)] for ____ in range(rows)]
min_diff = float('inf')
for i1 in range(rows):
for j1 in range(cols):
min_dp[i1][j1][i1][j1] = grid[i1][j1]
max_dp[i1][j1][i1][j1] = grid[i1][j1]
for i2 in range(i1, rows):
for j2 in range(j1, cols):
if i1 == i2 and j1 == j2:
continue
if i2 > i1 and j2 > j1:
min_val = min(
grid[i2][j2],
min_dp[i1][j1][i2-1][j2],
min_dp[i1][j1][i2][j2-1],
min_dp[i1][j1][i2-1][j2-1]
)
max_val = max(
grid[i2][j2],
max_dp[i1][j1][i2-1][j2],
max_dp[i1][j1][i2][j2-1],
max_dp[i1][j1][i2-1][j2-1]
)
elif i2 > i1:
min_val = min(grid[i2][j2], min_dp[i1][j1][i2-1][j2])
max_val = max(grid[i2][j2], max_dp[i1][j1][i2-1][j2])
else: # j2 > j1
min_val = min(grid[i2][j2], min_dp[i1][j1][i2][j2-1])
max_val = max(grid[i2][j2], max_dp[i1][j1][i2][j2-1])
min_dp[i1][j1][i2][j2] = min_val
max_dp[i1][j1][i2][j2] = max_val
current_diff = abs(max_val - min_val)
min_diff = min(min_diff, current_diff)
return min_diff
4.4 复杂度分析
这个解法的时间复杂度是O(n²m²),空间复杂度也是O(n²m²)。虽然看起来和暴力解法一样,但实际上通过动态规划避免了重复计算,效率要高得多。
5. 实际应用中的优化技巧
在实际编码面试或竞赛中,我们还可以采用一些优化技巧来进一步提高效率:
5.1 空间优化
注意到我们只需要计算子矩阵的最小绝对差,而不需要保存所有的min_dp和max_dp值。因此可以优化空间复杂度:
python复制def minAbsoluteDifference(grid):
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
min_diff = float('inf')
for i1 in range(rows):
for j1 in range(cols):
# 当前子矩阵的最小值和最大值
current_min = grid[i1][j1]
current_max = grid[i1][j1]
for i2 in range(i1, rows):
if i2 > i1:
# 更新当前列的最小最大值
col_min = grid[i2][j1]
col_max = grid[i2][j1]
for j2 in range(j1, cols):
col_min = min(col_min, grid[i2][j2])
col_max = max(col_max, grid[i2][j2])
if j2 == j1:
current_min = col_min
current_max = col_max
else:
current_min = min(current_min, col_min)
current_max = max(current_max, col_max)
current_diff = abs(current_max - current_min)
min_diff = min(min_diff, current_diff)
else:
for j2 in range(j1, cols):
if j2 == j1:
current_min = grid[i1][j1]
current_max = grid[i1][j1]
else:
current_min = min(current_min, grid[i1][j2])
current_max = max(current_max, grid[i1][j2])
current_diff = abs(current_max - current_min)
min_diff = min(min_diff, current_diff)
return min_diff
5.2 提前终止
当发现当前子矩阵的绝对差已经是0时,可以直接返回,因为这是可能的最小值:
python复制if min_diff == 0:
return 0
5.3 边界情况处理
在实际编码中,还需要注意一些边界情况:
- 空矩阵或单元素矩阵
- 所有元素相同的矩阵
- 非常大的矩阵(需要确保算法效率)
6. 复杂度对比与选择
让我们总结一下各种解法的时间复杂度:
| 解法类型 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(n³m³) | O(1) | 仅适用于极小矩阵 |
| 单调队列优化 | O(n²m) | O(nm) | 中等大小矩阵 |
| 动态规划 | O(n²m²) | O(n²m²) | 小到中等矩阵 |
| 空间优化DP | O(n²m²) | O(1) | 中等大小矩阵 |
在实际面试中,建议按照以下步骤进行:
- 先提出暴力解法,分析其复杂度
- 提出优化思路(单调队列或DP)
- 实现优化后的解法
- 讨论可能的进一步优化(空间优化、提前终止等)
7. 常见错误与调试技巧
在解决这个问题时,容易犯的一些错误包括:
7.1 子矩阵定义错误
错误地认为子矩阵必须是正方形,或者忽略了单行/单列的子矩阵。正确的子矩阵定义应该是任意连续的行和列组成的矩形。
7.2 边界条件处理不当
特别是在动态规划实现中,容易忽略i1==i2或j1==j2的情况,导致数组越界或错误结果。
7.3 复杂度估计错误
可能会低估暴力解法的时间复杂度,认为O(n²m²)足够,实际上内层还有找最小最大值的O(nm)操作。
调试技巧:
- 先用小矩阵测试(如2x2或3x3)
- 打印中间结果,验证min_dp和max_dp的计算是否正确
- 对于边界情况单独测试
8. 类似题目推荐
为了巩固这类问题的解法,可以尝试以下类似题目:
- LeetCode 1504. 统计全1子矩阵
- LeetCode 1074. 元素和为目标值的子矩阵数量
- LeetCode 363. 矩形区域不超过K的最大数值和
- LeetCode 85. 最大矩形
这些题目都涉及到子矩阵的处理,可以使用类似的优化技巧。
