题单 · 习题解析

洛谷 P1719 最大加权矩形

★★★ 「ans 初始化成 0」样例挡不住;而对拍抓不抓得到取决于 n 造多大 —— n ≤ 6 抓 23 次,n = 30..120 抓 0 次

原题:洛谷 P1719出自 第 6 章 前缀和与差分 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1719,日期见页头。两边不一致时信原站。

题目描述

为了更好地备战 NOIP 2013,电脑组的几个女孩子 LYQ、ZSC、ZHQ 认为:我们不光需要机房, 我们还需要运动。于是她们决定找校长申请一块电脑组的课余运动场地。 听说她们都是电脑组的高手,校长没有马上答应她们,而是先给她们出了一道数学题, 并告诉她们,她们能获得的运动场地的面积就是她们能找到的这个最大的数字。

校长给她们一个大小为 n × n 的矩阵,矩阵中的每一个元素都有一个整数权值, 要她们求出该矩阵中的最大加权矩形(即从中找一大小不限的矩形, 使其中包含的所有元素的权值和最大)中所有元素的权值和, 且矩阵中每个元素的权值均在区间 [-127, 127] 内。

几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH、TZY 小朋友帮忙计算, 但是遗憾的是,他们的答案都不一样。涉及土地的事情我们可不能含糊, 你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?

输入格式:第一行包含一个正整数 n。接下来 n 行每行包含 n 个整数,表示给定的矩阵。

输出格式:输出一行一个整数,表示该矩阵的最大加权矩形中所有元素的权值和。

数据范围:对于 100% 的数据,1 ≤ n ≤ 120

输入输出样例

输入

4
0 -2 -7 0
 9 2 -6 2
-4 1 -4  1 
-1 8  0 -2

输出

15

样例解释:该矩阵中的最大加权矩形为

 9   2
-4   1
-1   8

它们的和为 15上面那段输出是仓库里的 p1719.cpp 真跑出来的。

1先把题读清楚:「大小不限」不等于「可以是空的」

矩形至少要有一个格子。所以矩阵全是负数时,答案是最大的那个负数

输入

3
-1 -2 -3
-4 -5 -6
-7 -8 -9

输出

-1

一个全负的 3×3。答案是 -1(右上角那个 1×1 的矩形)—— 不是 0,因为 0 不是任何矩形的和。这一条第 ⑤ 步会变成这一页最贵的一课。

2第 ① 版:四重循环枚举边界 + 逐格加(对,但 O(n⁶))

题意的逐字翻译:上下左右四个边界各枚举一遍,再把矩形里的数加起来。

p1719Brute.cpp第 ① 版:O(n⁶)
4×4 秒出。⚠ n = 120 时它要 50 秒 —— 见第 ④ 步那张表。
// P1719 的第 ① 版:枚举四个边界,然后老老实实把里面的数加一遍
//
// 题意的逐字翻译:「找一个大小不限的矩形,使里面所有元素的和最大」
// ⇒ 上边界 i1、下边界 i2、左边界 j1、右边界 j2 各枚举一遍(四重循环),
// 再用两重循环把这块矩形里的数加起来。⇒ O(n⁶)。
//
// ⚠ n = 120 时这是 **7.6 × 10¹¹** 次加法(正文第 ④ 步有那张表),跑不完。
// ★ 但它仍然要写:它是这一页的标准答案,也是对拍里「一定对」的那一栏。
//
// ⚠⚠ ans 的初值是 **最小的那个数**,不是 0 —— 见 p1719Zero.cpp 那份反例。
#include <bits/stdc++.h>
using namespace std;
int n;
int a[125][125];
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) cin >> a[i][j];
int ans = a[1][1]; // ★ 不是 0:矩形不能为空
for (int i1 = 1; i1 <= n; i1++)
for (int i2 = i1; i2 <= n; i2++)
for (int j1 = 1; j1 <= n; j1++)
for (int j2 = j1; j2 <= n; j2++) {
int sum = 0;
for (int i = i1; i <= i2; i++)
for (int j = j1; j <= j2; j++) sum += a[i][j];
ans = max(ans, sum);
}
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3第 ② 版:二维前缀和 —— ★ 它就已经能 AC 了

第 6 章第 13 步那两个式子原样搬过来, 「把矩形里的数加一遍」那两重循环就没了:

