1351-统计有序矩阵中的负数
给你一个 m * n
的矩阵 grid
,矩阵中的元素无论是按行还是按列,都以非递增顺序排列。 请你统计并返回 grid
中 负数
的数目。
示例 1:
**输入:** grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
**输出:** 8
**解释:** 矩阵中共有 8 个负数。
示例 2:
**输入:** grid = [[3,2],[1,0]]
**输出:** 0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 100
-100 <= grid[i][j] <= 100
进阶: 你可以设计一个时间复杂度为 O(n + m)
的解决方案吗?
方法一:暴力
观察数据范围注意到矩阵大小不会超过 100*100=10^4,所以我们可以遍历矩阵所有数,统计负数的个数。
1 | class Solution { |
复杂度分析
- 时间复杂度:O(nm),即矩阵元素的总个数。
- 空间复杂度:O(1)。
方法二:二分查找
注意到题目中给了一个性质,即矩阵中的元素无论是按行还是按列,都以非递增顺序排列,可以考虑把这个性质利用起来优化暴力。已知这个性质告诉了我们每一行的数都是有序的,所以我们通过二分查找可以找到每一行中从前往后的第一个负数,那么这个位置之后到这一行的末尾里所有的数必然是负数了,可以直接统计。
遍历矩阵的每一行。
二分查找到该行从前往后的第一个负数,考虑第 i 行,我们记这个位置为 pos_i,那么第 i 行 [pos_i,m-1] 中的所有数都是负数,所以这一行对答案的贡献就是 m-1-pos_i+1=m-pos_i。
最后的答案就是 \sum_{i=0}^{n-1}(m-pos_i)。
1 | class Solution { |
复杂度分析
- 时间复杂度:二分查找一行的时间复杂度为logm,需要遍历n行,所以总时间复杂度是O(nlogm)。
- 空间复杂度:O(1)。
方法三:分治
方法二其实只利用了一部分的性质,即每一行是非递增的,但其实整个矩阵是每行每列均非递增,这说明了一个更重要的性质:每一行从前往后第一个负数的位置是不断递减的,即我们设第 i 行的第一个负数的位置为 pos_i,不失一般性,我们把一行全是正数的 pos 设为 m,则
pos_0>=pos_1>=pos_2>=…>=pos_{n-1}
所以我们可以依此设计一个分治算法。
我们设计一个函数 solve(l,r,L,R) 表示我们在统计 [l,r] 行的答案,第 [l,r] 行 pos 的位置在 [L,R] 列中,计算 [l,r] 的中间行第 mid 行的的 pos_{mid,算完以后根据之前的方法计算这一行对答案的贡献。然后根据我们之前发现的性质,可以知道 [l,mid-1] 中所有行的 pos 是大于等于 pos_{mid,[mid+1,r] 中所有行的 pos 值是小于等于 pos_{mid 的,所以可以分成两部分递归下去,即:
solve(l,mid-1,pos_{mid},R)
和
solve(mid+1,r,L,pos_{mid})
所以答案就是 m-pos_{mid}+solve(l,mid-1,pos_{mid},R)+solve(mid+1,r,L,pos_{mid})。
递归函数入口为 solve(0,n-1,0,m-1)。
1 | class Solution { |
复杂度分析
时间复杂度:代码中找第一个负数的位置是直接遍历 [L,R] 找的,再考虑到 n 和 m 同阶,所以每个 solve 函数里需要消耗 O(n) 的时间,由主定理可得时间复杂度为:
T(n)=2T(n/2)+O(n)=O(nlogn)
空间复杂度:O(1)。
方法四:倒序遍历
考虑方法三发现的性质,我们可以设计一个更简单的方法。考虑我们已经算出第 i 行的从前往后第一个负数的位置 pos_i,那么第 i+1 行的时候,pos_{i+1 的位置肯定是位于 [0,pos_i] 中,所以对于第 i+1 行我们倒着从 pos_i 循环找 pos_{i+1 即可,这个循环起始变量是一直在递减的。
1 | class Solution { |
复杂度分析
- 时间复杂度:考虑每次循环变量的起始位置是单调不降的,所以起始位置最多移动 m 次,时间复杂度 O(n+m)。
- 空间复杂度:O(1)。