浅谈动态规划——01背包

灌注Franklin_Tse,谢谢喵

本文暂时不谈记忆化搜索

先看例题

P1048采药

(其实就是个加了题目背景的01背包板子题)

我知道你可能不想读题,所以我把题意写在这里了

题意

你总共有T的时间

有n个物品,第 i 个物品的价值为w[i],拿走它消耗的时间为v[i],且每个物品只能拿一次

计算出能拿取的物品的最大总价值

我猜你会这样想

最大总价值?我每次都拿性价比最高的物品不就好了?

哦,我真是个天才!

然后开心地写了个贪心,成功爆零

其实很容易造一组数据来证明贪心不是正解,比如这样:


有4个物品,背包体积为19

编号 1 2 3 4
消耗时间 7 6 7 6
价值 9 5 8 6

如果用贪心,很明显会选择1和3,但最优解很明显是1、2、4

为什么呢?

贪心选取的是当前条件下的下一步的最优解,并不是考虑问题的每一步,这就很容易造成一部分空间浪费掉

所以贪心通常不是整个问题的最优解

正片开始

二维数组dp解法

分析

对于每一个物品,都只有取和不取两种选择,最优解一定存在于n次选择中(感觉像一句废话)

要得到第n个物品拿或不拿哪种更优,就需要直到第n - 1个物品拿或不拿哪种更优

何也?

因为取第n个物品时一定已经确定了前n - 1个物品的情况(拿或不拿),所以在前n - 1个物品取最优解时再取第n个物品一定优于前n - 1个物品不是最优解的情况


简单分析完原理之后,就可以对这个问题再进行一次转换:

对于第 i 个物品的选择(拿或不拿)可以变成这样:

1.拿:用T - v[i]时间拿前i - 1个

2.不拿:用T时间拿前i - 1个


我猜你又开始看不懂了

你可能会有这样的 疑问:为什么拿第 i 个物品是用T - v[i]时间拿前i - 1个?

还记着吗,v[i]是拿走第 i 个物品消耗的时间

这里就相当于把第 i 个物品消耗的时间提前先拿出来,用剩下的时间取选择其余i - 1个物品,这样才能保证第 i 个物品能拿走


总结一下,求n个物品的最优解是必须知道用T时间选n - 1种和用T - v[i]选n - 1种哪个更优

但是我们其实并不知道在当前物品的后面有几个物品,因而无法判断要不要拿,那么我们不妨进行一下枚举,只要产生了更优的情况,就把它存下来

到这里就可以看出,在整个过程中,我们既要考虑每一种物品,又要考虑每一种物品在时间允许范围内的解,这样我们就可以用一个二维数组存储中间过程,也就是这样:

(这个人终于扯完了)

1.确定状态:

设状态dp[i][j]为:到第i个物品为止,花费j时间,能拿到的物品的最大价值

2.确定转移方程

对于第i个物品,有拿和不拿两种选择

拿:

dp[i][j] = dp[i - 1][j - v[i]] + w[i];

不拿:

dp[i][j] = dp[i - 1][j];

最终结果要求最大值,所以只需要对这二者取最大值,也就是这样:

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - t[i]] + v[i]);

整理一下,就能得到核心die码

for(int i = 1; i <= n; ++ i)//枚举n个物品
	for(int j = 0; j <= T; ++ j)//枚举时间
        //判断一下,避免越界
        //如果时间够用,选择最优解
		if(j >= v[i])dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
		//时间不够,只能不拿
		else dp[i][j] = dp[i - 1][j];

到这就结束了吗?

如果物品数量和总时间的数量级分别到达1e4,则二维数组的数量级会到达1e8,直接寄

所以要怎么对空间复杂度进行优化呢?

自己用笔算几组数据,你可能会发现一个特点:

计算数组的第 i 行数据只用到了第i - 1行和第 i 个物品

也就是说,第i - 2行以及之前的部分都用不到了,我们能不能想一种方式把这些空间全都优化掉?

于是就有了第一种优化:

滚动数组优化

滚动数组,顾名思义就是将数组循环使用

比如在这个问题中,我们每一次计算都使用数组的第i - 1行来计算第 i 行,也就是对于任意一行,仅与其上一行有关,于是就想到只开两行,交替使用

for(int i = 1; i <= M; ++ i)
{
	int x = i & 1;//i & 1和i % 2一样,位运算可能会稍微快一点
	for(int j = 0; j <= T; ++ j)
        //这里把原来的所有i换成x
        //i - 1换成x ^ 1代表另一行
		if(j >= t[i])dp[x][j] = max(dp[x ^ 1][j], dp[x ^ 1][j - v[i]] + w[i]);
		else dp[x][j] = dp[x ^ 1][j];
}
cout << dp[M & 1][T] << '\n';

这个好好看看,比较常用


前面的做法都存在一个复制的过程,把上一行的数据复制到当前行,然后再加上当前物品选择最优解

那么有没有方法能够把这个复制的过程替换掉?

于是就有了这个:

一维数组最终优化

只用一行,去掉复制这一项操作

枚举物品时不变

枚举空间改为反向,执行条件为j >= v[i],防止越界

基本思想:让 j 反着走,未经过的部分是上一行,经过的是下一行

for(int i = 1; i <= n; ++ i)
	for(int j = T; j >= v[i]; j --)
		dp[j] = max(dp[j], dp[j - v[i]] + w[i]);

看完了再自己试试这道题P1060开心的金明

已经尽量讲得简单一点了,我真的想让你看懂

哪里还看不懂在评论区说一下,等我有空再改

点个赞再走叭

posted @ 2023-10-24 23:07  ShadowDream  阅读(41)  评论(0)    收藏  举报