数字金字塔
题目描述
解法一:搜索
首先考虑一下这一题可以使用贪心算法嘛?答案是不可以使用贪心算法。每次都贪心地选择最大的那个数,那么结果就是7->8->1->7->5,答案就是28,但是真正的应该是7->3->8->7->5,答最优解答案是30。因此,不能使用贪心算法。
我们可以使用二维数组来存储这个数字金字塔,从图形上来说,就是把图中的数字金字塔转换为直角三角形。从金字塔的某个点可以向左下方和右下方走,就等价于在这个直角三角形中是向下走和向右下走,这样就更利于搜索。设当前点为 ( x , y ) (x,y) (x,y),向下走就是 ( x + 1 , y ) (x+1,y) (x+1,y),向右下走就是 ( x + 1 , y + 1 ) (x+1,y+1) (x+1,y+1)。
如下图所示:
我们设最优解答案是 a n s ans ans,设变量 c c c用来记录从上到下到达该点 ( x , y ) (x,y) (x,y)时的累加和(此时 c c c应该包括了点 ( x , y ) (x,y) (x,y)的权值 a [ x ] [ y ] a[x][y] a[x][y]了),那么当我们向下走时,点 ( x + 1 , y ) (x+1,y) (x+1,y)的累加和应该就是 c + a [ x + 1 ] [ y ] c+a[x+1][y] c+a[x+1][y];当我们向右下走时,点 ( x + 1 , y + 1 ) (x+1,y+1) (x+1,y+1)的累加和应该就是 c + a [ x + 1 ] [ y + 1 ] c+a[x+1][y+1] c+a[x+1][y+1]。
写出核心代码如下:
void dfs(int x,int y,int c)
{
if(x==n-1) //走到了最后一层
{
if(c>ans)//比较此时走到最后一层的这个路径上的累加和c和最优解答案ans
ans=c;
return; //回溯
}
dfs(x+1,y,c+a[x+1][y]); //向下方走
dfs(x+1,y+1,c+a[x+1][y+1]); //向右下方走
}
画出的搜索树如下:
解法二:记忆化搜索
问题:从上到下的累加和是不能重复用的,但是从下到上的累加和是可以重复用的,如何理解这句话呢?
如下图解释,要注意,“从下到上的累加和是可以重复用的” 这个条件是我们可以使用记忆化搜索的前提,一般能使用记忆化搜索都会有这个性质。
理解如下图:
该记忆化搜索时,每一个节点只会被搜索一次,第一层有1个节点,搜索1次;第二层有2个节点,搜索2次, ⋯ \cdots ⋯,第 n n n层有 n n n个节点,搜索 n n n次,因此时间复杂度为: 1 + 2 + ⋯ + n = n ( n + 1 ) 2 1+2+\cdots+n=\dfrac {n(n+1)}{2} 1+2+⋯+n=2n(n+1)也就是 O ( n 2 ) O(n^2) O(n2)。如果 n n n的数据规模不超过 1 0 4 10^4 104,一般都能过。
写出完整能Ac的代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=510;
int n;
int a[N][N];
int f[N][N]; //用来记录从下到上回溯返回时计算出的累加和
//记忆化搜索
int dfs(int x,int y)
{
//如果这个点(x,y)已经被搜索过了,计算出来累加和了,那么我们就直接利用这个累加和就好了
//而不用继续往下搜索(x,y)这个点了
if(f[x][y]!=0)
return f[x][y];
//当走到最后一层时,从下到上记录累加和,那么此时它的累计和f[x][y]也就是它自身的权值a[x][y]
if(x==n)
f[x][y]=a[x][y];
//否则就是它自身的权值+max(下方的累加和f[x+1][y],右下方的累加和f[x+1][y+1])
else
f[x][y]=a[x][y]+max(dfs(x+1,y),dfs(x+1,y+1));
return f[x][y]; //返回搜索的节点(x,y)的结果
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
dfs(1,1); //从金字塔的顶端节点(1,1)开始搜索
//由于是从下到上统计出来的累加和,那么总的结果就全部都累加到了
//金字塔的顶端节点(1,1)上了,因此是答案就是f[1][1]
printf("%d\n",f[1][1]);
return 0;
}
解法三:动态规划(顺推法)
我们用一个二维数组 f [ x ] [ y ] f[x][y] f[x][y]表示从上到下到达点 ( x , y ) (x,y) (x,y)时的累加和。分两种情况:从左上方走到当前点 ( x , y ) (x,y) (x,y),左上方的累加和是 f [ x − 1 ] [ y − 1 ] f[x-1][y-1] f[x−1][y−1];从右上方走到当前点 ( x , y ) (x,y) (x,y),右上方的累加和是 f [ x − 1 ] [ y ] f[x-1][y] f[x−1][y]。设当前这个点 ( x , y ) (x,y) (x,y)的权值为 a [ x ] [ y ] a[x][y] a[x][y],那么点 ( x , y ) (x,y) (x,y)的累加和就是 f [ x ] [ y ] = m a x ( f [ x − 1 ] [ y − 1 ] , f [ x − 1 ] [ y ] ) + a [ x ] [ y ] f[x][y]=max(f[x-1][y-1],f[x-1][y])+a[x][y] f[x][y]=max(f[x−1][y−1],f[x−1][y])+a[x][y]。
如下图所示:
问题:为什么会有如下的初始化代码呢?
for (int i = 0; i <= n; i++) { for (int j = 0; j <= i + 1; j++)//注意这里是j<=i+1而不是j<=i //因为有负数,所以应该将两边也设为-INF dp[i][j] = INF;//因为三角形中的数有可能是负数,所以dp[i][j]也有可能是负数 }
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=510,INF=2e9;
int n;
int a[N][N];
int f[N][N];//表示从起点走到a[i][j]这个点的所经历的路径之和
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
//对f数组进行初始化全部为负无穷
//因为有负数,所以应该将边缘两边外面的位置都设为-INF
//因此i从0开始而不是从1 j从0开始到i+1 而不是从1到i
for(int i=0;i<=n;i++)
for(int j=0;j<=i+1;j++)
f[i][j]=-INF;
//对第一层做特殊处理,金字塔顶端就只有一个数,那么这个数本身就是最大值
f[1][1]=a[1][1];
//从金字塔第二层开始枚举到第n层
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
int ans=-INF;//注意这里三角形中的数有可能是负数,所以最大值也有可能是负数
//从上到下已经把结果都累加到了最后一层了,因此,我们需要去比较最后一层里面的结果
//然后找出最大的那个,就最优解
//求出金字塔最后一层的最大值,比较最后一层每一列中的最大值,找出最大的那个最大值
for(int j=1;j<=n;j++)
ans=max(ans,f[n][j]);
printf("%d\n",ans);
return 0;
}
解法四:动态规划(逆推法)
我们用一个二维数组 f [ x ] [ y ] f[x][y] f[x][y]表示从下到上到达点 ( x , y ) (x,y) (x,y)时的累加和。分两种情况:从下方走到点 ( x , y ) (x,y) (x,y),下方的累加和是 f [ x + 1 ] [ y ] f[x+1][y] f[x+1][y];从右下方走到点 ( x , y ) (x,y) (x,y),右下方的累加和是 f [ x + 1 ] [ y + 1 ] f[x+1][y+1] f[x+1][y+1]。
设当前这个点 ( x , y ) (x,y) (x,y)的权值为 a [ x ] [ y ] a[x][y] a[x][y],那么点 ( x , y ) (x,y) (x,y)的累加和就是 f [ x ] [ y ] = m a x ( f [ x + 1 ] [ y ] , f [ x + 1 ] [ y + 1 ] ) + a [ x ] [ y ] f[x][y]=max(f[x+1][y],f[x+1][y+1])+a[x][y] f[x][y]=max(f[x+1][y],f[x+1][y+1])+a[x][y]。
用逆推法的好处就在于不用判断边界情况。从下到上累加,那么最终就会把累加和集中于起点,因此最后输出起点就好了。
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=510;
int n;
int a[N][N];
int f[N][N];//表示从起点走到a[i][j]这个点的所经历的路径之和
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
//从下到上计算出累加和
for(int i=n;i>=1;i--)
for(int j=i;j>=1;j--)
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
//最终把累加和都集中于起点(1,1) 直接输出起点累加和就好了
printf("%d\n",f[1][1]);
return 0;
}