【LeetCode 每日一题】1895. 最大的幻方——(解法二)前缀和优化

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)。
  • 解决方案
    • 行前缀和 rowSumrowSum[i][j+1] 存储第 i 行前 j 个元素的和。这样计算第 i 行从 cc+k-1 的区间和只需 O ( 1 ) O(1) O(1):rowSum[i][c+k] - rowSum[i][c]
    • 列前缀和 colSumcolSum[i+1][j] 存储第 j 列前 i 个元素的和。计算同理。
    • 对角线 :由于对角线方向不固定且数量较少,通常不做前缀和优化(或者需要维护两个方向的对角线前缀和数组,实现较复杂且收益有限),这里选择在验证时暴力遍历,单次耗时 O ( k ) O(k) O(k)。

2. 算法流程

  1. 预处理 :计算并填充 rowSumcolSum 数组。

  2. 枚举

    • 遍历所有可能的左上角坐标 (i, j)
    • 剪枝/递增枚举 :内层循环枚举边长 k
      • 代码中的 kans 开始遍历(ans 是当前找到的最大边长)。我们只关心是否能找到比当前 ans 更大的幻方。如果当前区域能形成边长为 k 的幻方,就更新 ans
      • 不过,代码里写的 for (int k = ans; ...) 实际上意味着它会重复检查等于 ans 的情况,并且如果找到了更大的,它会继续往上找。
  3. 验证 (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)

  • 预处理 :计算 rowSumcolSum 需遍历矩阵一次,耗时 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)

  • 计算依据
    • 使用了两个额外的二维数组 rowSumcolSum,大小均与原矩阵相当。
  • 结论 : O ( M ⋅ N ) O(M \cdot N) O(M⋅N)。
    • 这是典型的空间换时间策略。
相关推荐
a程序小傲2 小时前
中国邮政Java面试被问:边缘计算的数据同步和计算卸载
java·服务器·开发语言·算法·面试·职场和发展·边缘计算
苦藤新鸡2 小时前
21.在有序的二位数组中用O(m+n)的算法找target
算法
小尧嵌入式2 小时前
【Linux开发二】数字反转|除数累加|差分数组|vector插入和访问|小数四舍五入及向上取整|矩阵逆置|基础文件IO|深入文件IO
linux·服务器·开发语言·c++·线性代数·算法·矩阵
试试勇气2 小时前
Linux学习笔记(十二)--用户缓冲区
linux·笔记·学习
@小博的博客2 小时前
Linux 中的编译器 GCC 的编译原理和使用详解
linux·运维·服务器
one____dream2 小时前
【算法】大整数数组连续进位
python·算法
one____dream2 小时前
【算法】合并两个有序链表
数据结构·python·算法·链表
大江东去浪淘尽千古风流人物2 小时前
【Project Aria】Meta新一代的AR眼镜及其数据集
人工智能·嵌入式硬件·算法·性能优化·ar·dsp开发
电饭叔2 小时前
has_solution = False 是什么 费马大定律代码化和定理《计算机科学中的数学》外扩学习3
学习·算法