动态规划-01背包-详解

动态规划,我都能看懂版

最近在学习动态规划的算法,能力有限,着实是有点难到我了,所以准备把自己的想法记录下来方便以后来复习。

定义

其实,在我看来,动态规划其实和迭代很相似,都是通过小问题一步步求解到最终问题的过程。每一次小问题的解又是下一个问题的必备条件,这样就能把复杂的问题,通过解决一个个小问题而得到最终答案。

最简单的案例(斐波那契数列)

相信斐波那契数列或者说兔子序列大家都不会陌生,从第3个数开始,f(n)=f(n-1) + f(n-2)。那其实很多时候我们都是通过递归来解决问题的。

#递归
#include <iostream>
using namespace std;
int dg(int n) {
	// 触底要开始回归,否则继续递进
	if (n == 1 || n == 2) {
		return 1;
	} else {
		return dg(n - 1) + dg(n - 2);
	}
}

但是,我的小破本本儿通过递归运行求解的话,大概到dg(30)就需要三四秒了,这就让我很好奇,递归那么难以理解的内容,我学会了,结果他的效率就这么低下?
为了找到为什么那么慢,于是我在草纸上画了一下递归的过程!噢!原来递归这么墨迹!例如这里才递进三次,就有四个dg(26)的值重复求解了。
在这里插入图片描述
好好好,那么动态规划算法怎么做呢?这就涉及到动态转移方程和dp数组了。我理解的动态转移方程就是小问题跟大问题的关系,其实我刚刚写的就是动态转移方程。
那么,这里我们还是通过数组的形式再写一遍:
dp[n] = dp[n - 1] + dp[n - 2]
在这里,有一个很重点很重点地方我们需要理解,那就是dp[n]到底代表了什么。在这个题目中,dp[n]代表着斐波那契数列第n个数,是不是很简单?嘿嘿~一会儿你就不这么觉得了。
那么,代码呈上!

#include <iostream>
using namespace std;
int main () {
    // 获取需要求解的是第几个数
    int n;
    cin >> n;
    // 假定我们最多求解第100个数,那么我们定义dp数组
    int dp[101];
    // 这里,我们需要给上初始值
    dp[1] = 1;
    dp[2] = 1;
    // 好了,根据动态转移方程,从第3个元素开始填充dp数组
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
        cout << dp[i] << " ";
    }
    
}

代码输入10的结果
好了,这个例子很简单,主要是学习动态规划的思路。那么接下来就是经典的背包问题。

01背包

题目:
有一个小偷,他带着容量为v的背包去偷东西,摆在他面前的是n样东西(每一个东西都只有一件),它们都有自己的体积和价值,求解小偷能拿取的最大价值。
输入: 第一行有2个整数,分别是v,n 接下来的n行分别是每一样东西的体积和价值、 输入样例:
70 3
71 100
69 1
1 2
输出样例: 3

好了,我已经很努力的简化题目了。这一题乍一看,根本无从下手,但是我想了想,好像是可以有暴力算法的,既然每一件物品都只有一件,那么每一件物品的状态就可以分为:拿或者不拿,那么,我们把n件物品,2^n的情况都遍历一遍,筛去超体积的情况,那然后留下最大值就可以求解到。emmmm,复杂度有点高啊!

动态规划怎么去定义dp数组呢,dp数组又是什么意思呢?这个过程,我觉得是动态规划最难的地方,这里我们需要定义一个二维数组dp[i][j],而dp[i][j]则代表了从第1件物品到第i件物品中选取,装到容积为j的背包里时,最大价值。【这里比较难理解,但是理解了以后其实就解决问题了】。很多大佬都是选择使用表格的形式来解释这个问题,所以我们就也一样!看图!
表格说明
好了,是不是开始头大了,表格有什么用啊!我get不到啊!不着急,看似表格让问题更复杂了,实则不然。我们知道了每一格的意义,那么横向来看,其实每一行都是在决定是否要拿去该行对应的物品。

而这又分为两种情况,第一种是情况是背包容量不够,所以不拿,那么这一格的内容就应该是从第0个物品到第i-1个物品中挑选放到容量为j的背包中的最大价值,这一步的理解很重要!第i个不拿,可不就是从0~i-1个中拿呗!

第二种情况则是,背包容量够了!完了,这又得分两种情况,我是拿还是不拿,那么这个时候就得价值来说话了, 价值更高则拿,否则我就不拿呗。

那么这里就需要有一个计算和判断了。如果,我拿了,那么背包空间必定还得重新分配,分配吗?嘿嘿,不需要,别忘了,之前我们是从空间为1物品为1的情况往下做的,所以剩下空间能拿的最大值已经在表格中了,所以,如果拿的话,总价值应该是该物品的价值(即第i件物品的价值)加上剩余空间能装的最大价值。

不拿的话,和背包容量不够同理。计算完成,那么就需要从两种情况中取价值比较大的那种情况填入即可。

好了,分析了那么久,动态转移方程出来了。
动态转移方程

【觉得啰嗦可以跳过本段】首先,我们来看dp[i - 1][j],根据dp数组的含义,我们理解为容积为j的背包装1~i - 1物件情况下,最高价值。这不就是不拿第i件物品的情况嘛!!接着,我们看dp[i - 1][j - vol[i]],说明一下,vol[i]是指第i件物品所占空间,那么j - vol[i]不就是把第i件物品放进了背包,那么空间减少了,同样的需要加上val[i]即第i件物品的价值。那么dp[i-1][j - vol[i]]这个不就可以理解为背包剩余空间装1 ~ i - 1物品的最高价值。

