题目描述与示例

题目描述

给定一个二维整数矩阵,要在这个矩阵中。选出一个子矩阵,使得这个子矩阵内所有的数字和尽量大

我们把这个子矩阵称为 “和最大子矩阵”,子矩阵的选取原则,是原矩阵中一段相互连续的矩形区域

输入描述

输入的第一行包含两个整数N,M

(1 <= N, M <= 10)

表示一个 N 行 M 列的矩阵

下面有N行 每行有M个整数

同一行中每两个数字之间有一个空格

最后一个数字后面没有空格

所有的数字得在-1000 ~ 1000之间

输出描述

输出一行,一个数字。表示选出的“和最大子矩阵”内所有数字的和

示例

输入

3 4
-3 5 -1 5
 2 4 -2 4
-1 3 -1 3

输出

20

说明

一个3*4的矩阵中 后面3列的和为20,和最大

解题思路

如何表示一个子矩阵

一个子矩阵可以由四个参数决定,分别为上底、下底、左宽、右宽,分别用变量abcd表示的话,如下图中灰色区域为通过四个参数所确定的矩形。

暂时无法在飞书文档外展示此内容

如果我们想要枚举所有子矩阵,只需要分别枚举abcd,写一个4层嵌套的for循环即可。

for a in range(n):
     for b in range(a, n):
         for c in range(m):
             for d in range(c, m):
                pass

暴力解法

暴力解法是很容易想到的,我们只需要枚举所有的子矩阵,然后对每一个子矩阵进行矩阵内所有元素求和即可。其核心代码为

for a in range(n):
     for b in range(a, n):
         for c in range(m):
             for d in range(c, m):
                 submat_sum = 0
                 for i in range(a, b+1):
                     for j in range(c, d+1):
                         submat_sum += mat[i][j]
                 ans = max(submat_sum, ans)

注意到会出现6for循环嵌套,时间复杂度为O(n^3m^3)。由于数据范围为(1 <= n, m <= 10),故取最大值时复杂度约为O(10^6),无法通过全部用例,故应该思考如何优化。

二维前缀和优化

注意,该方法和LeetCode 304、二维区域和检索 – 矩阵不可变 是类似的。

注意到每一个子矩阵的计算都可以用以下方式进行拆解。

暂时无法在飞书文档外展示此内容

拆解后的四个区域具有一个共同的特点:它们的上底均为上边界、左宽均为左边界

因此需要考虑类似一维前缀和的方法,将所有的上底为上边界、左宽为左边界(即a = 0c = 0)的子矩阵的和提前记录在二维前缀和矩阵pre_sum_mat中。

pre_sum_mat是一个大小为(n+1)*(m+1)的矩阵,pre_sum_mat[i][j]表示以第0行、第0列为开头(取得到的开区间),第i行、第j列为结尾(取不到的闭区间)的子矩阵的和。

暂时无法在飞书文档外展示此内容

上述的四个区域的和,就可以分别使用pre_sum_mat[b+1][d+1]pre_sum_mat[b+1][c]pre_sum_mat[a][d+1]pre_sum_mat[a][c]来表示了。

这里对开/闭区间的理解是非常重要的,如果想不清楚的话,后面的代码很容易出错。如果把子矩阵用一种类似切片的方法表示(并不严谨的写法)为mat[a:b+1][c:d+1]。那么上述的分析过程可以写为

sum(mat[a:b+1][c:d+1])
= sum(mat[:b+1][:d+1]) + sum(mat[:a][:c]) - sum(mat[:b+1][:c]) - sum(mat[:a][:d+1])

= pre_sum_mat[b+1][d+1] + pre_sum_mat[a][c] - pre_sum_mat[b+1][c] - pre_sum_mat[a][d+1]

那么,在原矩阵mat中,分别以abcd为上底、下底、左宽、右宽的子矩阵的和,就可以记为

