LeetCode 1895. 最大的幻方

1895. 最大的幻方

一个 k x k 的 幻方 指的是一个 k x k 填满整数的方格阵,且每一行、每一列以及两条对角线的和 全部相等 。幻方中的整数 不需要互不相同 。显然,每个 1 x 1 的方格都是一个幻方。

给你一个 m x n 的整数矩阵 grid ,请你返回矩阵中 最大幻方 的 尺寸 (即边长 k)。

示例 1:

输入:grid = [[7,1,4,5,6],[2,5,1,6,4],[1,5,4,3,2],[1,2,7,3,4]]
输出:3
解释:最大幻方尺寸为 3 。
每一行,每一列以及两条对角线的和都等于 12 。
- 每一行的和:5+1+6 = 5+4+3 = 2+7+3 = 12
- 每一列的和:5+5+2 = 1+4+7 = 6+3+3 = 12
- 对角线的和:5+4+3 = 6+4+2 = 12

示例 2:

输入:grid = [[5,1,3,1],[9,3,3,1],[1,3,3,8]]
输出:2

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 50
  • 1 <= grid[i][j] <= 10^6

提示 1

Check all squares in the matrix and find the largest one.

 

解法:枚举正方形 + 前缀和

思路与算法

我们只需要按照从大到小的顺序枚举正方形的边长 edge,再枚举给定的矩阵 grid 中所有边长为 edge 的正方形,并依次判断它们是否满足幻方的要求即可。

这样做的时间复杂度是多少呢?我们记 x=min(m,n),那么 edge 的范围为 [1,x],边长为 edge 的正方形有 (m−edge+1)(n−edge+1)=O(mn) 个,对于每个正方形,我们需要计算其每一行、列和对角线的和,一共有 edge 行 edge 列以及 2 条对角线,那么计算这些和的总时间复杂度为 ((2⋅edge+2)⋅edge)=O(x^2)。将所有项相乘,总时间复杂度即为 O(x^3*m*n)。

我们无法 100% 保证 O(x^3*mn) 的算法可以在规定时间内通过所有的测试数据:虽然它的时间复杂度看起来很大,但是常数实际上很小,如果代码写得比较优秀,还是有通过的机会的。

但做一些不复杂的优化也是很有必要的。一个可行的优化点是:我们可以预处理出矩阵 grid 每一行以及每一列的前缀和,这样对于计算和的部分:

每一行只需要 O(1) 的时间即可求和,所有的 edge 行的总时间复杂度为 O(x);

每一列只需要 O(1) 的时间即可求和,所有的 edge 列的总时间复杂度为 O(x);

我们没有预处理对角线的前缀和,这是因为对角线只有 2 条,即使我们直接计算求和,时间复杂度也为 O(2⋅x)=O(x)。

因此,求和部分的总时间复杂度从 O(x^2) 降低为 O(x),总时间复杂度降低为 O(x^2 * mn),对于本题 m,n≤50 的范围,该时间复杂度是合理的。

前缀和的具体实现过程可以参考下面的代码。

优化

我们只需要在 [2,x] 的范围内从大到小遍历 edge 即可,这是因为边长为 1 的正方形一定是一个幻方。

Java版:

class Solution {
    public int largestMagicSquare(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] rowsum = new int[m][n];
        int[][] colsum = new int[m][n];
        
        // 每一行的前缀和
        for (int i = 0; i < m; i++) {
            rowsum[i][0] = grid[i][0];
            for (int j = 1; j < n; j++) {
                rowsum[i][j] = rowsum[i][j - 1] + grid[i][j];
            }
        }

        // 每一列的前缀和
        for (int j = 0; j < n; j++) {
            colsum[0][j] = grid[0][j];
            for (int i = 1; i < m; i++) {
                colsum[i][j] = colsum[i - 1][j] + grid[i][j];
            }
        }

        // 从大到小枚举边长 edge
        for (int edge = Math.min(m, n); edge >= 2; edge--) {
            // 枚举正方形的左上角位置 (i,j)
            for (int i = 0; i <= m - edge; i++) {
                for (int j = 0; j <= n - edge; j++) {
                    // 先计算第一行的和作为样本
                    int target = rowsum[i][j + edge - 1] - (j > 0 ? rowsum[i][j - 1] : 0);
                    boolean check = true;

                    // 枚举每一行并用前缀和直接求和
                    // 由于我们已经拿第一行作为样本了,这里可以跳过第一行
                    for (int ii = i + 1; ii < i + edge; ii++) {
                        if (rowsum[ii][j + edge - 1] - (j > 0 ? rowsum[ii][j - 1] : 0) != target) {
                            check = false;
                            break;
                        }
                    }
                    if (!check) {
                        continue;
                    }

                    // 枚举每一列并用前缀和直接求和
                    for (int jj = j; jj < j + edge; jj++) {
                        if (colsum[i + edge - 1][jj] - (i > 0 ? colsum[i - 1][jj] : 0) != target) {
                            check = false;
                            break;
                        }
                    }
                    if (!check) {
                        continue;
                    }

                    // d1 和 d2 分别表示两条对角线的和
                    int d1 = 0, d2 = 0;
                    // 两条对角线直接遍历求和
                    for (int k = 0; k < edge; k++) {
                        d1 += grid[i + k][j + k];
                        d2 += grid[i + k][j + edge -1 - k];
                    }
                    if (d1 == target && d2 == target) {
                        return edge;
                    }
                }
            }
        }
        return 1;
    }
}

Python3版:

class Solution:
    def largestMagicSquare(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        rowsum = [[0] * n for _ in range(m)]
        colsum = [[0] * n for _ in range(m)]

        # 每一行的前缀和
        for i in range(m):
            rowsum[i][0] = grid[i][0]
            for j in range(1, n):
                rowsum[i][j] = rowsum[i][j - 1] + grid[i][j]

        # 每一列的前缀和
        for j in range(n):
            colsum[0][j] = grid[0][j]
            for i in range(1, m):
                colsum[i][j] = colsum[i - 1][j] + grid[i][j]
        
        # 从大到小枚举边长 edge
        for edge in range(min(m, n), 1, -1):
            # 枚举正方形的左上角位置 (i,j)
            for i in range(m - edge + 1):
                for j in range(n - edge + 1):
                    # 先计算第一行的和作为样本
                    target = rowsum[i][j + edge - 1] - (rowsum[i][j - 1] if j > 0 else 0)
                    check = True

                    # 枚举每一行并用前缀和直接求和
                    for ii in range(i + 1, i + edge):
                        if rowsum[ii][j + edge - 1] - (rowsum[ii][j - 1] if j > 0 else 0) != target:
                            check = False
                            break
                    if not check:
                        continue
                    
                    # 枚举每一列并用前缀和直接求和
                    for jj in range(j, j + edge):
                        if colsum[i + edge - 1][jj] - (colsum[i - 1][jj] if i > 0 else 0) != target:
                            check = False 
                            break
                    if not check:
                        continue 
                    
                    # d1 和 d2 分别表示两条对角线的和
                    d1, d2 = 0, 0
                    # 两条对角线直接遍历求和
                    for k in range(edge):
                        d1 += grid[i + k][j + k]
                        d2 += grid[i + k][j + edge - 1 - k]
                    if d1 == target and d2 == target:
                        return edge 
        return 1 
                    
                    

复杂度分析

  • 时间复杂度:O(mn * min(m,n)^2)。

  • 空间复杂度:O(mn),即为存储前缀和需要的空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值