Problem: 1895. 最大的幻方
文章目录
- 整体思路
-
-
- [1. 核心优化:前缀和 (Prefix Sum)](#1. 核心优化:前缀和 (Prefix Sum))
- [2. 算法流程](#2. 算法流程)
-
- 完整代码
- 时空复杂度
-
-
- [1. 时间复杂度: O ( M ⋅ N ⋅ K 2 ) O(M \cdot N \cdot K^2) O(M⋅N⋅K2)](#1. 时间复杂度: O ( M ⋅ N ⋅ K 2 ) O(M \cdot N \cdot K^2) O(M⋅N⋅K2))
- [2. 空间复杂度: O ( M ⋅ N ) O(M \cdot N) O(M⋅N)](#2. 空间复杂度: O ( M ⋅ N ) O(M \cdot N) O(M⋅N))
-
整体思路
1. 核心优化:前缀和 (Prefix Sum)
- 问题 :检查一个 k × k k \times k k×k 区域是否为幻方时,需要反复计算每一行和每一列的和。暴力计算每次都需要 O ( k 2 ) O(k^2) O(k2)。
- 解决方案 :
- 行前缀和
rowSum:rowSum[i][j+1]存储第i行前j个元素的和。这样计算第i行从c到c+k-1的区间和只需 O ( 1 ) O(1) O(1):rowSum[i][c+k] - rowSum[i][c]。 - 列前缀和
colSum:colSum[i+1][j]存储第j列前i个元素的和。计算同理。 - 对角线 :由于对角线方向不固定且数量较少,通常不做前缀和优化(或者需要维护两个方向的对角线前缀和数组,实现较复杂且收益有限),这里选择在验证时暴力遍历,单次耗时 O ( k ) O(k) O(k)。
- 行前缀和
2. 算法流程
-
预处理 :计算并填充
rowSum和colSum数组。 -
枚举:
- 遍历所有可能的左上角坐标
(i, j)。 - 剪枝/递增枚举 :内层循环枚举边长
k。- 代码中的
k从ans开始遍历(ans是当前找到的最大边长)。我们只关心是否能找到比当前ans更大的幻方。如果当前区域能形成边长为k的幻方,就更新ans。 - 不过,代码里写的
for (int k = ans; ...)实际上意味着它会重复检查等于ans的情况,并且如果找到了更大的,它会继续往上找。
- 代码中的
- 遍历所有可能的左上角坐标
-
验证 (
isMagicSquare):- 利用前缀和数组, O ( 1 ) O(1) O(1) 算出每一行、每一列的和并进行比较。
- 暴力计算两条对角线的和。
完整代码
java
class Solution {
// 定义全局前缀和数组,避免传参
int[][] rowSum;
int[][] colSum;
public int largestMagicSquare(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// 1. 初始化并计算前缀和
// 维度多开 1 是为了处理边界情况,且让索引对应更直观
// rowSum[i][j] 表示第 i 行,前 j 个元素的和
rowSum = new int[m][n + 1];
colSum = new int[m + 1][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 当前行前缀和 = 前一个位置的和 + 当前元素值
rowSum[i][j + 1] = rowSum[i][j] + grid[i][j];
// 当前列前缀和 = 上一个位置的和 + 当前元素值
colSum[i + 1][j] = colSum[i][j] + grid[i][j];
}
}
// ans 记录当前找到的最大幻方边长,初始为 1
int ans = 1;
// 2. 遍历所有可能的左上角 (i, j)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 3. 枚举可能的边长 k
// 这里的逻辑是:只尝试 >= 当前 ans 的边长。
// 如果发现了一个更大的 k 满足条件,就更新 ans。
// 这样后续的循环就会尝试更大的边长,实现了剪枝效果。
for (int k = ans; k <= Math.min(m - i, n - j); k++) {
if (isMagicSquare(grid, i, j, k)) {
ans = k;
}
}
}
}
return ans;
}
// 辅助验证函数,使用了前缀和优化
private boolean isMagicSquare(int[][] grid, int r, int c, int k) {
// 计算目标和:直接取第一行的区间和
// O(1) 操作
int target = rowSum[r][c + k] - rowSum[r][c];
// 1. 检查每一行的区间和是否等于 target
// 循环 k 次,每次 O(1),总耗时 O(k)
for (int i = 0; i < k; i++) {
if (rowSum[r + i][c + k] - rowSum[r + i][c] != target) {
return false;
}
}
// 2. 检查每一列的区间和是否等于 target
// 循环 k 次,每次 O(1),总耗时 O(k)
for (int j = 0; j < k; j++) {
if (colSum[r + k][c + j] - colSum[r][c + j] != target) {
return false;
}
}
// 3. 检查主对角线
// 无法利用行列前缀和,需暴力遍历,耗时 O(k)
int diag1 = 0;
for (int i = 0; i < k; i++) {
diag1 += grid[r + i][c + i];
}
if (diag1 != target) {
return false;
}
// 4. 检查副对角线
// 耗时 O(k)
int diag2 = 0;
for (int i = 0; i < k; i++) {
diag2 += grid[r + i][c + k - i - 1];
}
if (diag2 != target) {
return false;
}
return true;
}
}
时空复杂度
假设矩阵行数为 M M M,列数为 N N N,且 K = min ( M , N ) K = \min(M, N) K=min(M,N)。
1. 时间复杂度: O ( M ⋅ N ⋅ K 2 ) O(M \cdot N \cdot K^2) O(M⋅N⋅K2)
- 预处理 :计算
rowSum和colSum需遍历矩阵一次,耗时 O ( M N ) O(MN) O(MN)。 - 枚举与验证 :
- 外层循环遍历所有点
(i, j),约 M N MN MN 次。 - 内层循环枚举
k,最多 K K K 次。 isMagicSquare验证函数:- 行检查: O ( k ) O(k) O(k)。
- 列检查: O ( k ) O(k) O(k)。
- 对角线检查: O ( k ) O(k) O(k)。
- 单次验证总耗时 O ( k ) O(k) O(k)。
- 总计 : ∑ ( M N ⋅ k ) ≈ O ( M ⋅ N ⋅ K 2 ) \sum (MN \cdot k) \approx O(M \cdot N \cdot K^2) ∑(MN⋅k)≈O(M⋅N⋅K2)。
- 外层循环遍历所有点
- 对比 :相比上一版暴力解法的 O ( M ⋅ N ⋅ K 3 ) O(M \cdot N \cdot K^3) O(M⋅N⋅K3),这里消除了一个 K K K 因子,效率显著提升。
2. 空间复杂度: O ( M ⋅ N ) O(M \cdot N) O(M⋅N)
- 计算依据 :
- 使用了两个额外的二维数组
rowSum和colSum,大小均与原矩阵相当。
- 使用了两个额外的二维数组
- 结论 : O ( M ⋅ N ) O(M \cdot N) O(M⋅N)。
- 这是典型的空间换时间策略。