C++ 每日一题13:数字金字塔

每日一题13:数字金字塔

题目描述

观察如下数字金字塔。请写一个程序查找从最高点到底部任意点(数字金字塔共有R(1\leq R \leq 1000R\in \mathbb{Z})行)结束的路径,使路径经过数字的和最大,每一步只能走到左下方的点或右下方的点。

7

3  8

8  1  0

2  7  4  4

4  5  2  6  5

在如上样例中,7→3→8→7→5的路径产生了所有路径中最大的和7+3+8+7+5=30。

输入格式

第1行输入R,表示行数。

之后每行为数字金字塔特定行包含的整数(所有数非负且不大于100)。

输出格式

一行,路径产生的最大的和。

分析

方法1

直接搜索。设二维数组a存放数字金字塔中的每个整数,ans存放最终结果即最大的和。问题要求从最高点按照规则走到最低点的路径的最大权值和,路径起终点固定,走法规则明确,可考虑使用搜索解决问题。

定义无返回类型递归函数dfs(int x,int y,int cur),其中xy表示当前已从(1,1)走到(x,y),目前已走路径上的权值和为cur

x==n时,到达递归出口,如果cur>ans,则把ans更新为cur。当x<n时,未到达递归出口,则向下一行两个位置行走,即递归执行dfs(x+1,y,cur+a[x+1][y])dfs(x+1,y+1,cur+a[x+1][y+1])

方法1实际上把所有路径都走了一边,由于每一条路径由n-1步组成,每一步有左右两种选择,因此路径总数为2^{n-1},时间复杂度为O(2^{n-1})n太大会超时。

方法2

方法1之所以会超时,是因为进行了重复搜索。当多次来到同一点时,可以直接调用来到这一点时的路径权值和,避免重复搜索。该方法称为记忆化搜索。

定义dfs(int x,int y)表示从(x,y)出发到终点的路径最大权值和,所以dfs(1,1)为所求答案。计算dfs(x,y)时考虑第一步是向左还是向右,就把所有路径分成两大类。

第一步向左的路径:从(x,y)出发到终点的这类路径就被分成两个部分,先从(x,y)(x+1,y),要使得这种情况的路径权值和最大,第二部分从(x+1,y)到终点的路径权值和也要最大,所以这一部分可以表示成dfs(x+1,y)。综上所述,第一步向左的路径最大权值和为a[x][y]+dfs(x+1,y)

第一步向右的路径:从(x,y)出发到终点的这类路径就被分成两个部分,先从(x,y)(x+1,y+1),要使得这种情况的路径权值和最大,第二部分从(x+1,y+1)到终点的路径权值和也要最大,所以这一部分可以表示成dfs(x+1,y+1)。综上所述,第一步向左的路径最大权值和为a[x][y]+dfs(x+1,y+1)。分析方法与如上分类同理。

同样为了避免重复搜索,开设全局数组f记录从每个点出发到终点路径的最大权值和,一开始全部初始化为-1表示未被计算过(即未到达过这一点)。在计算dfs(x,y)时,首先查询f[x][y],如果f[x][y]\neq -1,说明dfs(x,y)之前已被计算过,直接返回f[x][y]即可,否则计算出dfs(x,y)的值并存储在f[x][y]中,以便下次调用。

由于f[x][y]对于每个合法的(x,y)都计算且仅计算过一次,而且是在O(1)内完成的,因此时间复杂度为O(n^2),不超时。

方法3

方法3使用动态规划完成。方法2的记忆化搜索本质上与动态规划高度相似。

确定状态

题目要求从(1,1)出发到底层路径的最大权值和,路径是由各个点串联而成,路径起点固定,终点和中间点相对不固定。因此定义f[x][y]表示从(1,1)出发到达(x,y)的路径最大权值和,并设ans=max(f[n][1],f[n][2],f[n][3],······,f[n][n])

确定状态转移方程和边界条件

不考虑(1,1)(x,y)的每一步是如何走的,只考虑最后一步是如何走的,根据最后一步是向左还是向右分成如下两种情况:

