力扣2536子矩阵元素加1-差分数组解法详解

问题描述

给你一个正整数 n,表示最初有一个 n x n 的矩阵,每个元素都是 0。

给你一个二维整数数组 queries,其中 queries[i] = [row1, col1, row2, col2]。对于每个查询 i,需要找到所有满足 row1 <= x <= row2col1 <= y <= col2 的位置 (x, y),并将这个位置上的元素加 1。

返回执行完所有查询后得到的矩阵。

暴力解法的问题

如果直接模拟,对每个查询都遍历子矩阵中的每个元素进行加1:

go 复制代码
for _, q := range queries {
    for i := q[0]; i <= q[2]; i++ {
        for j := q[1]; j <= q[3]; j++ {
            matrix[i][j]++
        }
    }
}

时间复杂度为 O(queries * n^2),在最坏情况下约为 500^2 * 10^4 = 2.5e9 操作,必然超时。

差分数组的第一性原理

一维差分数组的本质

假设有一个数组 a = [3, 5, 2, 6],我们构造它的差分数组 diff

text 复制代码
a    = [3, 5, 2, 6]
diff = [3, 2, -3, 4]

其中 diff[i] = a[i] - a[i-1](假设 a[-1] = 0):

  • diff[0] = 3 - 0 = 3
  • diff[1] = 5 - 3 = 2
  • diff[2] = 2 - 5 = -3
  • diff[3] = 6 - 2 = 4

关键观察:原数组可以通过差分数组的前缀和还原:

text 复制代码
a[0] = diff[0] = 3
a[1] = diff[0] + diff[1] = 3 + 2 = 5
a[2] = diff[0] + diff[1] + diff[2] = 3 + 2 + (-3) = 2
a[3] = diff[0] + diff[1] + diff[2] + diff[3] = 3 + 2 + (-3) + 4 = 6

a[i] = sum(diff[0..i])

为什么区间更新只需要修改两个端点?

假设要对区间 [l, r] 的所有元素加 val,直接修改需要 O(r-l+1) 次操作。

但在差分数组中,我们只需:

  • diff[l] += val
  • diff[r+1] -= val

为什么这样有效?

因为差分数组的前缀和还原性质:

  • 对于 i < l:前缀和不受影响,a[i] 不变
  • 对于 l <= i <= r:前缀和会累加 diff[l] 的增量 val,所以 a[i] += val
  • 对于 i > r:前缀和累加了 diff[l]+valdiff[r+1]-val,两者抵消,a[i] 不变

这就是差分数组的精髓:通过标记边界,利用前缀和的累积效应,实现区间更新

二维差分数组的推导

二维情况下,我们要对子矩阵 (r1,c1)(r2,c2) 的所有元素加 val

类比一维,我们需要在差分矩阵 diff 中标记边界,使得二维前缀和还原时,恰好只有目标子矩阵的元素被加上 val

二维前缀和公式

text 复制代码
result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]

要让 (r1,c1)(r2,c2) 区域加 val,我们需要:

  1. diff[r1][c1]val :从 (r1,c1) 开始,右下方所有位置都会累加这个值
  2. diff[r1][c2+1]val :阻止 c2 列右侧继续累加
  3. diff[r2+1][c1]val :阻止 r2 行下方继续累加
  4. diff[r2+1][c2+1]val:补偿右下角被重复减去的值

这四个操作确保只有目标矩形区域受到影响。

为什么是这四个位置?

让我们用容斥原理理解:

  • diff[r1][c1] += val:影响范围是从 (r1,c1)(n-1,n-1) 的整个右下区域
  • diff[r1][c2+1] -= val:减去从 (r1,c2+1) 到右下的区域,排除右侧超出部分
  • diff[r2+1][c1] -= val:减去从 (r2+1,c1) 到右下的区域,排除下方超出部分
  • diff[r2+1][c2+1] += val:加回 (r2+1,c2+1) 到右下的区域,因为它被减了两次

通过容斥,最终只有 (r1,c1)(r2,c2) 的矩形区域受到 +val 的影响。

完整示例演示

