LeetCode 1878. 矩阵中最大的三个菱形和
题目描述
给你一个 m x n 的整数矩阵 grid,请你找出矩阵中 所有菱形 的边界上的元素之和,返回其中最大的三个 互不相同 的和,并按 降序 排列。如果不足三个,则返回全部。
注意:
- 菱形由四个顶点定义,边与对角线平行(即旋转45度的正方形)。
- 菱形的边长定义为顶点之间的曼哈顿距离的一半。
- 矩阵元素均为 正整数。
解题思路
本题的关键在于如何高效地计算任意一个菱形的边界和。由于菱形边界是由四条对角线方向的线段组成,我们可以预先计算两个方向的前缀和:
- 主对角线方向 (行和列同时增加):
diag_sum - 反对角线方向 (行增加、列减少):
anti_sum
通过这两个前缀和,我们可以在 O(1) 时间内求出任意一条对角线线段的和。然后枚举所有可能的菱形中心(同时也是菱形的顶点之一)和边长,累加四条边的和,得到整个菱形的边界和。最后维护最大的三个不同值即可。
算法步骤
1. 预处理前缀和
定义两个二维数组:
diag_sum[i+1][j+1]:表示从(0,0)到(i,j)这条主对角线上所有元素的和(即所有(i-t, j-t)的格子)。anti_sum[i+1][j]:表示从右上方向到(i,j)这条反对角线上所有元素的和(即所有(i-t, j+t)的格子)。
递推公式:
cpp
diag_sum[i+1][j+1] = diag_sum[i][j] + grid[i][j];
anti_sum[i+1][j] = anti_sum[i][j+1] + grid[i][j];
2. 查询函数
query_diagonal(x, y, k):返回从(x,y)开始沿主对角线方向连续k个元素的和(包括起点)。实现为diag_sum[x+k][y+k] - diag_sum[x][y]。query_anti_diagonal(x, y, k):返回从(x,y)开始沿反对角线方向连续k个元素的和(包括起点)。实现为anti_sum[x+k][y+1-k] - anti_sum[x][y+1]。
3. 维护最大的三个值
使用三个变量 x, y, z 分别存储第一大、第二大、第三大的值(初始为0)。编写 update 函数,当新值 v 出现时,按降序插入并保证不重复(通过严格比较实现)。
4. 枚举所有菱形
遍历每个格子 (i, j) 作为菱形的 几何中心(同时也是菱形的顶点之一):
-
首先将单个格子视为边长为0的菱形,更新和。
-
计算当前中心所能延伸的最大边长
mx = min(i, m-1-i, j, n-1-j)。 -
对于每个边长
k = 1 .. mx,计算菱形边界和:- 上顶点:
(i-k, j) - 下顶点:
(i+k, j) - 左顶点:
(i, j-k) - 右顶点:
(i, j+k)
四条边的求和(注意不要重复计算顶点):
a:上顶点 → 右顶点(主对角线方向,长度 k)b:左顶点 → 下顶点(主对角线方向,长度 k)c:左顶点 → 上顶点(反对角线方向,长度 k-1)d:右顶点 → 下顶点(反对角线方向,长度 k+1)
最终菱形和 =
a + b + c + d。调用
update记录该和。 - 上顶点:
5. 输出结果
将 {x, y, z} 存入数组,移除末尾可能存在的0(因为题目保证元素为正,若不足三个不同的和,剩下的0会被移除),返回该数组。
代码实现(C++)
cpp
class Solution {
public:
vector<int> getBiggestThree(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
// 两个前缀和数组
vector diag_sum(m + 1, vector<int>(n + 1));
vector anti_sum(m + 1, vector<int>(n + 1));
// 计算前缀和
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
int v = grid[i][j];
diag_sum[i + 1][j + 1] = diag_sum[i][j] + v;
anti_sum[i + 1][j] = anti_sum[i][j + 1] + v;
}
}
// 查询主对角线段和
auto query_diagonal = [&](int x, int y, int k) -> int {
return diag_sum[x + k][y + k] - diag_sum[x][y];
};
// 查询反对角线段和
auto query_anti_diagonal = [&](int x, int y, int k) -> int {
return anti_sum[x + k][y + 1 - k] - anti_sum[x][y + 1];
};
int x = 0, y = 0, z = 0; // 最大的三个不同值
// 插入新值到前三大的辅助函数
auto update = [&](int v) -> void {
if(v > x){
z = y;
y = x;
x = v;
} else if(v < x && v > y){
z = y;
y = v;
} else if(v < y && v > z){
z = v;
}
};
// 枚举所有可能的菱形
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
update(grid[i][j]); // 边长为0的菱形
int mx = min({i, m - 1 - i, j, n - 1 - j}); // 最大可能边长
for(int k = 1; k <= mx; k++){
// 四条边的和
int a = query_diagonal(i - k, j, k); // 上顶点 → 右顶点
int b = query_diagonal(i, j - k, k); // 左顶点 → 下顶点
int c = query_anti_diagonal(i - k + 1, j - 1, k - 1); // 左顶点 → 上顶点
int d = query_anti_diagonal(i, j + k, k + 1); // 右顶点 → 下顶点
update(a + b + c + d);
}
}
}
vector<int> ans = {x, y, z};
while(ans.back() == 0) // 去掉末尾的0(因为元素为正,不足三个时补0)
ans.pop_back();
return ans;
}
};
代码详解
1. 前缀和数组
diag_sum和anti_sum均比原矩阵多一行一列,方便处理边界。- 递推时
diag_sum[i+1][j+1]由左上角diag_sum[i][j]得来;anti_sum[i+1][j]由右上角anti_sum[i][j+1]得来。
2. 查询函数
query_diagonal(x, y, k):起点(x,y),终点(x+k-1, y+k-1)。前缀和diag_sum[x+k][y+k]包含了(0,0)到(x+k-1, y+k-1)的所有对角线和,减去diag_sum[x][y]即得所需段。query_anti_diagonal(x, y, k):起点(x,y),终点(x+k-1, y-k+1)。前缀和anti_sum[x+k][y+1-k]包含了(0,0)到该终点的反对角线和,减去anti_sum[x][y+1]即得。
3. 更新前三大的逻辑
update保证插入的值严格大于或小于已有值才更新,从而保持三个值互不相同。- 初始值设为0,由于矩阵元素为正,任何实际和都大于0,因此0只会在不足三个和时留在末尾。
4. 菱形边界的计算
以中心 (i,j)、边长 k 的菱形为例,其四个顶点坐标如上所述。四条边的选取方式如下:
- 上边 :从上顶点
(i-k, j)到右顶点(i, j+k),沿主对角线方向,包含上顶点但不包含右顶点。长度为k,故用query_diagonal(i-k, j, k)。 - 左边 :从左顶点
(i, j-k)到下顶点(i+k, j),沿主对角线方向,包含左顶点但不包含下顶点。长度为k,用query_diagonal(i, j-k, k)。 - 左边到上边的斜边 :从左顶点到上顶点,沿反对角线方向,但需要避开左顶点和上顶点(它们已被前两条边包含),因此起点为
(i-k+1, j-1),长度为k-1,用query_anti_diagonal(i-k+1, j-1, k-1)。 - 右边到下边的斜边 :从右顶点到下顶点,沿反对角线方向,包含右顶点和下顶点(因为前两条边未包含它们),长度为
k+1,用query_anti_diagonal(i, j+k, k+1)。
四部分相加正好覆盖菱形边界上所有格子,且不重不漏。
5. 结果处理
- 将三个最大值放入数组,若末尾有0(即不足三个不同和),则弹出这些0,返回剩余部分。
复杂度分析
- 时间复杂度 :O(m × n × min(m, n))
枚举每个格子作为中心 O(m×n),每个中心枚举边长 O(min(m,n)),每次计算菱形和为 O(1)。 - 空间复杂度 :O(m × n)
两个前缀和数组各占 O(m×n) 空间。
总结
本题的核心技巧是 对角线方向的前缀和,它将菱形边界和的计算从 O(边长) 降到了 O(1),使得我们可以暴力枚举所有可能的菱形。在实现时注意边界的处理和线段划分的准确性,即可正确求解。