浅谈动态规划——01背包
灌注Franklin_Tse,谢谢喵
本文暂时不谈记忆化搜索
先看例题
(其实就是个加了题目背景的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开心的金明
已经尽量讲得简单一点了,我真的想让你看懂
哪里还看不懂在评论区说一下,等我有空再改
点个赞再走叭

浙公网安备 33010602011771号