动态规划(DP)算法题一

问题描述与状态定义

数字三角形问题。 有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数,如图所示。
请添加图片描述
从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来。如何走才能使得这个和尽量大?

分析

首先我们遇到这种问题的时候,先对问题进行抽离出本源,再去对问题求解。
针对这道题目,我们读完题目大概了解道: 我们需要对图里面的元素进行相应的操作,使得连续的元素之和达到最大。这个时候,我们可以将图中元素抽离为二维数组里面的每一个元素。
如图所示
在这里插入图片描述
编号规则:第一行第一个为a[1][1],第二行第一个为a[2][1],第二行第二个为a[2][2]…依次类推。

这里的二维数组a[i][j]是编号为i,j里面的数,而二维数组D[i][j]是编号为i,j的这个节点下面最大的和。

我们通过图可以清楚的了解到这是一类最优子问题,在这里需要用到动态规划的相关问题了,而对于动态规划而已,首先最关键的就是状态和状态转移方程,在这里可以知道
状态: (i,j)格子的最优解就是当前格子的元素加上“从(i,j)格子出发的最大的和”
状态转移方程:D[i][j]= a[i][j] + max{D[i+1][j] , D[i+1][j+1]}
这里的D[i+1][j]和D[i+1][j+1]分别是a[i][j]的左右元素。

记忆化搜索与递推

有了状态转移方程,那么应该怎么计算呢?

方法一: 递归方法

int solve(int i, int j)
{
	return a[i][j] + ( i == n ? 0 : max( solve(i+1,j),solve(i+1,j+1) ));
}

这里的递归方法主要就是在状态转移方程下增加了边界条件: 当遍历到最下面的一行时,使返回值为0, 这里就是用三目运算符实现的。

这里是完整的代码。

#include <iostream>
using namespace std;

int a[100][100]={0};
int b[100][100]={-1};
int n;
int solve(int i, int j);

int main()
{
	cin>>n;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= i; j++)
			cin>>a[i][j];
	cout<<solve(1,1);
}

int solve(int i, int j)
{
	return a[i][j] + ( i == n ? 0 : max( solve(i+1,j),solve(i+1,j+1) ));
}

输入:
4
1
3 2
4 10 1
4 3 2 20
预期输出结果:
24
在这里插入图片描述
这里通过了该组样例。

方法二: 递推计算

由于在第一种方法里面,会重复计算入度为2的节点,然后考虑到树的节点N和树的高度h之间的关系是指数关系,随着高度的增加,节点N会跟着爆炸式增长,那么原本N的二次方的时间复杂度就会变成2的N次方的时间复杂度。会大大增加,那么就会有大量的不必要的运算。所以第二种方法就是为了避免了这种情况的出现。

int i,j;
for(j = 1; j <= n;j++)
	d[n][j] = a[n][j];
for(i = n-1; i >= 1; i--)
	for(j = 1; j <= i; j++)
		d[i][j] = a[i][j] + max(d[i+1][j],d[i+1][j+1]);

这里的二位数组d是最后得到的结果数组,a为输入的数组。

完整的代码如下所示:

#include <iostream>
using namespace std;

int main()
{
	int a[100][100] = {0};
	int d[100][100] = {-1};
	int n;
	cin>>n;
	
	for(int i = 1; i <= n; i++ )
		for(int j = 1; j <= i; j++)
			cin>>a[i][j];
	
	int i,j;
	for(j = 1; j <= n;j++)
		d[n][j] = a[n][j];
	for(i = n-1; i >= 1; i--)
		for(j = 1; j <= i; j++)
			d[i][j] = a[i][j] + max(d[i+1][j],d[i+1][j+1]);
		
	cout<<d[1][1];
}

输入:
4
1
3 2
4 10 1
4 3 2 20
预期输出结果:
24
在这里插入图片描述
程序通过了该组样例。

方法三: 记忆化搜索

这个方法就是在第一种方法的基础上做出一点变化,在d的初始化时,给出初始值-1,由于计算过的d不太可能为负数,所以这也就成为了作为唯一判断的标准。

int solve(int i, int j)
{
	if(d[i][j] > 0) return d[i][j];
	return a[i][j] + ( i == n ? 0 : max(solve(i+1,j+1),solve(i+1,j)));
}

完整代码:

#include <iostream>
using namespace std;

int a[100][100]={0};
int d[100][100]={-1};
int n;
int solve(int i, int j);

int main()
{
	cin>>n;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= i; j++)
			cin>>a[i][j];
	cout<<solve(1,1);
}

int solve(int i, int j)
{
	if(d[i][j] > 0) return d[i][j];
	return a[i][j] + ( i == n ? 0 : max(solve(i+1,j+1),solve(i+1,j)));
}

在这里插入图片描述
该组数据通过了样例。

总结

最终用了三种方法得到了结果,分别是递归,递推,记忆性递归
后面两种方法都是在第一种方法的基础上得到的,并且都是旨在改进时间复杂度,建议把第一种弄懂,因为只要第一种懂了,那么第二种和第三种就都水到渠成了。

那么,小编分享的动态规划的题目就讲到这里了。请期待下一次的更新啦!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值