s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];          // 预处理
sum     = s[i2][j2] - s[i1-1][j2] - s[i2][j1-1] + s[i1-1][j1-1];  // 查询,O(1)
p1719.cpp第 ② 版:二维前缀和(能 AC)
// P1719 最大加权矩形 —— 能 AC 的那一版:二维前缀和 + 枚举四个边界
//
// 第 6 章第 13 步那两个式子,原样搬过来:
// 预处理 s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
// 查询 (i1,j1)~(i2,j2) 的和 = s[i2][j2] - s[i1-1][j2] - s[i2][j1-1] + s[i1-1][j1-1]
//
// 于是「把里面的数加一遍」那两重循环没了:O(n⁶) → O(n⁴)。
// n = 120 ⇒ 枚举 (i1,i2) 和 (j1,j2) 各 7260 种,一共 5271 万次查询,每次四则运算四步。
//
// ⚠ ans 的初值必须是**某个真实存在的数**(这里用 a[1][1]),不能是 0 ——
// 矩阵可以全是负数,那时答案是「最大的那个负数」,而 0 不是任何矩形的和。
#include <bits/stdc++.h>
using namespace std;
int n;
int s[125][125]; // s[i][j]:左上角 (1,1) 到 (i,j) 的和
int main() {
cin >> n;
int first = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int x;
cin >> x;
if (i == 1 && j == 1) first = x;
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;
}
int ans = first; // ★ 初值是一个真的存在的矩形(1×1 那个)
for (int i1 = 1; i1 <= n; i1++)
for (int i2 = i1; i2 <= n; i2++)
for (int j1 = 1; j1 <= n; j1++)
for (int j2 = j1; j2 <= n; j2++) {
int sum = s[i2][j2] - s[i1 - 1][j2] - s[i2][j1 - 1] + s[i1 - 1][j1 - 1];
ans = max(ans, sum);
}
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

O(n⁶)O(n⁴)n = 1205270 万个矩形,每个四步四则运算 —— 本机 0.02 秒

4★ 换尺子 + 秒表

p1719Count.cpp三种写法各碰了多少下
数的是同一个动作:为了知道某个矩形的和,一共动了多少下。
// 换一把尺子:三种写法各碰了多少下
//
// 用法:./p1719Count <n> 人话版
// ./p1719Count <n> csv 只打 `键,值`,给 check:viz 用
//
// 数的是同一个动作:「为了知道某个矩形的和,一共动了多少下」——
// ① 暴力 :把矩形里每个格子加一遍 ⇒ 所有矩形的面积之和
// ② 二维前缀和:每个矩形四步四则运算 ⇒ 矩形个数 × 4
// ③ 枚举上下行 + 最大子段和:每对 (i1,i2) 扫一行 ⇒ 矩形对数 × n
//
// ★ 矩形个数 = C(n+1,2)²(上下边界一对、左右边界一对)。
// 所有矩形的面积之和有闭式:Σ 高 × Σ 宽 = [Σ_{h=1..n} h(n-h+1)]²。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
long long n = (argc > 1) ? atoll(argv[1]) : 120;
bool csv = (argc > 2 && string(argv[2]) == "csv");
long long pairs = n * (n + 1) / 2; // 一维上「选一段」的方案数
long long rects = pairs * pairs;
long long spanSum = 0; // Σ_{h=1..n} h × (n-h+1)
for (long long h = 1; h <= n; h++) spanSum += h * (n - h + 1);
long long brute = spanSum * spanSum; // 所有矩形的面积之和
long long fast = rects * 4;
long long kadane = pairs * n;
if (csv) {
printf("n,%lld\nrects,%lld\nbrute,%lld\nfast,%lld\nkadane,%lld\n",
n, rects, brute, fast, kadane);
} else {
printf("n = %lld:一共 %lld 个矩形\n", n, rects);
printf("① 暴力(逐格加) %lld 下\n", brute);
printf("② 二维前缀和(每个 4 步)%lld 下\n", fast);
printf("③ 上下行 + 最大子段和 %lld 下\n", kadane);
printf("⇒ ① / ② = %lld 倍,② / ③ = %lld 倍\n", brute / fast, fast / kadane);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-26,独占):

n ① 逐格加 O(n⁶) ② 二维前缀和 O(n⁴) ③ 上下行 + 最大子段和 O(n³)
40 0.08 秒 0.00 秒 0.00 秒
60 0.83 秒 0.00 秒 0.00 秒
80 4.58 秒 0.00 秒 0.00 秒
120 50.41 秒 0.02 秒 0.00 秒
碰的下数(n = 120 87 166 657 600 210 830 400 871 200

★ 注意 ① 那一列:n 从 80 到 120 只大了 1.5 倍,时间涨了 11 倍1.5⁶ = 11.4)—— 指数是 6 的时候,「再大一点点」是没有的。

5★★★ 那个初始化:样例挡不住,而对拍抓不抓得到取决于生成器把 n 造多大

