ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LeetCode 598 区间加法 II:从暴力模拟到数学最优解

LeetCode 598 区间加法 II:从暴力模拟到数学最优解 在实际算法面试和日常编程训练中LeetCode 上的“区间加法 II”这类题目考察的往往不是复杂的循环或递归而是对问题本质的洞察和数学抽象能力。很多开发者一看到“区间操作”、“累加”等字眼可能会下意识地想到模拟整个矩阵的填充过程结果导致在数据范围较大时超时或内存溢出。本文将带你深入解析力扣第 598 题“区间加法 II”通过 Python 语言揭示其背后的数学规律并给出从暴力模拟到最优解法的完整演进路径。无论你是正在准备技术面试还是希望提升自己的算法思维理解这道题都能让你在面对类似“范围覆盖”、“最大重叠”问题时拥有更清晰的解题思路。我们将从理解题意开始逐步分析为什么直接模拟不可行然后推导出寻找所有操作区间交集的核心思想最后给出简洁高效的 Python 实现并讨论其时间复杂度和空间复杂度。文章还会包含详细的代码注释、测试用例以及在实际编码中容易踩到的坑。1. 理解问题区间加法 II 到底在问什么题目描述通常如下给定一个初始所有元素均为 0 的m x n矩阵M以及一系列操作ops。每个操作ops[i] [ai, bi]表示对于所有满足0 i ai且0 j bi的元素M[i][j]将其值加 1。在执行完所有操作后你需要返回矩阵中最大整数的个数。简单来说每次操作都会给矩阵左上角的一个矩形区域从(0,0)到(ai-1, bi-1)内的所有元素加 1。我们的目标是找出经过所有操作后矩阵中值最大的元素有多少个。1.1 一个直观的例子假设m 3,n 3初始矩阵为[[0, 0, 0], [0, 0, 0], [0, 0, 0]]操作ops [[2,2], [3,3]]。执行[2,2]将前2行、前2列的区域即左上角2x2的矩形加1。执行[3,3]将前3行、前3列的区域即整个3x3的矩形加1。最终矩阵变为[[2, 2, 1], [2, 2, 1], [1, 1, 1]]最大值为2出现的位置是左上角2x2的区域共4个。所以答案是4。1.2 关键洞察最大值的来源为什么最大值是2并且只出现在左上角因为元素M[i][j]的值等于覆盖了该位置(i, j)的操作数量。一个位置被越多的操作区间覆盖它的值就越大。因此矩阵中的最大值就是被所有操作共同覆盖的那些位置的值。有多少个操作最大值就是多少。而最大值的个数就是被所有操作共同覆盖的区域的面积。那么如何找到被所有操作共同覆盖的区域观察每个操作[a, b]它覆盖的行范围是[0, a-1]列范围是[0, b-1]。所有操作共同覆盖的行范围就是所有a的最小值对应的行范围所有操作共同覆盖的列范围就是所有b的最小值对应的列范围。结论最大值的个数 min_a * min_b其中min_a是所有操作中ai的最小值min_b是所有操作中bi的最小值。同时这个范围不能超过矩阵本身的大小m和n。2. 从暴力模拟到最优解法在推导出数学解法之前我们先看看最直接的思路及其局限性这能帮助我们更好地理解最优解法的价值。2.1 暴力模拟法及其缺陷最直观的方法是按照题目描述创建一个m x n的二维数组然后遍历每个操作[a, b]将对应的矩形区域内的每个元素加1。最后遍历整个矩阵统计最大值的个数。def maxCount_bruteforce(m: int, n: int, ops: List[List[int]]) - int: # 初始化矩阵 matrix [[0] * n for _ in range(m)] # 执行所有操作 for a, b in ops: for i in range(a): for j in range(b): matrix[i][j] 1 # 找出最大值并统计个数 max_val 0 count 0 for i in range(m): for j in range(n): if matrix[i][j] max_val: max_val matrix[i][j] count 1 elif matrix[i][j] max_val: count 1 return count复杂度分析时间复杂度O(K * m * n)其中 K 是操作的数量len(ops)。在最坏情况下每个操作都覆盖整个矩阵复杂度约为 O(K * m * n)这是不可接受的。空间复杂度O(m * n)用于存储整个矩阵。当m和n很大题目可能达到 40000且操作较多时这种解法必然会超时或超出内存限制。因此我们必须寻找更优解。2.2 最优解法寻找最小交集根据第1部分的结论我们不需要模拟整个过程只需要找到所有操作区间在行和列方向上的最小边界。初始化min_a m,min_b n。因为矩阵本身的大小(m, n)是所有操作的理论上限。遍历每个操作[a, b]min_a min(min_a, a)min_b min(min_b, b)最终结果即为min_a * min_b。这个解法的核心在于最终值最大的区域就是行方向上被所有操作覆盖的最小行数min_a和列方向上被所有操作覆盖的最小列数min_b所围成的矩形区域。from typing import List def maxCount(m: int, n: int, ops: List[List[int]]) - int: 计算执行所有区间加法操作后矩阵中最大整数的个数。 参数: m (int): 矩阵行数。 n (int): 矩阵列数。 ops (List[List[int]]): 操作列表每个操作形如 [ai, bi]。 返回: int: 最大整数的个数。 # 初始化最小行和最小列为矩阵边界 min_a, min_b m, n # 遍历所有操作更新最小行和最小列 for a, b in ops: min_a min(min_a, a) min_b min(min_b, b) # 最大整数的个数即为最小交集矩形的面积 return min_a * min_b复杂度分析时间复杂度O(K)其中 K 是操作的数量len(ops)。我们只需要遍历一次操作列表。空间复杂度O(1)只使用了常数级别的额外空间。3. 代码实现与详细解释让我们深入分析上面最优解法的代码并理解每个细节。3.1 函数签名与参数处理from typing import List def maxCount(m: int, n: int, ops: List[List[int]]) - int:我们使用typing模块中的List来标注类型提高代码可读性。函数接收三个参数矩阵的行数m、列数n以及操作列表ops。3.2 初始化与遍历逻辑min_a, min_b m, n这里将min_a和min_b初始化为m和n。这是因为如果ops为空列表没有任何操作那么整个矩阵的最大值依然是0最大值的个数就是整个矩阵的面积m * n。初始化为此值可以优雅地处理ops为空的情况。for a, b in ops: min_a min(min_a, a) min_b min(min_b, b)遍历每个操作。a和b分别代表当前操作能影响到的行数和列数从0开始计数。通过不断取最小值我们找到了所有操作在行和列方向上的“最大公约数”区域即被所有操作共同覆盖的区域。注意操作[a, b]影响的行索引是0到a-1列索引是0到b-1。因此a和b直接决定了矩形的大小min(a)和min(b)直接决定了共同区域的大小。不需要进行a-1或b-1的操作。3.3 返回结果return min_a * min_b最终共同区域的行数为min_a列数为min_b该矩形区域内每一个单元格都被所有操作覆盖了因此它们的值就是操作次数也是矩阵中的最大值。这个矩形的面积min_a * min_b就是最大值的个数。4. 运行验证与测试用例编写完算法后必须用多种测试用例进行验证确保其正确性和健壮性。4.1 基础测试我们可以直接在 Python 交互环境或一个简单的脚本中测试。# test_maxcount.py from typing import List def maxCount(m: int, n: int, ops: List[List[int]]) - int: min_a, min_b m, n for a, b in ops: min_a min(min_a, a) min_b min(min_b, b) return min_a * min_b # 测试用例1: 题目示例 assert maxCount(3, 3, [[2,2],[3,3]]) 4 print(Test 1 passed.) # 测试用例2: 无操作 assert maxCount(3, 3, []) 9 # 全为0最大值0的个数是9 print(Test 2 passed.) # 测试用例3: 单个操作 assert maxCount(3, 3, [[1,1]]) 1 # 只有(0,0)位置被加1 print(Test 3 passed.) # 测试用例4: 操作区间超出矩阵但题目保证 ai m, bi n # 这里测试 ai, bi 等于边界的情况 assert maxCount(3, 3, [[3,2],[2,3]]) 4 # 共同区域是2x2 print(Test 4 passed.) # 测试用例5: 操作区间逐渐变小 assert maxCount(40000, 40000, [[39999,39999],[20000,30000],[10,20]]) 10*20 # 200 print(Test 5 passed.) print(All tests passed!)运行这个脚本如果所有断言通过则说明我们的算法在这些场景下是正确的。4.2 复杂度验证为了直观感受优化效果我们可以模拟一个大规模场景仅做理解不实际运行暴力解法。import time m, n 40000, 40000 # 模拟1000个随机操作但保证 ai, bi 在合理范围 import random random.seed(42) ops [[random.randint(1, 40000), random.randint(1, 40000)] for _ in range(1000)] start time.time() result maxCount(m, n, ops) end time.time() print(fResult: {result}) print(fTime taken (optimal): {end - start:.6f} seconds) # 暴力解法在此数据规模下无法运行此处仅用于对比思路对于最优解法即使m, n40000且有 1000 个操作运行时间也仅在毫秒级别因为时间复杂度是 O(1000)。5. 常见问题与排查指南即使理解了算法在实现和调试时也可能遇到一些问题。5.1 问题清单与解决方案问题现象可能原因检查与解决方案结果比预期小初始化错误。如果ops为空结果应为m*n。若将min_a/min_b初始化为0则结果会为0。确认min_a和min_b初始化为m和n。结果比预期大逻辑理解偏差。误以为min_a和min_b是操作中a和b的最大值。重新审题最大值出现在被所有操作覆盖的区域即a和b的最小值决定的区域。处理ops为空时出错代码没有考虑ops为空列表的情况导致遍历出错或返回0。确保循环for a, b in ops:在ops为空时能安全跳过并且初始化值能给出正确结果 (m*n)。输入参数类型错误函数接收的ops可能不是严格的List[List[int]]或者m,n不是整数。在函数开始添加类型检查或断言适用于调试。在实际LeetCode环境中输入是规范的。5.2 思维误区澄清误区一需要模拟矩阵操作纠正题目只要求最大值的个数不关心中间过程和其他值。通过数学分析可以直接定位到最大值区域。误区二min_a和min_b需要减1纠正操作[a, b]影响的行是0到a-1共a行。所以所有操作共同影响的行数就是最小的那个a即min_a。面积是min_a * min_b不需要减1。误区三需要考虑操作顺序纠正因为加法操作是可交换和可结合的最终每个位置的值只取决于它被多少个操作覆盖与操作执行的顺序无关。所以我们的解法是普适的。6. 最佳实践与扩展思考掌握了本题的核心解法后我们可以进一步思考如何将其内化为一种解题模式并应用到其他场景。6.1 算法思维总结本题的优化过程体现了算法设计中一种重要的思想根据问题目标简化或跳过不必要的计算。当题目只关心极值、总和、是否存在等聚合信息时往往不需要模拟出完整的状态。类似的LeetCode题目还有第 453 题“最小操作次数使数组元素相等”不一定真去操作找到数学关系。第 419 题“甲板上的战舰”不需要模拟攻击过程通过战舰头部的特征计数。第 598 题本身就是很好的例子。在面试中遇到涉及“所有区间的交集”、“最大重叠次数”、“最小公共范围”的问题时可以优先考虑是否可以通过遍历一次数据维护几个关键变量如最小值、最大值来得到答案。6.2 代码健壮性建议防御性编程虽然LeetCode保证输入有效但在实际工程中应对输入进行检查。def maxCount_robust(m: int, n: int, ops: List[List[int]]) - int: if not isinstance(m, int) or not isinstance(n, int) or m 0 or n 0: return 0 # 或抛出异常 min_a, min_b m, n for op in ops: if len(op) ! 2: continue # 或跳过非法操作 a, b op[0], op[1] # 确保操作在矩阵范围内 if a 0 and b 0: min_a min(min_a, a) min_b min(min_b, b) return min_a * min_b使用生成器如果操作列表非常大可以考虑使用生成器来节省内存但本题中通常不需要。6.3 扩展与变种如果操作不是加1而是加一个任意值val呢问题会变得更复杂因为最大值可能出现在被叠加权重和最大的区域而不仅仅是重叠次数最多的区域。这可能需要使用二维差分数组或更复杂的数据结构来高效解决。如果要求返回最大值的具体位置呢在计算出min_a和min_b后最大值区域就是[0, min_a-1]行和[0, min_b-1]列围成的矩形。可以返回这个矩形内所有的坐标。如果操作是任意的矩形区域不一定从(0,0)开始呢这就变成了经典的“区间叠加求最大重叠次数”问题可以使用扫描线算法来解决。理解“区间加法 II”的数学本质不仅是为了解决一道题更是为了培养一种化繁为简的算法直觉。在面临复杂问题时先问目标是什么再判断是否需要所有中间数据这种思维能帮助你在面试和实际开发中更高效地找到解决方案。下一步可以尝试用这种“寻找关键交集”的思路去解决 LeetCode 上标签为“Math”或“Array”的类似题目巩固这一技巧。
返回列表