submat_sum = (pre_sum_mat[b+1][d+1] + pre_sum_mat[a][c] -
              pre_sum_mat[b+1][c] - pre_sum_mat[a][d+1])

上述计算的时间复杂度为O(1),因此这种做法规避了暴力解对子矩阵求和时出现的反复计算,降低了最内层求和时时间复杂度。如果把外部的循环体加上,代码为

for a in range(n):
    for b in range(a, n):
        for c in range(m):
            for d in range(c, m):
                submat_sum = pre_sum_mat[b+1][d+1] + pre_sum_mat[a][c] - \
                             pre_sum_mat[b+1][c] - pre_sum_mat[a][d+1]
                ans = max(submat_sum, ans)

如果不想让最内层的索引出现+1,则可以修改for循环的范围,代码变为

for a in range(n):
    for b in range(a+1, n+1):
        for c in range(m):
            for d in range(c+1, m+1):
                submat_sum = pre_sum_mat[b][d] + pre_sum_mat[a][c] - \
                             pre_sum_mat[b][c] - pre_sum_mat[a][d]
                ans = max(submat_sum, ans)

上述过程的时间复杂度为O(n^2m^2)。当nm取最大值时复杂度约为O(10^4),可以通过全部用例。

二维前缀和矩阵的构建

二维前缀和矩阵pre_sum_mat的构建也要用到类似上述的拆分过程,其核心代码如下

pre_sum_mat = [[0] * (m+1) for _ in range(n+1)]
for i in range(1, n+1):
    for j in range(1, m+1):
        pre_sum_mat[i][j] = pre_sum_mat[i-1][j] + pre_sum_mat[i][j-1] - \
                            pre_sum_mat[i-1][j-1] + mat[i-1][j-1]

要特别注意二维前缀和pre_sum_mat的大小,在两个维度上均比原矩阵矩阵mat1

该过程的时间复杂度为O(nm)

代码

解法一:二维前缀和

Python

# 题目:2023B-最大子矩阵和
# 分值:200
# 作者:闭着眼睛学数理化
# 算法:二维前缀和
# 代码有看不懂的地方请直接在群上提问


from math import inf


n, m = map(int, input().split())
mat = list()
for _ in range(n):
    row = list(map(int, input().split()))
    mat.append(row)

# 构建二维前缀和数组
pre_sum_mat = [[0] * (m+1) for _ in range(n+1)]
for i in range(1, n+1):
    for j in range(1, m+1):
        pre_sum_mat[i][j] = pre_sum_mat[i-1][j] + pre_sum_mat[i][j-1] - \
                            pre_sum_mat[i-1][j-1] + mat[i-1][j-1]

# 初始化答案为负无穷小
ans = -inf

# 枚举上底a
for a in range(n):
    # 枚举下底b
    for b in range(a, n):
        # 枚举左宽c
        for c in range(m):
            # 枚举右宽d
            for d in range(c, m):
                # 此时四个参数能够表示一个子矩阵
                # 根据式子计算子矩阵和,更新ans
                submat_sum = pre_sum_mat[b+1][d+1] + pre_sum_mat[a][c] - \
                             pre_sum_mat[b+1][c] - pre_sum_mat[a][d+1]
                ans = max(submat_sum, ans)

print(ans)

Java

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        int[][] mat = new int[n][m];

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                mat[i][j] = scanner.nextInt();
            }
        }

        int[][] preSumMat = new int[n + 1][m + 1];

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                preSumMat[i][j] = preSumMat[i - 1][j] + preSumMat[i][j - 1] - preSumMat[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }

        int ans = Integer.MIN_VALUE;

        for (int a = 0; a < n; a++) {
            for (int b = a; b < n; b++) {
                for (int c = 0; c < m; c++) {
                    for (int d = c; d < m; d++) {
                        int submatSum = preSumMat[b + 1][d + 1] + preSumMat[a][c] - preSumMat[b + 1][c] - preSumMat[a][d + 1];
                        ans = Math.max(submatSum, ans);
                    }
                }
            }
        }

        System.out.println(ans);
    }
}