ok,到目前为止,我们分析完了dp数组的含义和动态转移方程,剩下就是最简单的代码部分了。

#include <iostream>
#include <cstring>
using namespace std;
int main () {
//  有一个小偷,他带着容量为v的背包去偷东西,摆在他面前的是n样东西(每一个东西都只有一件),它们都有自己的体积和价值,求解小偷能拿取的最大价值。
//	输入:
//	第一行有2个整数,分别是v,n
//	接下来的n行分别是每一样东西的体积和价值、
//	输入样例:
//	70 3
//	71 100
//	69 1
//	1 2
//	输出样例:
//	3
	int v, n;
	cin >> v >> n;
	// 定义物品价值、所占空间的数组还有dp数组,数组长度取决于n即物品的件数
	int val[100], vol[100], dp[100][100];
	// 将dp数组全部赋零,因为当背包空间为0,或者从0件物品开始拿,价值最高就是0
	memset(dp, 10000, 0);
	for (int i = 1; i <= n; i++) {
		cin >> vol[i] >> val[i];
	}
	
	// 开始遍历dp数组并开始赋值
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= v; j++) {
			// i代表物品编号,j代表背包空间
			// 做第一层判断,背包空间是否能够装下第i件物品
			if (j >= vol[i]) {
				// 空间够的情况下,取价值高的那情况
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - vol[i]] + val[i]);
			} else {
				// 空间不够,直接选择不拿第i件物品
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	// 输出空间为v,取所有n件物品的值,即题目要求的答案
	cout << dp[n][v];
}

好了,01背包问题,二维数组的解法就完成了。

接下来就是一维数组的解法了。我们知道,二维数组的时候我们需要记录的不仅有背包的容量,还有物品的编号,其实,物品的编号是可以省略的,那么此时数组的下标就只表示了背包的容量。

一维数组的解法其实和二维本质是一样的,只不过节省了一些存储的空间。物品编号虽然不再以下标的形式体现,但是我们写代码的时候依然还是用两层for循环来进行嵌套,第一层循环的变量i依然表示的是物品编号,j依然表示的是背包的容量。

这个时候,我们还需要再回头来看动态转移方程
动态转移方程
我们发现一维数组看似是没有办法来拿到这个dp[i - 1][j]这个数据的,这该怎办?

这时候,我们来解读一下我们的代码

// 开始遍历dp数组并开始赋值
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= v; j++) {
			// i代表物品编号,j代表背包空间
			// 做第一层判断,背包空间是否能够装下第i件物品
			if (j >= vol[i]) {
				// 空间够的情况下,取价值高的那情况
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - vol[i]] + val[i]);
			} else {
				// 空间不够,直接选择不拿第i件物品
				dp[i][j] = dp[i - 1][j];
			}
		}
	}

不难发现,这里的代码是一行一行的进行dp二维数组的填充,我们如果用一维数组的话,dp数组其实代表的是之前二维数组的每一行。

那么我们在一个一个元素填充的时候,未填充的部分就是上一行的内容【这一步很重要,我们需要好好揣摩一下这句话】

所以不拿第i件物品时的最高价值,就是dp[j],因为如果我不修改这里的数值,那么它就是上一行的数值,即dp[i-1][j]的值。

现在需要解决一个问题,那就是dp[i - 1][j - vol[i]] + val[i]这个值该怎样来获取,重点在于dp[i - 1][j - vol[i]],首先因为这是上一行的内容,所以我们得从dp数组这一行没有修改过的地方来拿,又看到这里j要减去第i件物品的占用空间,所以我们第二层循环需要dp数组从后往前来填入。

好了,接下来看代码

#include <iostream>
#include <cstring>
using namespace std;
int main () {
//  有一个小偷,他带着容量为v的背包去偷东西,摆在他面前的是n样东西(每一个东西都只有一件),它们都有自己的体积和价值,求解小偷能拿取的最大价值。
//	输入:
//	第一行有2个整数,分别是v,n
//	接下来的n行分别是每一样东西的体积和价值、
//	输入样例:
//	70 3
//	71 100
//	69 1
//	1 2
//	输出样例:
//	3
	int v, n;
	cin >> v >> n;
	// 定义物品价值、所占空间的数组还有dp数组,数组长度取决于n即物品的件数
	int val[100], vol[100], dp[100] = {0};
	for (int i = 1; i <= n; i++) {
		cin >> vol[i] >> val[i];
	}
	
	// 开始写循环进行数组的赋值
	for (int i = 1; i <= n; i++) {
		for (int j = v; j > 0; j--) {
			// i代表物品编号,j代表背包空间
			// 做第一层判断,背包空间是否能够装下第i件物品
			if (j >= vol[i]) {
				// 空间够的情况下,取价值高的那情况
				dp[j] = max(dp[j], dp[j - vol[i]] + val[i]);
			} else {
				// 空间不够,直接选择不拿第i件物品
				dp[j] = dp[j];
			}
		}
	}
	// 输出空间为v,取所有n件物品的值,即题目要求的答案
	cout << dp[v];
}

这篇是我对01背包的解题详解,可能有些过于详细,读者可能需要自己真正的读读题,写一写代码,理解关键思路。如果有问题,欢迎大家私信交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值