假设 n=3,queries = [[0,0,1,1]],即对子矩阵 (0,0) 到 (1,1) 加1。

步骤1:初始化差分数组

text 复制代码
diff:
0 0 0
0 0 0
0 0 0

步骤2:处理查询 [0,0,1,1]

按照四个操作:

  1. diff[0][0] += 1
text 复制代码
1 0 0
0 0 0
0 0 0
  1. diff[0][2] -= 1 (c2+1=2)
text 复制代码
1 0 -1
0 0 0
0 0 0
  1. diff[2][0] -= 1 (r2+1=2)
text 复制代码
1 0 -1
0 0 0
-1 0 0
  1. diff[2][2] += 1 (r2+1=2, c2+1=2)
text 复制代码
1 0 -1
0 0 0
-1 0 1

步骤3:二维前缀和还原

逐个计算 result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]

  • result[0][0] = 0 + 0 - 0 + 1 = 1
  • result[0][1] = 0 + 1 - 0 + 0 = 1
  • result[0][2] = 0 + 1 - 0 + (-1) = 0
  • result[1][0] = 1 + 0 - 0 + 0 = 1
  • result[1][1] = 1 + 1 - 1 + 0 = 1
  • result[1][2] = 1 + 1 - 1 + 0 = 0
  • result[2][0] = 1 + 0 - 0 + (-1) = 0
  • result[2][1] = 0 + 1 - 1 + 0 = 0
  • result[2][2] = 0 + 0 - 0 + 1 = 0

最终矩阵

text 复制代码
1 1 0
1 1 0
0 0 0

完美!只有 (0,0) 到 (1,1) 的子矩阵被加1了。

代码实现

go 复制代码
func rangeAddQueries(n int, queries [][]int) [][]int {
    // 创建差分数组
    diff := make([][]int, n+1)
    for i := range diff {
        diff[i] = make([]int, n+1)
    }
    
    // 处理每个查询,在差分数组中标记
    for _, q := range queries {
        r1, c1, r2, c2 := q[0], q[1], q[2], q[3]
        diff[r1][c1]++
        if r2+1 < n {
            diff[r2+1][c1]--
        }
        if c2+1 < n {
            diff[r1][c2+1]--
        }
        if r2+1 < n && c2+1 < n {
            diff[r2+1][c2+1]++
        }
    }
    
    // 计算二维前缀和,得到最终矩阵
    result := make([][]int, n)
    for i := range result {
        result[i] = make([]int, n)
    }
    
    for i := 0; i < n; i++ {
        for j := 0; j < n; j++ {
            // result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]
            if i > 0 {
                result[i][j] += result[i-1][j]
            }
            if j > 0 {
                result[i][j] += result[i][j-1]
            }
            if i > 0 && j > 0 {
                result[i][j] -= result[i-1][j-1]
            }
            result[i][j] += diff[i][j]
        }
    }
    
    return result
}
相关推荐
汗流浃背了吧,老弟!3 小时前
中文分词全切分算法
算法·中文分词·easyui
~~李木子~~3 小时前
贪心算法实验1
算法·ios·贪心算法
·云扬·3 小时前
【LeetCode Hot 100】 136. 只出现一次的数字
算法·leetcode·职场和发展
Xiaochen_123 小时前
有边数限制的最短路:Bellman-Ford 算法
c语言·数据结构·c++·程序人生·算法·学习方法·最简单的算法理解
熬了夜的程序员4 小时前
【LeetCode】114. 二叉树展开为链表
leetcode·链表·深度优先
西西弗Sisyphus7 小时前
线性代数 - 理解求解矩阵特征值的特征方程
线性代数·矩阵·特征值·特征向量
大胆飞猪8 小时前
递归、剪枝、回溯算法---全排列、子集问题(力扣.46,78)
算法·leetcode·剪枝
Kisorge10 小时前
【电机控制】基于STM32F103C8T6的二轮平衡车设计——LQR线性二次线控制器(算法篇)
stm32·嵌入式硬件·算法
铭哥的编程日记11 小时前
深入浅出蓝桥杯:算法基础概念与实战应用(二)基础算法(下)
算法·职场和发展·蓝桥杯