598. 区间加法 II
问题描述
给你一个 m x n
的矩阵 M
,初始时全部元素为 0。在 M
上执行 k
次操作,其中每个操作用一个长度为 2 的正整数数组 ops[i] = [ai, bi]
表示。
在第 i
次操作中,对所有满足 0 ≤ r < ai
且 0 ≤ c < bi
的元素 M[r][c]
加 1。
请你返回 k
次操作后,M
中最大整数的元素个数。
示例:
输入: m = 3, n = 3, ops = [[2,2],[3,3]]
输出: 4
解释:
初始矩阵: [[0,0,0],[0,0,0],[0,0,0]]
操作[2,2]: [[1,1,0],[1,1,0],[0,0,0]]
操作[3,3]: [[2,2,1],[2,2,1],[1,1,1]]
最大整数是2,出现了4次。
算法思路
核心
- 操作特性:每次操作都是对左上角矩形区域
[0,0]
到[ai-1,bi-1]
加 1 - 关键:被所有操作覆盖的区域,其值最大
- 交集思想:找所有操作矩形的最大公共区域
贪心策略
- 每次操作都会影响左上角的一个矩形区域
- 被操作次数最多的区域 = 被所有操作都覆盖的区域
- 这个区域是所有操作矩形的交集
数学分析
设:
minRow = min(ops[i][0] 到 ops[m][0])
minCol = min(ops[i][1] 到 ops[m][1])
则最大值区域为左上角的 minRow × minCol
矩形,且该区域的值等于操作次数 k
。
为什么?
- 任何超出
minRow
或minCol
的位置,至少被一个操作遗漏 - 只有
[0, minRow) × [0, minCol)
区域被所有操作覆盖
代码实现
class Solution {
/**
* 计算区间加法后矩阵中最大整数的元素个数
*
* @param m 矩阵行数
* @param n 矩阵列数
* @param ops 操作数组,每个操作[ai, bi]表示对[0,ai)×[0,bi)区域加1
* @return 最大整数的元素个数
*/
public int maxCount(int m, int n, int[][] ops) {
// 边界情况:如果没有操作,所有元素都是0
if (ops.length == 0) {
return m * n;
}
// 找到所有操作中行范围的最小值
int minRow = Integer.MAX_VALUE;
// 找到所有操作中列范围的最小值
int minCol = Integer.MAX_VALUE;
// 遍历所有操作
for (int[] op : ops) {
int rows = op[0]; // 当前操作影响的行数
int cols = op[1]; // 当前操作影响的列数
// 更新最小行范围
minRow = Math.min(minRow, rows);
// 更新最小列范围
minCol = Math.min(minCol, cols);
}
// 最大值区域是左上角的 minRow × minCol 矩形
// 但不能超过矩阵本身的大小
int resultRow = Math.min(minRow, m);
int resultCol = Math.min(minCol, n);
return resultRow * resultCol;
}
}
优化(更简洁)
class Solution {
/**
* 优化:代码更简洁
*
* @param m 矩阵行数
* @param n 矩阵列数
* @param ops 操作数组
* @return 最大整数的元素个数
*/
public int maxCount(int m, int n, int[][] ops) {
int minRow = m; // 初始值设为矩阵行数
int minCol = n; // 初始值设为矩阵列数
// 遍历所有操作,找最小的行和列范围
for (int[] op : ops) {
minRow = Math.min(minRow, op[0]);
minCol = Math.min(minCol, op[1]);
}
return minRow * minCol;
}
}
算法分析
-
时间复杂度:O(k)
- k 是操作次数
- 需要遍历所有操作一次
-
空间复杂度:O(1)
- 只使用常数额外空间
-
关键:
- 避免了实际构建和操作矩阵
- 直接通过数学分析得出结果
- 时间复杂度从 O(k×m×n) 优化到 O(k)
算法过程
m=3, n=3, ops=[[2,2],[3,3]]
:
1:分析每个操作的影响区域
- 操作
[2,2]
:影响[0,2) × [0,2)
= 前2行前2列 - 操作
[3,3]
:影响[0,3) × [0,3)
= 全部3×3区域
2:找所有操作的交集
- 行范围交集:min(2,3) = 2
- 列范围交集:min(2,3) = 2
- 公共区域:2×2 的左上角区域
3:计算结果
- 该区域被所有操作覆盖,值为2(操作次数)
- 区域大小:2 × 2 = 4
- 返回 4
验证过程:
初始: [[0,0,0],
[0,0,0],
[0,0,0]]
操作[2,2]: [[1,1,0],
[1,1,0],
[0,0,0]]
操作[3,3]: [[2,2,1],
[2,2,1],
[1,1,1]]
最大值2出现了4次,正确。
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[][] ops1 = {{2,2},{3,3}};
System.out.println("Test 1: " + solution.maxCount(3, 3, ops1)); // 4
// 测试用例2:无操作
int[][] ops2 = {};
System.out.println("Test 2: " + solution.maxCount(3, 3, ops2)); // 9
// 所有元素都是0,最大值0出现9次
// 测试用例3:操作范围超出矩阵
int[][] ops3 = {{4,4},{2,2}};
System.out.println("Test 3: " + solution.maxCount(3, 3, ops3)); // 4
// minRow=min(4,2)=2, minCol=min(4,2)=2 → 2×2=4
// 测试用例4:单个操作
int[][] ops4 = {{1,1}};
System.out.println("Test 4: " + solution.maxCount(3, 3, ops4)); // 1
// 只有[0,0]位置被加1
// 测试用例5:多个操作,最小范围为1
int[][] ops5 = {{1,3},{3,1},{2,2}};
System.out.println("Test 5: " + solution.maxCount(3, 3, ops5)); // 1
// minRow=min(1,3,2)=1, minCol=min(3,1,2)=1 → 1×1=1
// 测试用例6:大矩阵小操作
int[][] ops6 = {{2,2},{2,2}};
System.out.println("Test 6: " + solution.maxCount(100, 100, ops6)); // 4
// 与矩阵大小无关,只与操作有关
}
关键点
-
交集思想:
- 被最多次操作覆盖的区域 = 所有操作区域的
交集
- 交集的大小由最小的操作范围决定
- 被最多次操作覆盖的区域 = 所有操作区域的
-
避免暴力模拟:
- 如果实际模拟每次操作,时间复杂度为 O(k×m×n)
- 通过数学分析优化到 O(k)
-
边界处理:
- 无操作时,所有元素都是0,最大值出现 m×n 次
- 操作范围可能大于矩阵大小,需取最小值
-
贪心正确性证明:
- 任何不在
[0,minRow)×[0,minCol)
的位置 - 至少被一个操作遗漏(因为该操作的范围更小)
- 所以这些位置的值 < k
- 只有交集区域的值 = k
- 任何不在
常见问题
-
为什么只需要找最小值?
- 因为操作区域都是从
(0,0)
开始的矩形 - 所有操作都覆盖的区域由最小的行和列范围决定
- 这是矩形交集的性质
- 因为操作区域都是从
-
如果操作不是从(0,0)开始怎么办?
- 本题的操作都是从左上角开始
- 如果是任意矩形,就需要更复杂的区间树或差分数组
-
矩阵大小m,n的作用?
- 在优化版本中似乎没用到
- 实际上,当
minRow > m
或minCol > n
时需要限制 - 但在代码中,直接用
minRow = min(minRow, op[0])
,初始值为m,所以自动处理了边界
-
算法的直观理解:
- 想象每次操作都在左上角贴一个矩形贴纸
- 被所有贴纸都覆盖的区域就是重叠最多的区域
- 这个区域的大小由最小的贴纸决定
-
与区间更新的区别:
- 本题是多次区间加法,但起始点固定
- 如果是任意区间的更新,通常用差分数组或线段树
- 本题的特殊性允许O(1)空间解法