题目描述
XYZ\texttt{XYZ}XYZ 电视台正在开发一个新的游戏节目,节目中有一种三角形堆叠的球,每个球都有一个整数值。参赛者可以选择一些球拿走,奖品是所选球的值之和。但是,如果参赛者要拿走某个球,那么他必须同时拿走所有直接位于这个球上方的球。这意味着在选择球时必须遵循一个金字塔形的依赖关系:如果要拿走一个球,就必须拿走支撑它的所有上层球。参赛者可以选择不拿任何球,此时奖品为 000 。现在,节目导演想知道对于给定的球堆叠,参赛者能够获得的最大奖品是多少。
输入格式
每个测试用例由多行组成。第一行包含一个整数 NNN ,表示球堆叠的行数( 1≤N≤10001 \leq N \leq 10001≤N≤1000 )。接下来 NNN 行,第 iii 行包含 iii 个整数 BijB_{ij}Bij ( −105≤Bij≤105-10^5 \leq B_{ij} \leq 10^5−105≤Bij≤105 ),表示第 iii 行第 jjj 个球的值(第一行是最顶层,每行内第一个球是最左边的球)。最后一个测试用例之后是一个仅包含 000 的行。
输出格式
对于每个测试用例,输出一行一个整数,表示参赛者能够获得的最大奖品。
样例输入
4
3
-5 3
-8 2 -8
3 9 -2 7
2
-2
1 -10
3
1
-5 3
6 -4 1
0
样例输出
7
0
6
题目分析
问题本质
这个问题可以看作是在一个有向无环图(DAG\texttt{DAG}DAG)上寻找最大权重的闭合子图。每个球 (i,j)(i,j)(i,j) 依赖于它正上方的两个球 (i−1,j−1)(i-1,j-1)(i−1,j−1) 和 (i−1,j)(i-1,j)(i−1,j) (如果存在)。我们需要选择一个球集合,使得如果集合中包含某个球,那么该球的所有直接依赖球也必须包含在集合中。
关键观察
- 金字塔结构 :由于依赖关系是金字塔形的,任何被选择的球集合在视觉上都会形成一个或多个金字塔形状的区域。
- 列的重要性 :如果从列的角度来看,每个金字塔区域可以看作是由若干列的部分组成的。特别地,一个以 (i,j)(i,j)(i,j) 为右下角的金字塔区域,包含了第 111 列到第 jjj 列的部分行。
- 最优子结构 :我们可以将大问题分解为子问题。考虑以某个球 (i,j)(i,j)(i,j) 作为所选区域右下角的情况,我们可以选择:
- 让它作为一个独立金字塔的右下角。
- 让它与左侧的某个金字塔区域结合,形成更大的区域。
解题思路
基于上述观察,我们采用动态规划来解决这个问题。算法的核心步骤和定义如下:
-
列前缀和 :定义 g[i][j]g[i][j]g[i][j] 为从第 111 行到第 iii 行,第 jjj 列所有球的值之和。这可以通过递推快速计算:
g[i][j]=g[i−1][j]+value[i][j] g[i][j] = g[i-1][j] + value[i][j] g[i][j]=g[i−1][j]+value[i][j] -
金字塔区域总和 :定义 w[i][j]w[i][j]w[i][j] 为以球 (i,j)(i,j)(i,j) 为右下角的整个金字塔区域的总和。这个区域包括了第 111 列到第 jjj 列中,从特定行开始的部分球。可以通过递推计算:
w[i][j]=g[i][j]+w[i−1][j−1] w[i][j] = g[i][j] + w[i-1][j-1] w[i][j]=g[i][j]+w[i−1][j−1]
这个公式的含义是:以 (i,j)(i,j)(i,j) 为右下角的金字塔,等于第 jjj 列从顶部到第 iii 行的和,加上以 (i−1,j−1)(i-1, j-1)(i−1,j−1) 为右下角的(小一号的)金字塔。 -
动态规划状态 :定义 dp[i][j]dp[i][j]dp[i][j] 为以球 (i,j)(i,j)(i,j) 为右下角的金字塔区域能够获得的最大奖品值。注意,这里的“区域”可能是一个完整的独立金字塔,也可能是由左侧某个金字塔区域扩展而来。
状态转移有两种情况:- 情况一:独立金字塔 。此时 dp[i][j]=w[i][j]dp[i][j] = w[i][j]dp[i][j]=w[i][j] ,即直接取整个金字塔区域。
- 情况二:与左侧区域结合 。此时我们需要找到一个左侧的、结束行在 iii 之上的最佳区域,然后接上第 jjj 列从顶部到第 iii 行的部分。形式化地:
dp[i][j]=max(dp[i][j], maxk<i{dp[k][j−1]}+g[i][j]) dp[i][j] = \max(dp[i][j], \; \max_{k < i} \{ dp[k][j-1] \} + g[i][j] ) dp[i][j]=max(dp[i][j],k<imax{dp[k][j−1]}+g[i][j])
其中 maxk<i{dp[k][j−1]}\max_{k < i} \{ dp[k][j-1] \}maxk<i{dp[k][j−1]} 表示在第 j−1j-1j−1 列中,所有结束行 kkk 小于 iii 的区域的最大 dpdpdp 值。
-
后缀最大值优化 :直接计算 maxk<i{dp[k][j−1]}\max_{k < i} \{ dp[k][j-1] \}maxk<i{dp[k][j−1]} 需要遍历,会使复杂度达到 O(N3)O(N^3)O(N3) 。为了优化到 O(N2)O(N^2)O(N2) ,我们引入后缀最大值数组 sdp[i][j]sdp[i][j]sdp[i][j] ,它表示在第 jjj 列中,从第 iii 行开始(包括第 iii 行)往下(行号增大方向)的所有 dpdpdp 值中的最大值。这样, maxk<i{dp[k][j−1]}\max_{k < i} \{ dp[k][j-1] \}maxk<i{dp[k][j−1]} 就等于 sdp[i−1][j−1]sdp[i-1][j-1]sdp[i−1][j−1] 。 sdpsdpsdp 数组可以从下往上递推计算:
sdp[i][j]=max(dp[i][j], sdp[i+1][j]) sdp[i][j] = \max(dp[i][j], \; sdp[i+1][j]) sdp[i][j]=max(dp[i][j],sdp[i+1][j]) -
最终答案 :遍历所有可能的右下角 (i,j)(i,j)(i,j) ,取 dp[i][j]dp[i][j]dp[i][j] 的最大值即可。因为可以选择不拿任何球,所以答案至少为 000 ,我们在初始化时注意处理负值情况。
算法流程
- 读入 NNN ,若为 000 则结束。
- 读入球的值,并计算列前缀和数组 ggg 。
- 利用 ggg 计算金字塔区域总和数组 www 。
- 初始化 dpdpdp 和 sdpsdpsdp 数组为 000 。
- 对于每一列 jjj 从 111 到 NNN :
- 对于每一行 iii 从 jjj 到 NNN (因为金字塔要求 i≥ji \ge ji≥j):
- 计算 dp[i][j]=w[i][j]dp[i][j] = w[i][j]dp[i][j]=w[i][j] 。
- 如果 j>1j > 1j>1 ,则 dp[i][j]=max(dp[i][j], sdp[i−1][j−1]+g[i][j])dp[i][j] = \max(dp[i][j], \; sdp[i-1][j-1] + g[i][j])dp[i][j]=max(dp[i][j],sdp[i−1][j−1]+g[i][j]) 。
- 用 dp[i][j]dp[i][j]dp[i][j] 更新最终答案。
- 计算第 jjj 列的后缀最大值 sdpsdpsdp :从 i=Ni = Ni=N 往下到 i=ji = ji=j ,递推计算 sdp[i][j]sdp[i][j]sdp[i][j] 。
- 对于每一行 iii 从 jjj 到 NNN (因为金字塔要求 i≥ji \ge ji≥j):
- 输出最终答案。
复杂度分析
- 时间复杂度 :计算 ggg 和 www 需要 O(N2)O(N^2)O(N2) ,动态规划的双层循环也是 O(N2)O(N^2)O(N2) ,计算后缀最大值也是 O(N2)O(N^2)O(N2) 。总时间复杂度为 O(N2)O(N^2)O(N2) ,对于 N≤1000N \leq 1000N≤1000 是可接受的。
- 空间复杂度 :需要存储 ggg 、 www 、 dpdpdp 和 sdpsdpsdp 等多个 N×NN \times NN×N 的数组,空间复杂度为 O(N2)O(N^2)O(N2) 。由于 N≤1000N \leq 1000N≤1000 ,这也在合理范围内。
代码实现
// Ball Stacking
// UVa ID: 12357
// Verdict: Accepted
// Submission Date: 2025-12-03
// UVa Run Time: 0.170s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int value[MAXN][MAXN]; // 存储每个球的值
int colSum[MAXN][MAXN]; // g[i][j]:列前缀和
int pyramid[MAXN][MAXN]; // w[i][j]:金字塔区域总和
int dp[MAXN][MAXN]; // dp[i][j]:以(i,j)为右下角的最大和
int suffixMax[MAXN][MAXN]; // sdp[i][j]:后缀最大值
int main() {
int n;
// 循环处理每个测试用例,直到遇到 n == 0
while (cin >> n && n != 0) {
// 初始化数组(虽然全局变量默认为0,但显式重置更安全)
memset(value, 0, sizeof(value));
memset(colSum, 0, sizeof(colSum));
memset(pyramid, 0, sizeof(pyramid));
memset(dp, 0, sizeof(dp));
memset(suffixMax, 0, sizeof(suffixMax));
// 读入球的值并计算列前缀和
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
cin >> value[i][j];
// 列前缀和:当前值加上上一行同列的值
colSum[i][j] = colSum[i-1][j] + value[i][j];
}
}
// 计算金字塔区域总和 w[i][j]
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
// w[i][j] = g[i][j] + w[i-1][j-1]
pyramid[i][j] = colSum[i][j] + pyramid[i-1][j-1];
}
}
int ans = 0; // 最终答案,初始化为0(可以不选任何球)
// 动态规划,按列进行
for (int j = 1; j <= n; j++) {
// 对于当前列j,处理所有可能的行i (i >= j)
for (int i = j; i <= n; i++) {
// 情况1:独立的金字塔区域
dp[i][j] = pyramid[i][j];
// 情况2:与左侧(j-1列)的区域结合
if (j > 1) {
// 使用后缀最大值快速找到左侧最佳区域
dp[i][j] = max(dp[i][j], suffixMax[i-1][j-1] + colSum[i][j]);
}
// 更新全局最大答案
ans = max(ans, dp[i][j]);
}
// 计算当前列j的后缀最大值数组 sdp[][j]
// 从最后一行开始往前计算
suffixMax[n][j] = dp[n][j];
for (int i = n - 1; i >= j; i--) {
suffixMax[i][j] = max(suffixMax[i+1][j], dp[i][j]);
}
}
// 输出当前测试用例的答案
cout << ans << endl;
}
return 0;
}
总结
本题是一个经典的动态规划问题,其难点在于如何定义合适的状态来表示金字塔形状的依赖关系,以及如何通过预处理和后缀最大值优化来将算法复杂度控制在 O(N2)O(N^2)O(N2) 以内。核心思想是将问题分解为以每个球为右下角的子问题,并考虑该子问题是作为独立金字塔存在,还是与左侧的金字塔区域结合。通过列前缀和、金字塔区域总和以及后缀最大值等技巧,我们可以高效地计算出所有可能情况下的最大奖品值。
这个解法不仅正确,而且效率足够高,能够处理题目中 NNN 最大为 100010001000 的数据规模。
594

被折叠的 条评论
为什么被折叠?



