题解:洛谷 P3016 The Triangle
【题目来源】
洛谷:P3016 [USACO11FEB] The Triangle S
【题目描述】
农夫 FJ 颁给 Bessie 一个奇怪的奖项,以奖励她前几个月壮观的牛奶产量。他给她一个 \(N\) 行的三角形网格(当然,三角形网格每行是 \(1\) 到 \(N\) 之间递增)。网格中第 \(i\) 行有 \(i\) 个数,数值为 \(v_{i,j}\)(\(-10^9\le v_{i,j}\le 10^9\)),其中 \(j\) 为 \(1\sim i\) 的数列。
Bessie 在这个三角形网格中选择了一个行数至少为 \(K\) 的子三角形网格(\(1 \le K \le N \le 700\),\(1\le K\le 20\))。子三角形网格有可能和原来的三角形朝向相反,即子三角形网格朝向的较短行对应原来三角形的较长行。
Bessie 选择完子三角形网格后,FJ 会将子三角形中的所有数值求平均数,并抹除小数,然后给 Bessie 相对应的金币(若平均值为负,则从 Bessie 处拿相应的金币),Bessie 想要取得最大的利益(或最小的代价),请你帮她解决问题
例如,Bessie 获得了一个 \(N=3,K=2\) 的三角形网格。这个问题的图像为:
/ \
/ 5 \
/-8 4\
/2 -3 6\
---------
她可以选择 \(5\) 个子三角形的任意一个
/\
/ \ / \ / \ / \ /5 \
/ 5 \ / \5\ / 5 \ / 5/\ /----\
/-8 4\ /-8 \4\ /-8 4\ /-8/ 4\ /\-8 4/\
/2 -3 6\ / 2 -3\6\ /-------\ / 2/-3 6\ / 2\-3/6 \
--------- --------- -2 -3 6 --------- ----------
三角形 左下 顶部 右下 中间
整体
这是平均值最大的一组:
/ \
/ 5/\
/-8/ 4\
/ 2/-3 6\
---------
这个子三角形的平均值为 \((4-3+6)\div3\),等于 \(2.\dot3\)(即 \(2.333\dots\)),所以答案为 \(2\)。
帮助 Bessie 求出她可获得金币的最大值。
【输入】
- 第一行两个用空格隔开的正整数 \(N,K\)。
- 第 \(2\sim N\) 行,\(i+1\) 行输入 \(i\) 个用空格隔开的整数 \(v_{i,j}\)。
【输出】
- 一行,输出 Bessie 得到的金币最大值(可能为负)。
【输入样例】
3 2
5
-8 4
2 -3 6
【输出样例】
2
【核心思想】
-
问题分析:给定一个 \(N\) 行三角形网格(第 \(i\) 行有 \(i\) 个数),需要选择一个行数至少为 \(K\) 的子三角形(可正立或倒立),使得子三角形内所有数的平均值(向下取整)最大。要求输出最大平均值(整数部分)。\(N \le 700\),\(K \le 20\),数值范围 \(\pm 10^9\),结果可能为负。本质是一个二维前缀和 + 枚举扩展问题,由于 \(N\) 较小,可以通过枚举子三角形的顶点位置,并逐步扩展高度,利用行前缀和快速计算区域和,动态维护当前子三角形的元素总和与个数,从而在 \(O(N^3)\) 时间内求出最大值。
-
算法选择:
- 行前缀和:预处理每行的前缀和
sa[i][j],以便 \(O(1)\) 计算某行任意连续区间的和。 - 枚举顶点 + 动态扩展:分别枚举正立和倒立子三角形的“顶点”位置,然后从最小高度 \(K\) 开始,逐行增加高度,每次累加新加入行的区间和,更新元素总数和总和,计算当前平均值的整数部分并更新答案。
- 复杂度:枚举顶点数 \(O(N^2)\),每个顶点最多扩展 \(O(N)\) 层,总操作 \(O(N^3)\),\(N=700\) 时约 \(3.4\times10^8\) 次简单运算,在 C++ 中可接受。
- 行前缀和:预处理每行的前缀和
-
关键步骤:
- 读入与预处理:读入 \(N, K\) 和三角形数组 \(a[i][j]\)(\(1 \le j \le i\)),计算每行的前缀和
sa[i][j]。 - 枚举正立(朝上)子三角形:
- 顶部顶点为 \((i, j)\)(第 \(i\) 行第 \(j\) 列),\(i\) 从 \(1\) 到 \(N-K+1\),\(j\) 从 \(1\) 到 \(i\)。
- 初始化当前子三角形的元素总和
tot = 0,当前高度h = 0。 - 逐行向下扩展:\(h\) 从 \(1\) 开始,直到 \(i+h-1 \le N\)。对于第 \(r\) 行(\(r = i+h-1\)),该行在子三角形中的列范围为 \([j, j+h-1]\),利用前缀和计算该区间和,累加到
tot。 - 当前子三角形的元素个数为
cnt = h*(h+1)/2。 - 若 \(h \ge K\),计算平均值整数部分
tot / cnt,更新答案。
- 枚举倒立(朝下)子三角形:
- 底部顶点为 \((i, j)\)(第 \(i\) 行第 \(j\) 列),要求 \(i \ge 2K-1\),且 \(j\) 满足 \(j \ge K\) 且 \(i-K+1 \ge j\)(以保证上方的行有足够的列)。
- 初始化
tot = 0,h = 0。 - 逐行向上扩展:\(h\) 从 \(1\) 开始,直到 \(i-h+1 \ge 1\) 且 \(j-h+1 \ge 1\)。对于第 \(r\) 行(\(r = i-h+1\)),该行在子三角形中的列范围为 \([j-h+1, j]\),利用前缀和计算区间和,累加到
tot。 - 当前子三角形元素个数为
cnt = h*(h+1)/2。 - 若 \(h \ge K\),更新答案。
- 输出:最终答案
ans(可能为负数)。
- 读入与预处理:读入 \(N, K\) 和三角形数组 \(a[i][j]\)(\(1 \le j \le i\)),计算每行的前缀和
-
时间/空间复杂度:
- 时间复杂度:\(O(N^3)\),\(N \le 700\),约 \(3.4\times10^8\) 次操作,常数较小,可接受。
- 空间复杂度:\(O(N^2)\),存储三角形数组和前缀和。
-
枚举 + 前缀和的核心思想:
- 行前缀和加速求和:对于每行,预处理前缀和,使任意连续子段和能在 \(O(1)\) 时间内得到,避免内层循环重复计算。
- 动态扩展维护:枚举顶点后,通过逐层增加高度,每次只累加新加入的一行,维护总和与元素个数,避免了重新计算整个子三角形的开销。
- 正立与倒立统一处理:正立和倒立子三角形只需调整扩展方向和列范围即可,逻辑对称。
- 整数平均:由于要求向下取整,直接用整数除法
sum / cnt,无需浮点数。 - 边界条件:倒立三角形的列范围需保证 \(j-h+1 \ge 1\),且 \(i-h+1 \ge j\),确保子三角形完全位于原三角形内。
- 适用于小规模网格中求所有定向子三角形平均值最值的问题。
【算法标签】
普及 #前缀和
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用 long long 避免溢出
const int N = 705; // 最大网格规模
int n, m; // n: 三角形行数, m: 子三角形最小行数(即 K)
int ans = -1e18; // 最终答案(最大平均值的整数部分)
int a[N][N]; // 原始三角形数值
int sa[N][N]; // sa[i][j] 表示第 i 行前 j 个数的前缀和
signed main()
{
cin >> n >> m; // 输入 N 和 K
// 读入三角形并构建每行的前缀和
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
cin >> a[i][j];
sa[i][j] = sa[i][j - 1] + a[i][j];
}
}
// ---- 枚举所有正立(朝上)的子三角形 ----
// 子三角形的顶部顶点为 (i, j),即第 i 行第 j 个元素
for (int i = 1; i + m - 1 <= n; i++) // 顶部所在行,需保证能放下至少 m 行
{
for (int j = 1; j <= i; j++) // 顶部在该行的列位置
{
int tot = 0; // 当前子三角形的元素总和
// 前 m-1 行(即最小行数的前几行),每行元素个数逐渐增加
// 对于行号 row = i + k - 1,该行对应的列范围是从 j 到 j + k - 1
for (int k = 1; k < m; k++)
{
int row = i + k - 1;
int col = j + k - 1;
tot += sa[row][col] - sa[row][j - 1];
}
// 从第 m 行开始,子三角形的行数可以继续增大(k 表示当前处理的行号偏移,即总行数 = k)
// 注意循环条件保证行数不超过 n
for (int k = m; i + k - 1 <= n; k++)
{
int row = i + k - 1;
int col = j + k - 1;
tot += sa[row][col] - sa[row][j - 1]; // 累加当前行的和
int cnt = k * (k + 1) / 2; // 当前子三角形的元素个数(第 k 行有 k 个元素)
ans = max(ans, tot / cnt); // 更新最大值(整除,即向下取整的平均值)
}
}
}
// ---- 枚举所有倒立(朝下)的子三角形 ----
// 倒立三角形的“顶部”(实际是最底部的行)为 (i, j),即第 i 行第 j 个元素,
// 且 j 应位于该行的中间偏右位置,以保证三角形有 m 行
for (int i = 2 * m - 1; i <= n; i++) // 至少需要 2*m-1 行才能构成一个倒立三角形(从 i-m+1=m 开始,保证顶端那行有 m 列)
{
for (int j = m; i - m + 1 >= j; j++) // j 的范围保证上方有足够的列(每行行数和列数应该相同)
{
int tot = 0; // 当前子三角形的元素总和
// 前 m-1 行(从底部往上数),每行元素个数逐渐减少
// 对于行号 row = i - k + 1,该行对应的列范围是从 col 到 j
for (int k = 1; k < m; k++)
{
int row = i - k + 1;
int col = j - k + 1;
tot += sa[row][j] - sa[row][col - 1];
}
// 从第 m 行开始,继续向上扩展行数
for (int k = m; j - k + 1 >= 1 && i - k + 1 >= j; k++)
{
int row = i - k + 1;
int col = j - k + 1;
tot += sa[row][j] - sa[row][col - 1]; // 累加当前行的和
int cnt = k * (k + 1) / 2; // 当前子三角形的元素个数
ans = max(ans, tot / cnt); // 更新最大值
}
}
}
cout << ans << endl; // 输出最大金币数(平均值向下取整)
return 0;
}
【运行结果】
3 2
5
-8 4
2 -3 6
2
浙公网安备 33010602011771号