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⁶))
题意的逐字翻译:上下左右四个边界各枚举一遍,再把矩形里的数加起来。
// 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;}点「运行 ▶」看结果
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 最大加权矩形 —— 能 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;}点「运行 ▶」看结果
★ O(n⁶) → O(n⁴):n = 120 时 5270 万个矩形,每个四步四则运算 —— 本机 0.02 秒。
4★ 换尺子 + 秒表
// 换一把尺子:三种写法各碰了多少下//// 用法:./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 造多大
// 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;}点「运行 ▶」看结果
// 数据生成器(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 |
这个 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);
⚠ 这道题用不着它(第 ② 版 0.02 秒)。放在这儿是因为 「二维问题固定两行、退回一维」是个到处都用得上的动作, 而且它把最大子段和(第 21、22 章那一类 DP 的入门题)接上了。
7四个版本并排
| 版本 | 做法 | 样例 | 全负矩阵 | n = 120 |
能过吗 |
|---|---|---|---|---|---|
① p1719Brute |
枚举边界 + 逐格加 | ✓ | ✓ | 50.41 秒 | ✗ 超时 |
② p1719 |
二维前缀和 | ✓ | ✓ | ★ 0.02 秒 | ★ 能 |
③ p1719Zero |
② 但 ans = 0 |
✓ 一字不差 | ✗ 输出 0 | 0.02 秒 | ✗ |
④ p1719Kadane |
上下行 + 最大子段和 | ✓ | ✓ | ★ 0.00 秒 | ✓ |
- ⚠ 「大小不限」不等于「可以是空的」。
ans的初值要是一个真实存在的矩形 (比如a[1][1]),不能是 0。 - ★ 指数是 6 的时候没有「再大一点点」:
n从 80 到 120 只大 1.5 倍,时间涨 11 倍。 - ★★★ 对拍的小数据不是凑合 —— 小本身就是覆盖能力。
同一个 bug,随机
n ≤ 6抓到 23 / 300,照着题面规模随机n = 30..120抓到 0 / 300。