动态规划

动态规划

动态规划:用来解决一类最优化问题的算法思想,将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。

动态规划的递归写法

以Fibonacci数列为例,F0=1, F1=1, Fn=Fn-1+Fn-2
在这里插入图片描述

int F(int n)
{
	if(n == 0 || n == 1)
		return 1;
	else
		return F(n-1) + F(n-2);
}

为了避免重复计算,可以开一个一维数组dp,用以保存已经计算过的结果,其中dp[n]记录F(n)的结果,并用dp[n]=-1表示F(n)当前还没被计算过

int dp[MAXN];

然后就可以在递归当中判断dp[n]是否是-1:如果不是-1,说明已经计算过F(n),直接返回dp[n]就是结果;否则,按照递归式进行递归。

int F(int n)
{
	if(n == 0 || n == 1)
		return 1;
	if(dp[n] != -1) //递归边界
		return dp[n]; //已经计算过,直接返回结果,不再重复计算
	else
	{
		dp[n] = F(n-1) + F(n-2); //计算F(n),并保存至dp[n]
		return F(n-1) + F(n-2); //返回F(n)的结果
}

重叠子问题

如果一个问题可以被分解成若干个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题。动态规划通过记录重叠子问题的解,来使下次碰到相同的子问题时直接使用之前的记录的结果,以此避免大量重复计算。一个问题必须拥有重叠子问题,才能使用动态规划去解决。

动态规划的递推写法

以经典的数塔问题为例,将一些数字排成数塔的形状,其中第一层有一个数字,第二层有两个数字…第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个,问:最后将路径上所有数字相加后得到的和最大是多少?
在这里插入图片描述
按照题目的描述,如果开一个二维数组f,其中f[i][j]存放第i层的第j个数字,那么就有f[1][1]=5,f[2][1]=8,f[2][2]=3,f[3][1]=12,…,f[5][4]=9,f[5][5]=4。

从第一层的5出发,按5->8->7的路线来到7,并枚举从7出发的到达最底层的所有路径。但是,之后当按5->3->7的路线再次来到7时,又会去枚举从7出发的到达最底层的所有路径,这就导致了从7出发的到达最底层的所有路径都被反复地访问,做了许多多余的计算。

事实上,可以在第一次枚举从7出发的到达最底层的所有路径时就把路径上能产生的最大和记录下来,这样当再次访问到7这个数字时就可以获取这个最大值,避免重复计算。

令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和,例如dp[3][2]就是图中的7到最底层的路径最大和。在定义这个数组之后dp[1][1]就是最终想要的答案,现在想办法求出它。

细节:如果求出”从位置(1,1)到达最底层的最大和“dp[1][1],那么一定要先求出它的两个子问题”从位置(2,1)到达最底层的最大和dp[2][1]“和“从位置(2,2)到达最底层的最大和dp[2][2]”,即进行了一次决策:走数字5的左下还是右下。于是dp[1][1]就是dp[2][1]和dp[2][2]的较大值加上5。写出式子就是:
dp[1][1] = max(dp[2][1], dp[2][2]) + f[1][1]

归纳:如果要求出dp[i][j],那么一定要先求出它的两个子问题“从位置(i+1,j)到达最底层的最大和dp[i+1][j]”和“从位置(i+1, j+1)到达最底层的最大和dp[i+1][j+1]”,即进行了一次决策:走位置(i,j)的左下还是右下。于是dp[i][j]就是dp[i+1][j]和dp[i+1][j+1]的较大值加上f[i][j]。写成式子:
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]

把dp[i][j]称为问题的状态,而把上面的式子称为状态转移方程,把状态dp[i][j]转移为dp[i+1][j]和dp[i+1][j+1]。可以发现,状态dp[i][j]只与第i+1层的状态有关,而与其他层的状态无关,这样层号为i的状态就总是可以由层号为i+1的两个子状态得到。那么,如果总是将层号增大,数塔的最后一层的dp值总是等于元素本身,即dp[n][j] == f[n]j,把这种可以直接确定其结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组。

从最底层各位置的dp值开始,不断往上求出每一层各位置的dp值,最后就会得到dp[1][1],即为想要的答案。

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1000;
int f[maxn][maxn], dp[maxn][maxn];
int main()
{
	int n;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= i; j++)
		{
			scanf("%d", &f[i][j]); //输入数塔
		}

	}
	//边界
	for(int j = 1; j <=n; j++)
	{
		dp[n][j] = f[n][j];
	}
	//从第n-1层不断往上计算出dp[i][j]
	for(int i = n - 1; i >= 1; i--)
	{
		for(int j = 1; j <= i; j++)
		{
			//状态转移方程
			dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j];
		}
	}
	printf("%d\n", dp[1][1]); //dp[1][1]即为需要的答案
	return 0;
}
5
5
8 3
12 7 16
4 10 11 6
9 5 3 9 4
44

递归写法与递推写法的区别

1)递推写法的计算方式:自底向上,从边界开始,不断向上解决问题,直到解决了目标问题。
2)递归写法的计算方式;自顶向下,从目标问题开始,将它分解成子问题的组合,直到分解至边界为止。

最优子结构

如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称这个问题拥有最优子结构。最优子结构保证了动态规划中原有的最优解可以由子问题的最优解推导出来。因此一个问题必须拥有最优子结构,才能使用动态规划去解决。

重叠子问题与最优子结构

一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。

  1. 分治与动态规划。
    相同:都将问题分解为子问题,然后合并子问题的解得到原问题的解
    不同:
    1)分治法分解的子问题是不重叠的
    2)动态规划解决的问题拥有重叠子问题。
  2. 贪心与动态规划
    相同:都要求原问题必须拥有最优子结构
    不同:
    1)贪心法:采用类似“自顶向下”,但是并不等待子问题求解完毕后再选择使用哪一个,而是通过一种策略直接选择一个子问题去求解,没被选择的子问题就不去求解,直接抛弃。即,它总是只在上一步选择的基础上继续选择,因此整个过程以一种单链的流水方式进行,显然这种所谓“最优选择”的正确性需要用归纳法证明。
    2)动态规划:都从边界开始向上得到目标问题的解,即,总是会考虑所有子问题,并选择继承能得到最优结果的那个,对暂时没被继承的子问题,由于重叠子问题的存在,后期可能会再次考虑它们,因此还有机会成为全局最优的一部分,不需要放弃。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值