题解:洛谷 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

【核心思想】

  1. 问题分析:给定一个 \(N\) 行三角形网格(第 \(i\) 行有 \(i\) 个数),需要选择一个行数至少为 \(K\) 的子三角形(可正立或倒立),使得子三角形内所有数的平均值(向下取整)最大。要求输出最大平均值(整数部分)。\(N \le 700\)\(K \le 20\),数值范围 \(\pm 10^9\),结果可能为负。本质是一个二维前缀和 + 枚举扩展问题,由于 \(N\) 较小,可以通过枚举子三角形的顶点位置,并逐步扩展高度,利用行前缀和快速计算区域和,动态维护当前子三角形的元素总和与个数,从而在 \(O(N^3)\) 时间内求出最大值。

  2. 算法选择

    • 行前缀和:预处理每行的前缀和 sa[i][j],以便 \(O(1)\) 计算某行任意连续区间的和。
    • 枚举顶点 + 动态扩展:分别枚举正立和倒立子三角形的“顶点”位置,然后从最小高度 \(K\) 开始,逐行增加高度,每次累加新加入行的区间和,更新元素总数和总和,计算当前平均值的整数部分并更新答案。
    • 复杂度:枚举顶点数 \(O(N^2)\),每个顶点最多扩展 \(O(N)\) 层,总操作 \(O(N^3)\)\(N=700\) 时约 \(3.4\times10^8\) 次简单运算,在 C++ 中可接受。
  3. 关键步骤

    • 读入与预处理:读入 \(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 = 0h = 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(可能为负数)。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^3)\)\(N \le 700\),约 \(3.4\times10^8\) 次操作,常数较小,可接受。
    • 空间复杂度:\(O(N^2)\),存储三角形数组和前缀和。
  5. 枚举 + 前缀和的核心思想

    • 行前缀和加速求和:对于每行,预处理前缀和,使任意连续子段和能在 \(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
posted @ 2026-09-01 16:52  团爸讲算法  阅读(9)  评论(0)    收藏  举报