从“数字金字塔”说起
观察下面的数字金字塔。写一个程序查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以从当前点走到左下方的点也可以到达右下方的点。
在上面的样例中,13 -> 8 -> 26 -> 15 -> 24的路径得到的和最大:86。
解题
- 我们要在所有路径中,找到经过数字之和的最大值。每条路径都是可逆的,同一条路径,从上往下和从下往上,得到的数字之和是相同的。
- 如果自上而下枚举的话,到第 n 行将得到 n 个数字,还需要再打擂台找最大值,多出一个步骤。我们采用自下而上枚举,到第一行就得到唯一的结果。
- 约定用
f[i][j]
表示从第 n 行的某一列出发,到达第 i 行第 j 列的所有路径中数字之和的最大值, 要到达第 i 行第 j 列的位置,只有经过第 i + 1 行第 j 列,或者第 i + 1行第 j + 1列,也就是说
f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + a[i][j];
- 递推到第 1 行第 1 列时,
f[1][1]
就是题目所求的最优路径的数字之和
这是完整代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1009;
int n, a[N][N];
int f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= i; j ++) {
cin >> a[i][j];
}
}
for (int i = n; i >= 1; i --) {
for (int j = 1; j <= i; j ++) {
f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + a[i][j];
}
}
cout << f[1][1];
return 0;
}
优化
- 回忆一下变量赋值语句
a = a + b
,是什么意思?它表示先获取变量 a 和变量 b 的值,将它们相加,再赋值给变量 a
。注意前后顺序,先获取变量a
和b
的值,运算后,再赋值给变量a
,实现更新变量a
的目的。 - 再来观察上面代码的核心语句
f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + a[i][j];
将其改成下面这条语句,如何理解?
f[j] = max(f[j], f[j + 1]) + a[i][j];
它表示,先获取f[j]
和f[j + 1]
的值,运算后,再赋值给f[j]
,实现更新数组元素f[j]
的目的 - 我们在自下而上推算最大数字和的过程中,只关心前一步的数值对目前这一步的影响,而更早步骤得到的数值并没有保存的价值。也就是说,我们不必使用二维数组(实际上,二维数组的每一行可以看做是保存了自下而上每一步的中间结果),用一维数组就够了
- 重新思考这道题,我们先用
f[j]
表示自下而上走到第 i + 1 行第 j 列的所有路径中数字之和的最大值,用f[j + 1]
表示自下而上走到第 i + 1 行第 j + 1 列的所有路径中数字之和的最大值,哪个位置的数值大,就选择从哪个位置向上走到第 i 行第 j 列,加上a[i][j]
后,就得到自下而上走到第 i 行第 j 列的所有路径中数字之和的最大值,再将这个最优结果保存在f[j]
中,用赋值语句来表示:
f[j] = max(f[j], f[j + 1]) + a[i][j]
- 递推到第 1 行第 1 列时,
f[1]
就是题目所求的最优路径的数字之和
优化后的代码,仅使用一维数组,所需空间大大减少
#include <bits/stdc++.h>
using namespace std;
const int N = 1009;
int n, a[N][N];
int f[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= i; j ++) {
cin >> a[i][j];
}
}
for (int i = n; i >= 1; i --) {
for (int j = 1; j <= i; j ++) {
f[j] = max(f[j], f[j + 1]) + a[i][j];
}
}
cout << f[1];
return 0;
}
延伸
- 在内循环中,变量 j 从 1 到 i,对应着:
f[1] = max(f[1], f[2]) + a[i][1]
,用f[1]
和f[2]
更新f[1]
,
f[2] = max(f[2], f[3]) + a[i][2]
,用f[2]
和f[3]
更新f[2]
,
......
每一次更新的 f 数组元素,都不会影响接下来的赋值语句 - 假如内循环中,变量 j 从 i 到 1,对应着:
......
f[2] = max(f[2], f[3]) + a[i][2]
f[1] = max(f[1], f[2]) + a[i][1]
,新的f[2]
被用来更新f[1]
,
每一次更新的 f 数组元素,都会影响接下来的赋值语句。显然,这样得到的结果就不对了。 - 结论对我们后面的学习非常有用。在将二维数组压缩到一维数组的时候,循环变量 j 是一维数组 f 的下标,如果赋值语句右边的下标
(1) 只出现j + *
,循环变量j
应该从小到大递增
(2) 只出现j - *
,循环变量j
应该从大到小递减
(3) 有的是j + *
,有的是j - *
,则无法将二维数组压缩到一维数组,因为在循环的过程中,前一次更新的数组元素,会对后面的赋值语句产生影响