一、题意

输出n,然后输入一个n*n(n<=100)的数字矩形,要求你找出这个矩形中数字和最大的子矩形。
输出这个子矩形的数字和。

二、解析

先来回顾一下一维的最大连续子数列和问题:在一个长度为n的数列a[1...n]中,找到数字和最大的子序列。
在一维情形我们有4种方法:

  • 方法一:暴力枚举,枚举子序列首尾,然后求和,复杂度O(n^3),绝对不会用的方法。
  • 方法二:通过维护sum[i]=sum(a[1..i])数组,将时间复杂度降到O(n^2),但依然不完美。
  • 方法三:动态规划。dp[i]=max(a[i], dp[i-1]+a[i]),其中dp[i]表示以第i位数字为结尾的子序列的最大连续和,它要么就是一个最大子序列的开头,要么就接着作为dp[i-1]的延续。最终答案就是max(dp[i]),时间复杂度仅为O(n)。完美。
  • 方法四:由于最大连续和一定是sum[i]-sum[j],其中sum[j]是sum[0]...sum[i-1]中最小的那个,因此只需要边求sum边维护最小值即可。用时也是O(n)。同样完美。

个人比较喜欢用动态规划的方法。

回到这道题,上述的方法就能给我们一些思路,最终通过尝试发现,方法二能够比较完美的转化为二维的方法,但同时还要结合动态规划的思想:dp[i][j]表示以a[i][j]为右下角的子矩形中的最小数字和,sum[i][j]表示以a[i][j]为右下角的最大矩形的数字和;则dp[i][j]=max(dp[i][j], sum[i][j] - sum[li][j] - sum[i][lj] + sum[li][lj]),即枚举子矩形左上角的位置。

这样时间复杂度为O(n^4),考虑到n<=100,因此不会超时。

三、代码

#include <iostream>
#include <cmath>
using namespace std;
const int maxn = 100 + 5, INF = 1 << 30;
int n, a[maxn][maxn], sum[maxn][maxn], dp[maxn][maxn];

int main() {
    cin >> n;
    for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) cin >> a[i][j];
    int ans = -INF;
    for(int i = 1; i <= n; i ++) {
        for(int j = 1; j <= n; j ++) {
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
            dp[i][j] = -INF;

            for(int li = 0; li < i; li ++) {
                for(int lj = 0; lj < j; lj ++) {
                    dp[i][j] = max(dp[i][j], sum[i][j] - sum[li][j] - sum[i][lj] + sum[li][lj]);
                    ans = max(ans, dp[i][j]);
                }
            }
        }
    }
    cout << ans << endl;
}

四、归纳

  • 这里的矩形动态规划用到了一个小技巧:从a[1][1]开始填充数字,即将第0行和第0列留空为0.这样的好处是进行dp时不需要特意初始化这两行了,可以直接从a[1][1]开始dp,从而减少代码量。