最近在学习动态规划,在牛客上刷题时碰到了这一题。其实最初的想法是暴力和前缀和,但是时间复杂度极高,需要套4层循环。后来去网上搜了一下相关的题解和做法,进而了解到了前缀和+线性动态规划的做法。但是在成功做出这题之前,个人感觉所搜到的博主讲解偏向于代码的编写,对于我这种初入算法的小白来说还是蛮费力气的,所以本节内容我将和大家一起从算法原理到代码一一剖析,争取写出清晰且容易理解的算法思路。
题目链接:最大子矩阵_牛客题霸_牛客网 (nowcoder.com)
首先我们来明确一下题目要求:本题要求的是我们在一个给定的N x N 的“矩阵”中找到 “元素和”最大的“子矩阵”。这个N x N 的矩阵其实就是一个二维数组,我们把它理解为一个矩阵。那么它的子矩阵就是矩阵中的某一片矩阵型的区域,那所要求的“最大子矩阵”的自然是找到在所有子矩阵中,矩阵元素和最大的那个矩阵。
我们先来看示例1来帮助我们理解一下题意:
我们可以发现,除该子矩阵外,在其他任意地方随意圈出一个子矩阵中的元素和均比图示矩阵的元素和小,此时我们就找到了该矩阵的最大子矩阵。
以下是两种解法:
说实话,第一种纯暴力解法就不用看了,数据量稍微大一点,就超时了。
第二种使用二维数组前缀和对原始数组进行了预处理,得到了二维前缀和数组,之后在我们求圈定范围的子矩阵元素和时会带来不小便利,但是4层循环O(n^4)的时间复杂度仍然不可小觑。
那么我们有什么方法能减少时间复杂度呢?
我们先来了解一维数组的前缀和:
一维数组的前缀和是指将数组中从起始位置到当前位置的所有元素相加的结果,即上图中的prefix数组。
我们再来复习一下求一维数组最大子序列所使用到的线性dp算法:
算法原理:
状态表示:dp[i]所表示的是以i结尾的所有子数组中元素的最大和。
状态转移方程:用来更新动态规划数组dp[i]:
-
nums[i - 1]表示当前位置i对应的原始数组 nums 中的元素值。
-
dp[i - 1] + nums[i - 1] 表示从当前位置向前延伸的子序列的和,即以 nums[i - 1] 结尾的子序列和加上当前位置的元素值。这个值表示了当前位置开始的新子序列的和。
-
max(nums[i - 1], dp[i - 1] + nums[i - 1]) 选择了两种情况中的较大值:第一种情况是只包含当前元素 nums[i - 1],即以当前位置 i 结尾的子序列。第二种情况是将当前位置的元素加入到之前的子序列中,即从当前位置开始新的子序列。
-
将较大值更新到 dp[i],表示以 nums[i - 1] 结尾的最大子序列和。
这样,通过动态规划数组 dp 的更新,每个位置 i 都计算出了以该位置结尾的最大子序列和,最终找到整个数组的最大子序列和。
由于最后要输出的是dp数组中的最大值,我们可以使用一个变量来记录以 i 位置为结尾的最大子序列和,这样空间复杂度就从O(N)减少到了O(1)。
其实,对于我们这一题,也可以使用一维数组前缀和与线性dp来解决,我们只需要将二维数组“压缩”为一维数组就好了。那么压缩方式自然是使用前缀和了。
怎么压缩呢?压缩后如何使用前缀和搭配线性dp解题呢?我们接着往下看:
进行完这一步操作后,第 i 行中第 j 列的元素即为:第 j 列从起始位置到当前位置 i 的所有元素相加的结果。
其实通过上面的例子,我们不难发现进行预处理后的前缀和数组,仍然可以表示原数组的元素,更方便的是对于求原数组圈定矩阵元素和,在处理后的数组中仅通过使用末行数组元素减去起始行前一行的数组元素就可以得到所求矩阵的元素和。
通过不同的末行对起始行的减法操作,我们最后可以得到如下序列:
通过前后两张图的解析,其实我们不难发现在最终生成的任意一个子序列中,随机取一段连续的数字即可表示原数组中的子矩阵。即原矩阵中所有的子矩阵均可由生成的子序列得到。
大家一定要好好理解并验证上面的两张图和两段话,当理解通透了才便于进行后续代码的编写。
既然 原矩阵中所有的子矩阵均可由生成的子序列得到,那么最大子矩阵必定是所有子序列数组中的最大连续子序列(注意:是上面通过两层循环得到的10个子序列数组中的最大连续子序列元素和)。
那么我们接下来的目标很简单,就是对10个子序列数组中的每一个进行对最大连续子序列元素和的求解。所以我们需要再加上一层循环,用来遍历子序列数组。并且我们需要一个变量 ans 来记录每个子序列数组中的最大子序列元素和,需要注意的是 ans 在每次进入循环之前要更新为0,防止对后续最大子序列的求解造成影响。同时需要一个变量maxi 来记录所有子序列数组中最大的连续子序列元素和,而这个值就是我们的最大子矩阵元素和。
最终代码:
通常我们会对二维数组多申请一行和一列的空间并初始化为0,是为了dp的状态转移方程在使用时不需要对边界情况进行特殊处理,并且不对dp数组元素的结构造成影响,提高代码的简洁性。
#include <iostream>
#include <vector>
using namespace std;
const int N = 101; // 数组的最大大小
int main() {
int n = 0;
cin >> n;
// 初始化大小为 (n+1) x (n+1) 的二维数组,所有元素为 0
vector<vector<int> > arr(n + 1, vector<int>(n + 1, 0));
// 初始化大小为 (n+1) x (n+1) 的二维向量 dp,所有元素为 0
vector<vector<int> > dp(n + 1, vector<int>(n + 1, 0));
// 输入矩阵的元素,并计算每列的前缀和
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
cin >> arr[i][j];
dp[i][j] = dp[i - 1][j] + arr[i][j]; // 计算每列的前缀和
}
}
int maxi = -128 * 1E4; // 初始化最大值为一个该题中最小的数
int ans = 0;
// 遍历所有的行
for(int i = 0; i < n; i++) // 从第 0 行到第 n-1 行
{
for(int j = i + 1; j <= n; j++) // 从第 1 行到第 n 行
{
ans = 0;
// 遍历每一列,并计算当前行对的最大子序列和
for(int k = 1; k <= n; k++) // 遍历每一列的元素
{
// 计算当前行对的最大子序列和,并更新 ans
ans = max(dp[j][k] - dp[i][k], ans + dp[j][k] - dp[i][k]);
// 更新目前为止找到的最大子序列和(maxi)
maxi = max(ans, maxi);
}
}
}
cout << maxi << endl; // 输出最大子序列和
return 0;
}