p1719Zero.cpp错法:ans 初始化成 0
它和正解只差一个字符(ans 的初值)。这里喂的是全负矩阵:正解 -1,它 0。
// P1719 的错法演示:ans 初始化成 0
//
// 除了这一处,它和 p1719.cpp 一模一样。
//
// ⚠ 「大小不限」不等于「可以是空的」:矩形至少要有一个格子。
// 矩阵全是负数时,答案是**最大的那个负数**,而这一版会输出 0 —— 0 不是任何矩形的和。
//
// ★★ 样例挡不住它(样例答案 15 > 0)。
// ★★★ 而对拍抓不抓得到,取决于生成器把 n 造多大(实测,正文第 ⑤ 步):
// 随机 n <= 6 —— 300 轮抓 **23** 次(n = 1、2 时「全负」随手就有)
// 随机 n = 30..120 —— 300 轮抓 **0** 次(0.5^(n²) 已经是天文数字分之一)
// 专造全负 —— 300 轮抓 **300** 次
// ⇒ 小数据不是「凑合」,**小本身就是一种覆盖能力**;而要钉死它还得单开一个档位。
#include <bits/stdc++.h>
using namespace std;
int n;
int s[125][125];
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int x;
cin >> x;
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;
}
int ans = 0; // ⚠ 就是这里
for (int i1 = 1; i1 <= n; i1++)
for (int i2 = i1; i2 <= n; i2++)
for (int j1 = 1; j1 <= n; j1++)
for (int j2 = j1; j2 <= n; j2++)
ans = max(ans, s[i2][j2] - s[i1 - 1][j2] - s[i2][j1 - 1] + s[i1 - 1][j1 - 1]);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1719Gen.cpp生成器:三个档位
参数是「种子 档位」。档位 1 每格都是负的。
// 数据生成器(P1719 对拍用):`./p1719Gen <seed> [level]`
//
// level 0(默认)顺手写法:n <= 6,每格随机 -127..127
// level 1 ★ **全负**:每格随机 -127..-1 —— 专门给「ans 初始化成 0」那版准备的
// level 2 照着题面规模造:n = 30..120,每格随机 -127..127
//
// ★★★ 三个档位抓「ans 初始化成 0」的成绩是 23 / 300、300 / 300、**0 / 300**(正文第 ⑤ 步),
// 而最值得看的是**档位 0 和档位 2 的对比**:
// 同一种写法(随机填 -127..127),**把 n 造小反而抓得到,照着题面规模造就永远抓不到**。
// 道理很直白:这个 bug 要「整个矩阵全是负数」,概率是 0.5^(n²) ——
// n = 1、2 时随手就有,n = 120 时是 0.5^14400。
// ⇒ 对拍的小数据不是「凑合」,**小本身就是一种覆盖能力**;
// 而真要钉死这个 bug,还得单开档位 1。
#include <bits/stdc++.h>
using namespace std;
static mt19937 rng;
static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) {
unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1;
int level = (argc > 2) ? atoi(argv[2]) : 0;
rng.seed(seed);
int n = (level == 2) ? ri(30, 120) : ri(1, 6);
printf("%d\n", n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
printf("%d%c", level == 1 ? ri(-127, -1) : ri(-127, 127), j == n - 1 ? '\n' : ' ');
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(check:viz 每次都真跑一遍,每档 300 轮):

生成器 造出来的是什么 「ans = 0」那版被抓
档位 0:随机 n ≤ 6 顺手写法 23 / 300
档位 2:随机 n = 30..120 照着题面规模造 0 / 300
档位 1:全负 n ≤ 6 专门造那个局面 300 / 300
★★★ 把 n 造小反而抓得到 —— 小数据不是「凑合」

这个 bug 要的局面是「整个矩阵全是负数」,概率是 0.5^(n²)

n = 1   ->  1/2          <- 随手就有
n = 2   ->  1/16
n = 6   ->  0.5^36  ≈ 一百四十亿分之一
n = 120 ->  0.5^14400    <- 天文数字分之一

⇒ 档位 0 能抓到 23 次,全靠它造的 n 可以小到 1、2; 而「照着题面规模造大数据」那一档(n = 30..120一次都抓不到

★ 这和很多人的直觉相反:对拍要造小数据,不是因为跑得快,是因为小数据里 「极端局面」的密度高得多。 大数据留给秒表,小数据留给正确性。 ⚠ 但也别指望它:真要钉死这个 bug,还是得单开一个「全负」的档位 —— 第 5 章 P1042 那条(把每个错版的触发条件写下来,逐条问生成器造不造得出)在这儿又用了一次。

6第 ③ 版:把二维退回一维(这道题不需要它)

固定上边界 i1 和下边界 i2 之后,每一列都塌成一个数, 于是问题变成「在这一排数里选一段连续的,使和最大」—— 最大子段和

cur = (cur < 0) ? v : cur + v;    // 前面那截是负的,就别带着它走
ans = max(ans, cur);
p1719Kadane.cpp第 ③ 版:O(n³)

这道题用不着它(第 ② 版 0.02 秒)。放在这儿是因为 「二维问题固定两行、退回一维」是个到处都用得上的动作, 而且它把最大子段和(第 2122 章那一类 DP 的入门题)接上了。

7四个版本并排

版本 做法 样例 全负矩阵 n = 120 能过吗
p1719Brute 枚举边界 + 逐格加 50.41 秒 ✗ 超时
p1719 二维前缀和 0.02 秒
p1719Zero ② 但 ans = 0 一字不差 输出 0 0.02 秒
p1719Kadane 上下行 + 最大子段和 0.00 秒
这一页记住三句话
  1. 「大小不限」不等于「可以是空的」。 ans 的初值要是一个真实存在的矩形 (比如 a[1][1]),不能是 0。
  2. 指数是 6 的时候没有「再大一点点」n 从 80 到 120 只大 1.5 倍,时间涨 11 倍。
  3. ★★★ 对拍的小数据不是凑合 —— 小本身就是覆盖能力。 同一个 bug,随机 n ≤ 6 抓到 23 / 300,照着题面规模随机 n = 30..120 抓到 0 / 300