从这篇文章开始我们来好好啃啃dp这根硬骨头,仔细分析一下它的套路和一般做法,下面这道题是每个学dp的人应该都要接触的题目
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
分析题意:
求从数字三角形顶端的第一个数字往下走到底部,按照某一条路径,沿途收集到的数字总和的最大值。
由于每一步都有两种走法(往左下走 or 往右下走),因此如果有n
行,则一共有2^(n-1)
条路径。题目提到n
的范围是500
,因此用爆搜做的话肯定是会超时的。
为什么
dp
比爆搜要快呢,因为dp
是每次解决一类问题,是个集合,
而爆搜是每次解决一个问题,显然dp
更快
所以这道题我们考虑用dp
来做。
思考方式:
我们有两种思考方式,一种是自顶向下,另一种是自底向上。
对于第一种,当我们考虑每个数字的时候就要考虑是从左上方下来的还是从右上方下来的,这就会导致我们在考虑边界情况的时候,有的数只能从左上方或者右上方下来,因此会要添加一些特殊判断。
但是对于第二种,我们考虑每个数的时候就要考虑是从左下方上来的还是从右下方上来的,不管是考虑哪一个数字,它左右下方都一定没有出界,就无须判断边界了,因此这种做法比第一种代码量更少,可以少一些特殊判断,也更好写。
方法一:自底向上式
对于数字三角形这一类路线问题,我们状态表示一般可以用点的坐标来表示,用一个二维的数组来表示。
dp[i][j]状态表示有集合和属性两个方面。这道题的“集合”表示的是一类路径(注意,表示的是“一类”),“属性”表示的是最大值max
。具体见下图。
状态计算就是计算一下dp[i][j]
的值是多少,由于我们上面阐述了,dp[i][j]
表示的是一类路径的集合,这一类集合我们可以用下图的椭圆来表示,这个椭圆表示从下往上走,走到[i][j]
这个点所有路线的集合,我们要求这个集合内所有方案的最大值。
一般来说dp
问题在算集合中的最大值的时候,会将集合划分为若干个子集
(状态计算对应的是“集合划分
”),划分依据:取最后一个不同点
。对于本题来说,最后一步只有两种选择方式:从左下方上去([i+1][j]
走到[i][j]
)和从右下方上去([i+1][j+1]
走到[i][j]
)。
所以求整个椭圆最大值的时候只需分别求得左右两边的最大值,之后将两者取max即可。
那么椭圆左边部分max如何求呢?
我们发现,左边部分所有方案都是先从最后一行沿着某些路径走,先走到[i+1][j]
,然后往右上方走,走到[i][j]
,右边部分所有集合也都是像这样的一个形式。
那对于如何求所有这样形式的最大值max
,我们可以这样分析:每一种这样形式的路线都可以分为两部分:
第一部分是当前[i][j]
这个点(所有路线都包含这个点)。
第二部分是从最底层走到[i+1][j]
这个点。
因此我们求max
时,只需使变化的部分取max
即可,即第二个部分(第二部分最大值含义:从底部走到[i+1][j]
所有路线的最大值)。
我们发现,“第二部分最大值”根据定义恰好是dp[i+1][j]
状态表示(从底部走到[i+1][j]
所有路线max
)的结果。最后我们得出结论,椭圆左边部分max
即为dp[i+1][j]+w[i][j]
同理可求椭圆右边部分max
:dp[i+1][j+1]+w[i][j]
当椭圆左右两边max
都有了,我们可以得出最终结果(也就是状态转移方程):max(dp[i+1][j]+w[i][j],dp[i+1][j+1]+w[i][j])
题目所求即为dp[1][1]:从最下方走到顶部[1][1]的所有路线最大值。
时间复杂度:O(n^2)
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int dp[N][N],w[N][N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=i;++j)
{
cin>>w[i][j];
}
}
for(int i=1;i<=n;++i) dp[n][i] = w[n][i];
for(int i=n-1;i;--i)
{
for(int j=1;j<=i;++j)
{
dp[i][j] = max(dp[i+1][j],dp[i+1][j+1]) + w[i][j];
}
}
cout<<dp[1][1]<<endl;
return 0;
}
简化:
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int dp[N][N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=i;++j)
{
cin>>dp[i][j];
}
}
for(int i=n-1;i;--i)
{
for(int j=1;j<=i;++j)
{
dp[i][j]+=max(dp[i+1][j],dp[i+1][j+1]);//更新前是原三角形的值,更新后是状态的值
}
}
cout<<dp[1][1]<<endl;
return 0;
}
方式二:自顶向下,增加边界情况判断
//线性DP
//状态转移方程:dp[i][j]=max(dp[i-1][j-1]+a[i][j],dp[i-1][j]+a[i][j])
#include<iostream>
#include<cstdio>
#include<algorithm>
const int _max = 500;
#define inf 1e9
int a[_max+10][_max+10];
int dp[_max+10][_max+10];
int n;
using namespace std;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
cin>>a[i][j];
}
//状态数组初始化成负无穷,注意考虑边界,i,j从0开始,j到i+1
for(int i=0;i<=n;i++)
for(int j=0;j<=i+1;j++)
dp[i][j]=-inf;
dp[1][1]=a[1][1];
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
dp[i][j]=max(dp[i-1][j-1]+a[i][j],dp[i-1][j]+a[i][j]);
int maxv=-inf;
for(int i=1;i<=n;i++)//枚举最底层的各个终点的状态(起点到终点路径数字和),取最大值
maxv=max(maxv,dp[n][i]);
cout<<maxv<<endl;
return 0;
}