动态规划——经典DP,01背包

本文深入探讨了动态规划的概念,强调它并非单一算法,而是一种解决最优化问题的思路。通过斐波那契数列和数塔问题的例子,解释了动态规划如何避免重复计算,提高效率。接着,文章以01背包问题为实例,展示了动态规划的递推解法。总结了动态规划解决问题的三个关键步骤:识别重叠子问题、确定边界条件和建立状态转移方程。
摘要由CSDN通过智能技术生成

动态规划、斐波那契数列、数塔问题、01背包

动态规划从某种意义上来说不算是一种算法,更像是一种奇思妙想,它没有固定的写法、极其灵活,常常需要根据具体问题具体分析。

什么是动态规划

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

  • DP会将每个求解的子问题的解记录下来,这样,当下一次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算
  • 虽然DP采用上述方式提高了计算效率,但并不能说这就是DP的核心
  • 一般可以使用递归或者地推的写法来实现DP,其中递归写法又常常被成为记忆化搜索

DP的递归(斐波那契数列)

以斐波那契数列问题为例,我们应该考虑的时如何记录子问题的解,避免下次遇到相同的子问题时仍然进行重复计算。

斐波那契数列

定义:
F 0 = 1 , F 1 = 1 , F n = F n − 1 + F n − 2 ( n ≥ 2 ) F_0 = 1,F_1 = 1,F_n = F_{n-1}+F_{n-2} (n \ge 2) F0=1,F1=1,Fn=Fn1+Fn2(n2)

// C简单构造斐波那契数列
int F(int n) 
{
	if(n == 0 || n == 1) 
		return 1;
	else
		return F(n-1) + F(n-2)
}

事实上,这样的递归会涉及到很多重复的计算
比如,当 n = 5时,可以得到F(5) = F(4) + F(3),接下来计算F(4)时
又会有F(4) = F(3) + F(2),F(3)被计算了两次。
当n非常大的时候,复杂度就会变得很大。

所以,为了避免重复计算,我们可以使用DP的思想,创建一个一维数组dp,用以保存已经计算过的结果。
其中dp[n]记录F(n)的结果,并用dp[n]=-1表示当前还没有被计算过,这样的话,就可以在递归当中判断dp[n]是否是-1:

  • 不是-1,说明已经计算过F(n),直接返回dp[n]就是我们需要的结果
  • 是-1,则继续按照递归式进行递归
int dp[MAXN];
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 dp[n];
	}
}

通过记忆化搜索,记录重叠子问题的解,避免了大量的重复计算

DP的递推(数塔问题)

例:如下图所示,将一些数字排成数塔的形状,其中第一层有一个数字,第二层有两个数字……第n层有n个数字。现在要从第一层走到第n层,每次智能走向下一层链接的两个数字中的一个,求最后将路径上所有数字相加后得到的和的最大值
在这里插入图片描述
由题可知,从第一层走到第n层,有很多条路径可以走,如果我们按照统计做法,把每条路径都从头到尾走一遍,再求最大值,无疑会有很多重复的计算,那复杂度就太大了

按照题目的描述,如果开一个二维数组f,其中f[i][j]存放第i层第j个数字
那么就有f[1][1]=5、f[2][1]=8……f[4][4]=6

注意到一个细节:
如果要求出“从位置(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[1][1];

我们把dp[i][j]称为问题的状态,那么上述式子就是该问题的状态转移方程
可以发现,状态dp[i][j]只与第i+1层的状态有关,与其他层的状态无关,这样层号i的状态就总是由层号为i+1的两个子状态得到。
即数塔的最后一层的dp值总是等于元素本身:

dp[n][j]=f[n][j](1<=j<=n)

把这种可以直接确定其结果的部分称为边界,而DP的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组

#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 = 0;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];
		}
	}
	print("%d\n",dp[i][i]);
	return 0;
}

浅浅总结一下吧

经典DP其实可以说是有套路可以走的:

  • 发现重叠子问题
  • 找出边界
  • 求出状态转移方程

典中典——01背包

问题:
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件

如果我们采用暴力枚举的方式,那就会跟我们最初学习数塔问题时的想法一样,将每种情况列举一遍,这样的话他的复杂度无疑是非常糟糕的,所以我们当然会想到使用DP来解决这个问题。

简单分析,容易得出一下两种策略:

  1. 不放入第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,即dp[i-1][v]
  2. 放入第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,即dp[i-1][v-w[i]]+c[i]

所以我们只需要在这两种策略中做一个决策。求出最大的那一个就行:

//状态转移方程
dp[i][v] = max(dp[i-1][v],dp[i-1][v-w[i]]+c[i])

根据我们在数塔问题中的学习,我们可以直接写出他的代码:

// 01背包 经典DP
# include <cstdio>
#include <studio.h>
#include <stdlib.h>

int maxn = 100;
int maxv = 1000;
int w[maxn],c[maxn],dp[maxv];

int main()
{
	int n ,V;
	scanf("%d%d",&n, &V);
	for(int i =1;i<=n;i++)
	{
		scanf("%d", &w[i]);
	}
		for(int i =1;i<=n;i++)
	{
		scanf("%d", &c[i]);
	}
	// 边界
	for(int v =0; v<=V;v++)
	{
		dp[v]=0;
	}
	for(int i =1;i<=n;i++)
	{
		for(int v =V;v>=w[i];v--)
		{
			//状态转移方程
			dp[v] = max(dp[v] , dp[v-w[i]]+c[i[);
		}
	}
	//寻找dp[0...V]中最大的即为答案
	int max = 0;
	for(int v =0;v<=V;v++)
	{
		if(dp[v] > max)
		{
		max = dp[v];
		}
	}
	printf("%d\n",max);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值