数字三角形问题/数塔问题(杭电2084)
最近在做动态规划相关的算法题,记录一下这个经典的动归算法题。
问题描述
给出一个数塔(数字三角形),要求从数塔的顶端数走到数塔底端,每一步只能走到下一行的相邻位置,求经过结点的数字之和最大值。(杭电2084)
算法思路与代码实现
动态规划算法
为了方便描述,约定用high表示数塔高度,用data[1] [1] 表示数塔最高层(第一层)的数。
假设要走出一条经过结点的数字之和最大的“最优路径”,则每次往下走都必须选择:在此结点往下以最佳路径走能得到的数字之和较大的结点。
例如:以上图为例子,数塔顶端走的下一个位置,可以选择‘12’和‘15’ , 则使数字之和最大的最优路径的选择一定是‘12’和‘15’中,往下走能得到的最大数字之和中的较大的那一个。
用dp[i] [j] 表示以data[i] [j]位置为顶的数塔,一直走到底行位置得到的各节点数值之和的最大值(即从data[i] [j]位置从下走所能得到问题的最优值)。显然底层中dp[high] [j] = data[i] [j]即只有一个数的情况下,节点数字之和就是其本身代表的数。
在这种表示下,要求解的问题转化为求dp[1] [1],易得其状态转移方程如下:
d p [ i ] [ j ] = { d a t a [ i ] [ j ] + m a x { d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j + 1 ] } 1 ≤ j ≤ i , 1 ≤ i ≤ h i g h − 1 d a t a [ h i g h ] [ j ] 1 ≤ j ≤ h i g h , i = h i g h dp[i][j]= \left\{ \begin{aligned} &data[i][j]+max\{dp[i+1][j],\ dp[i+1][j+1]\ \} &\quad 1\leq j \leq i, 1\leq i \leq high-1\\ &data[high][j]&\quad 1\leq j \leq high\ ,\ i = high\\ \end{aligned} \right. dp[i][j]={data[i][j]+max{dp[i+1][j], dp[i+1][j+1] }data[high][j]1≤j≤i,1≤i≤high−11≤j≤high , i=high
所以,我们可以从数组底部出发,利用动态转移方程就可以解决这个问题
至于得到具体的转移路径,可以通过dp[i] [j]的计算数据(dp[i] [j]数塔)得到。即由上到下可走路线中的较大值就是最佳路径走过的点。
代码
带路径输出,便于理解的代码版本。
用path数组存储最佳路线,path[i]表示应该选择第i行的第path[i]个数作为路线上的数。
// Number of tower数塔问题
#include<stdio.h>
#include<stdlib.h> //standard_library
int max(int a,int b){
return (a>b)?a:b;
}
int main(){
// 数据输入
int data[101][101];
int high;
printf("h = "); scanf("%d",&high);
printf("输入数据:\n");
for(int i=1;i<=high;i++){
for(int j=1;j<=i;j++){
scanf("%d",&data[i][j]);
}
}
// 动态规划求解
int dp[101][101];
for(int i=1;i<=high;i++)dp[high][i]=data[high][i];
for(int i=high-1;i>=1;i--){
for(int j=1;j<=i;j++){
dp[i][j] = data[i][j] + max(dp[i+1][j], dp[i+1][j+1]); // 状态转移方程
}
}
// 输出结果和dp数塔
printf("sum = %d\n",dp[1][1]);
for(int i=1;i<=high;i++){
printf("%*c",(high-i)*2, ' ');
for(int j=1;j<=i;j++){
printf("%-3d ",dp[i][j]);
}printf("\n");
}
// 寻找路线
int i,j;
int *path = (int*)malloc(sizeof(int)*high);path[0]=1;
for(i = 1,j = 1;i<high;i++){
if(dp[i+1][j+1] > dp[i+1][j]){
j++;
}path[i] = j;
}
printf("path[i] = ");
for(int i=0;i<high;i++)printf("%d ",path[i]);puts("");
}
/*
一个测试用例
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
一个测试用例
5
9
12 15
10 6 8
2 18 9 5
19 7 10 4 16
*/
杭电提交版本
实际上,要解决这个问题并不需要开辟两个数组,只需要在一个数组内就可以解决。
#include<stdio.h>
int max(int a, int b){
return (a > b) ? a : b;
}
int main(){
// 数据输入
int n;
scanf("%d", &n);
int dp[101][101];
int high;
for(int i = 0; i<n; i++){
scanf("%d", &high);
for(int i = 1; i <= high; i++){
for(int j = 1; j <= i; j++){
scanf("%d", &dp[i][j]);
}
}
// 动态规划求解
for(int i = 1; i <= high; i++)dp[high][i] = dp[high][i];
for(int i = high-1; i >= 1; i--){
for(int j = 1; j <= i; j++){
dp[i][j] += max(dp[i + 1][j], dp[i + 1][j + 1]); // 状态转移方程
}
}
// 输出结果
printf("%d\n", dp[1][1]);
}
return 0;
}
代码测试
测试用例
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
结果:
算法心得和复杂度分析
复杂度分析:
该动态规划算法的时间复杂度主要取决于状态转移方程的计算次数,以及单次计算所耗费的时间。在这个问题中,状态转移方程的计算次数显然就是数塔中数据的个数即0.5 * h * h
个,而一次计算操只需要常数时间即可完成。所以算法的时间复杂度为:
O
(
h
2
)
O(h^2)
O(h2)
心得体会:
这个问题可以直接在原数据内进行动归计算,而不用自己另外开辟空间。
根据状态转移方程可以知,每次的dp式的结果只与上一次的dp式结果有关,如果数据从塔底处开始给出,我们就能将空间消耗缩减为O(h)。
分享的同时记录学习,有问题欢迎交流指正。
觉得有用的话就点个赞吧!