最后一步向左的路径:最后一步是从(x-1,y)(x,y),此类路径被分割成两部分,第一部分是从(1,1)走到(x-1,y),第二部分是从(x-1,y)走到(x,y),要计算此类路径的最大权值和,必须用到第一部分的最大权值和,此部分问题的性质与f[x][y]一样,就是f[x-1][y],第二部分就是a[x][y]。两部分相加即得到此类路径的最大权值和为a[x][y]+f[x-1][y]

最后一步向右的路径: 最后一步是从(x-1,y-1)(x,y),此类路径被分割成两部分,第一部分是从(1,1)走到(x-1,y-1),第二部分是从(x-1,y-1)走到(x,y),要计算此类路径的最大权值和,必须用到第一部分的最大权值和,此部分问题的性质与f[x][y]一样,f[1][1]==a[1][1]就是f[x-1][y-1],第二部分就是a[x][y]。两部分相加即得到此类路径的最大权值和为a[x][y]+f[x-1][y-1]

f[x][y]的计算需要求出如上两种情况的最大值。综上所述,得到状态转移方程为f[x][y]=a[x][y]+max(f[x-1][y-1],f[x-1][y])

与递归关系式需要终止条件一样,这里也需要对边界进行处理防止无限递归。计算f[x][y]时需要用到f[x-1][y-1]f[x-1][y],随着递归的深入,最终都要用到f[1][1]f[1][1]的计算不可再用状态转移方程,而是应直接赋予一个特值a[1][1]

所以根据上述得到边界条件为f[1][1]=a[1][1]

现在分析一下此动态规划的正确性,分析该解法是否满足使用动态规划的两个前提:

最优化原理:这个在分析状态转移方程时已经较为透彻,明显符合最优化原理。

无后效性:状态转移方程中,只会关心f[x-1][y-1]f[x-1][y]的值,计算f[x-1][y-1]f[x-1][y]时可能有多种不同的决策对应着最优值,选哪种决策对计算f[x][y]的决策没有影响,符合无后效性。

实现

由于状态转移方程就是递归关系式,边界条件就是递归终止条件,所以可以用递归来完成。递归存在重复调用,利用记忆化搜索可以解决这一点。记忆化搜索实现比较简单,且不会计算无用状态,但递归也会受到"栈的大小"和"递推+回归执行方式"的约束,另外记忆化实现调用状态的顺序是按照实际需求展开,没有大局规划,不利于进一步优化。

一种迭代法与分析边界条件相类似,计算f[x][y]用到状态f[x-1][y-1]f[x-1][y],这两个元素都在f[x][y]的上一行。也就是说,要计算第a行的状态的值,必须先把第a-1行的状态的值计算出来,再利用状态转移方程。可以先把f[1][1]赋值为a[1][1],再从第2行开始按照行递增的顺序计算出每一行的有效状态。

对于题目所给样例,计算f[x][y]的结果如下表所示:

样例对应的f[x][y]

x

y

1 2 3 4 5
1 7 / / / /
2 10 15 / / /
3 18 16 15 / /
4 20 25 20 19 /
5 24 30 27 26 24

该动态规划时间复杂度为O(n^2),不会超时。

代码

方法1

#include
using namespace std;
int a[1005][1005],n,ans;
void dfs(int x,int y,int cur){
	if(x==n){
		ans=max(ans,cur);
		return;
	}
	dfs(x+1,y,cur+a[x+1][y]);
	dfs(x+1,y+1,cur+a[x+1][y+1]);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			cin>>a[i][j];
	dfs(1,1,a[1][1]);
	cout<

方法2

#include
using namespace std;
int a[1005][1005],f[1005][1005],n;
int dfs(int x,int y){
	if(f[x][y]==-1)
		f[x][y]=(x==n?a[x][y]:a[x][y]+max(dfs(x+1,y),dfs(x+1,y+1)));
	return f[x][y];
} 
int main(){
	memset(f,-1,sizeof(f));
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			cin>>a[i][j];
	dfs(1,1);
	cout<

方法3

#include
using namespace std;
int a[1005][1005],f[1005][1005],n,ans;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			cin>>a[i][j];
	f[1][1]=a[1][1];
	for(int i=2;i<=n;i++)
		for(int j=1;j<=i;j++)
			f[i][j]=a[i][j]+max(f[i-1][j-1],f[i-1][j]);
	for(int i=1;i<=n;i++)
		ans=max(ans,f[n][i]);
	cout<

你可能感兴趣的:(c++)