C++

“`C++
#include <iostream>
#include <vector>
#include <limits>

using namespace std;

int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> mat(n, vector<int>(m));

<pre><code>for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> mat[i][j];
}
}

vector<vector<int>> pre_sum_mat(n + 1, vector<int>(m + 1, 0));

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
pre_sum_mat[i][j] = pre_sum_mat[i – 1][j] + pre_sum_mat[i][j – 1] – pre_sum_mat[i – 1][j – 1] + mat[i – 1][j – 1];
}
}

int ans = numeric_limits<int>::min();

for (int a = 0; a < n; a++) {
for (int b = a; b < n; b++) {
for (int c = 0; c < m; c++) {
for (int d = c; d < m; d++) {
int submat_sum = pre_sum_mat[b + 1][d + 1] + pre_sum_mat[a][c] – pre_sum_mat[b + 1][c] – pre_sum_mat[a][d + 1];
ans = max(submat_sum, ans);
}
}
}
}

cout << ans << endl;
return 0;
</code></pre>

}

<pre><code class="">### 时空复杂度

时间复杂度:O(n^2m^2)

空间复杂度:O(nm)。二维前缀和矩阵所占空间。

## 解法二:暴力解法(不推荐)

### Python

“`Python
# 题目:2023B-最大子矩阵和
# 分值:200
# 作者:闭着眼睛学数理化
# 算法:暴力解
# 代码有看不懂的地方请直接在群上提问

from math import inf

n, m = map(int, input().split())
mat = list()
for _ in range(n):
row = list(map(int, input().split()))
mat.append(row)

# 初始化答案为负无穷小
ans = -inf

for a in range(n):
for b in range(a, n):
for c in range(m):
for d in range(c, m):
submat_sum = 0
for i in range(a, b+1):
for j in range(c, d+1):
submat_sum += mat[i][j]
ans = max(submat_sum, ans)

print(ans)

Java

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        int[][] mat = new int[n][m];

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                mat[i][j] = scanner.nextInt();
            }
        }

        int ans = Integer.MIN_VALUE;

        for (int a = 0; a < n; a++) {
            for (int b = a; b < n; b++) {
                for (int c = 0; c < m; c++) {
                    for (int d = c; d < m; d++) {
                        int submatSum = 0;
                        for (int i = a; i <= b; i++) {
                            for (int j = c; j <= d; j++) {
                                submatSum += mat[i][j];
                            }
                        }
                        ans = Math.max(submatSum, ans);
                    }
                }
            }
        }

        System.out.println(ans);
    }
}

C++

“`C++
#include <iostream>
#include <vector>
#include <climits>

using namespace std;

int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> mat(n, vector<int>(m));

<pre><code>for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> mat[i][j];
}
}

int ans = INT_MIN;

for (int a = 0; a < n; a++) {
for (int b = a; b < n; b++) {
for (int c = 0; c < m; c++) {
for (int d = c; d < m; d++) {
int submatSum = 0;
for (int i = a; i <= b; i++) {
for (int j = c; j <= d; j++) {
submatSum += mat[i][j];
}
}
ans = max(submatSum, ans);
}
}
}
}

cout << ans << endl;

return 0;
</code></pre>

}

“`

时空复杂度

时间复杂度:O(n^3m^3)

空间复杂度:O(1)

说明

华为OD机试有三道题⽬,第⼀道和第⼆道属于简单或中等题,分值为 100 分,第三道为中等或困难题,分值为 200分,总分为 400 分。

机试分数越⾼评级越⾼,⼯资也就越⾼。

关于华为 OD 机试更加详细的介绍可以查看这篇⽂章:华为OD机考须知

关于机考题目汇总可以看这篇文章:华为OD机试真题 2023 A+B+C+D卷 + 2024新卷(Python&Java&C++)⽬录汇总(每⽇更新)