一、 [USACO1.5] [IOI1994]数字三角形 Number Triangles
题目描述
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 7→3→8→7→5 的路径产生了最大权值。
输入格式
第一个行一个正整数 r r r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例 #1
样例输入 #1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 #1
30
提示
【数据范围】
对于
100
%
100\%
100% 的数据,
1
≤
r
≤
1000
1\le r \le 1000
1≤r≤1000,所有输入在
[
0
,
100
]
[0,100]
[0,100] 范围内。
题目翻译来自NOCOW。
USACO Training Section 1.5
IOI1994 Day1T1
二、思考
如下图:给出两幅图,你会想到什么解决办法呢?
错误思路
找到每行的最大值,依次向下查找
如果是按照这样的思路,会发现其他路径还有比当前路径之和大,代码运行失败!
思路分析
题意:从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来。如何走才能使得这个和尽量大?
[分析]
如果熟悉回溯法,你可能会立刻发现这是一个动态的决策问题:每次有两种选择一一左下或右下。如果用回溯法求出所有可能的路线,就可以从中选出最优路线。但和往常一样,回溯法的效率太低:一个n层数字三角形的完整路线有 2”条,当n 很大时回溯法的速度将让人无法忍受。
为了得到高效的算法,需要用抽象的方法思考问题:把当前的位置(,看成一个状态(还记得吗?),然后定义状态(i的指标函 d(i,为从格子i,出发时能得的最大和(包括格子(G)本身的值)。在这个状态定义下,原问题的解是 d(1,1)。
下面看看不同状态之间是如何转移的。从格子(i,j)出发有两种决策。如果往左走,则走到(i+1,j)后需要求“从(i+1,j),出发后能得到的最大和”这一问题,即 d(i+1,j)。类似地,往右走之后需要求解 d(i11)。由于可以在这两个决策中自由选择,所以应选择 d(i+1,j)和d(i+1,j+1)中较大的一个。
换句话说,得到了所谓的状态转移方程:
d(i,j)=a(i,j)+max{d(i+l,j),d(i+l,j+I)}
如果往左走,那么最好情况等于(i,)格子里的值a(i,与“从(1出发的最大总和”之和,此时需注意这里的“最大”二字。如果连“从(+1.出发走到底部”这部分的和都不是最大的,加上 a(i,之后肯定也不是最大的。这个性质称为最优子结构 (optimalsubstructure),也可以描述成“全局最优解包含局部最优解”。不管怎样,状态和状态转移方程一起完整地描述了具体的算法。
回溯法
如图,构造初始的数字三角形
动态规划演示—从底端开始
最优解数字三角形构造完成,最优解为:30
此处回溯找出路径
从第一行开始,减去她它下一行左边或右边最优解的较大值,并从较大值开始往下求解
三、Code
C语言
//数字三角形--模板
#include <stdio.h>
int max(int a,int b) {
return a>b?a:b;
}
void Write();
void Solution(int n,int triangle[][1001]);
int n;//行数
int triangle[1001][1001]; //三角形数组 (报错:RE,数组大小没与题目统一 )
//主函数
int main() {
Write();
Solution(n,triangle);
printf("%d",triangle[1][1]);
return 0;
}
void Write() {
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= i; ++j)
scanf("%d",&triangle[i][j]);
}
//方案,核心代码
void Solution(int n,int triangle[][1001]) {
for(int i = n-1; i >= 1; --i) //逆序枚举
for(int j = 1; j <= i; ++j)
triangle[i][j] += max(triangle[i + 1][j],triangle[i + 1][j + 1]);
}
Java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] arr = new int[n + 1][];
for (int i = 1; i <= n; i++) {
arr[i] = new int[i + 1];
for (int j = 1; j <= i; j++) {
arr[i][j] = sc.nextInt();
}
}
for (int i = n - 1; i >= 1; i--) {
for (int j = 1; j <= i; j++) {
arr[i][j] += Math.max(arr[i + 1][j],arr[i + 1][j + 1]);
}
}
System.out.println(arr[1][1]);
}
}
C语言 数字三角形模板+回溯路径
//数字三角形模板+回溯路径
#include <stdio.h>
int max(int a,int b) {
return a>b?a:b;
}
int n; //数字三角形行数
int triangle[101][1001]; //数字三角形数组
int s[101]; //路径数组
//求解从三角形顶端到底端的路径所经过的数字和的最大值
void Solution(int n, int triangle[][1001]);
//求解路径数组
void TrackBack(int n, int triangle[][1001]);
int main() {
scanf("%d",&n);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
scanf("%d",&triangle[i][j]);
}
}
//求解从三角形顶端到底端的路径所经过的数字和的最大值
Solution(n, triangle);
//写入
printf("%d",triangle[1][1]);
//输出最优解路径
TrackBack(n, triangle);
printf("\n");
for (int i = 1; i <= n; ++i) {
printf( "%d ",s[i] );
}
return 0;
}
void Solution(int n, int triangle[][1001]) {
//从下往上计算
for (int i = n - 1; i >= 1; --i) {
for (int j = 1; j <= i; ++j) {
triangle[i][j] += max(triangle[i + 1][j], triangle[i + 1][j + 1]);
}
}
}
void TrackBack(int n, int triangle[][1001]) {
//从上往下计算
int i, j, maxium;
i = j = 1;
while(i < n) {
if (triangle[i + 1][j] > triangle[i + 1][j + 1]) {
s[i] = triangle[i][j] - triangle[i + 1][j];
i = i + 1;
j = j;
} else {
s[i] = triangle[i][j] - triangle[i + 1][j + 1];
i = i + 1;
j = j + 1;
}
}
s[i] = triangle[i][j];
}
运行结果
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
30
7 3 8 7 5
四、总结
动态规划的核心是状态、状态转移方程
状态转移方程:
d(i,j)=a(i,j)+max{d(i+l,j),d(i+l,j+I)}
此题的状态转移方程:
triangle[i][j] += max(triangle[i + 1][j],triangle[i + 1][j + 1]);
arr[i][j] += Math.max(arr[i + 1][j],arr[i + 1][j + 1]);
其他方法:记忆化搜索与递推;记忆化搜索要记得memset(d,-1,sizeof(d);
把d全部初始化为-1;再写递归函数。 此处笔者不太熟悉,略讲。
注意:不要与背包问题混淆!!!