经典动态规划:0-1 背包问题

文章目录

  • 经典动态规划:0-1 背包问题
  • 一、题目描述
  • 二、动规标准套路
  • 三、题目描述
  • 四、解法分析
  • 五、优化

一、题目描述

就讨论最常说的 0-1 背包问题,简单描述一下吧:

给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

举个简单的例子,输入如下

N = 3,//物品个数
W = 4//背包载重
wt = [2, 1, 3]//重量
val = [4, 2, 3]//价值

输出

算法返回 6,选择前两件物品装进背包,总重量 3 小于W,可以获得最大价值 6。

题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这也许就是 0-1 背包这个名词的来历。

解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据动态规划套路,直接走流程就行了。

二、动规标准套路

1. 第一步要明确两点,「状态」和「选择

先说状态,如何才能描述一个问题局面?只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题,对不对?所以状态有两个,就是「背包的容量」和「可选择的物品」。

再说选择,也很容易想到啊,对于每件物品,你能选择什么?选择就是「装进背包」或者「不装进背包」嘛。

明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:

for 状态1 in 状态1的所有取值:for 状态2 in 状态2的所有取值:for ...dp[状态1][状态2][...] = 择优(选择1,选择2...)

2. 第二步要明确dp数组的定义

dp数组是什么?其实就是描述问题局面的一个数组。换句话说,我们刚才明确问题有什么「状态」,现在需要用dp数组把状态表示出来。

首先看看刚才找到的「状态」有两个也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。

dp[i][w]dp[i][w]dp[i][w]的定义如下:对于前i个物品,当前背包的容量为www,这种情况下可以装的最大价值是dp[i][w]dp[i][w]dp[i][w]

比如说,如果 dp[3][5]dp[3][5]dp[3][5] === 666,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6

根据这个定义,我们想求的最终答案就是dp[N][W]dp[N][W]dp[N][W]。base case 就是dp[0][..]dp[0][..]dp[0][..] = dp[..][0]dp[..][0]dp[..][0] === 000,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。

细化上面的框架:

int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0for i in [1..N]:for w in [1..W]:dp[i][w] = max(把物品 i 装进背包,不把物品 i 装进背包)
return dp[N][W]

3. 第三步,根据「选择」,思考状态转移的逻辑

简单说就是,上面伪码中「把物品i装进背包」和「不把物品i装进背包怎么用代码体现出来呢

这一步要结合对dp数组的定义和我们的算法逻辑来分析

先重申一下刚才我们的dp数组的定义

dp[i][w]dp[i][w]dp[i][w]表示:对于前i个物品,当前背包的容量为w时,这种情况下可以装下的最大价值是dp[i][w]dp[i][w]dp[i][w]。

如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]dp[i][w]dp[i][w]应该等于dp[i−1][w]dp[i-1][w]dp[i−1][w]。你不装嘛,那就继承之前的结果

如果你把这第i个物品装入了背包,那么dp[i][w]dp[i][w]dp[i][w]应该等于dp[i−1][w−wt[i−1]]dp[i-1][w-wt[i-1]]dp[i−1][w−wt[i−1]] +++ val[i−1]val[i-1]val[i−1]。

首先,由于i是从 1 开始的,所以对val和wt的取值是i-1

而dp[i−1][w−wt[i−1]]dp[i-1][w-wt[i-1]]dp[i−1][w−wt[i−1]]也很好理解:你如果想装第i个物品,你怎么计算这时候的最大价值?换句话说,在装第i个物品的前提下,背包能装的最大价值是多少?

显然,你应该寻求剩余重量w−wt[i−1]w-wt[i-1]w−wt[i−1]限制下能装的最大价值,加上第i个物品的价值val[i−1]val[i-1]val[i−1],这就是装第i个物品的前提下,背包可以装的最大价值

综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码:

for i in [1..N]:for w in [1..W]:dp[i][w] = max(dp[i-1][w],dp[i-1][w - wt[i-1]] + val[i-1])
return dp[N][W]

4. 最后一步,把伪码翻译成代码,处理一些边界情况

int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {// vector 全填入 0,base case 已初始化vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));for (int i = 1; i <= N; i++) {for (int w = 1; w <= W; w++) {if (w - wt[i-1] < 0) {// 当前背包容量装不下,只能选择不装入背包dp[i][w] = dp[i - 1][w];} else {// 装入或者不装入背包,择优dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], dp[i - 1][w]);}}}return dp[N][W];
}

三、题目描述


对于这个问题,看起来和背包没有任何关系,为什么说它是背包问题呢?

首先回忆一下背包问题大致的描述是什么:

给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

那么对于这个问题,我们可以先对集合求和,得出sum,把问题转化为背包问题:

给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满

你看,这就是背包问题的模型,甚至比我们之前的经典背包问题还要简单一些,下面我们就直接转换成背包问题,开始套前面的背包问题框架即可。

四、解法分析

1. 第一步要明确两点,「状态」和「选择」

状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。

2. 第二步要明确dp数组的定义

按照背包问题的套路,可以给出如下定义:

dp[i][j]dp[i][j]dp[i][j] === xxx表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满

比如说,如果dp[4][9]dp[4][9]dp[4][9] === truetruetrue,其含义为:对于容量为 9 的背包,若只是用前 4 个物品,可以有一种方法把背包恰好装满

或者说对于本题,含义是对于给定的集合中,若只对前 4 个数字进行选择,存在一个子集的和可以恰好凑出 9。

根据这个定义,我们想求的最终答案就是dp[N][sum/2]dp[N][sum/2]dp[N][sum/2],base case 就是dp[..][0]dp[..][0]dp[..][0] === truetruetrue和dp[0][..]dp[0][..]dp[0][..] === falsefalsefalse,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包

3. 第三步,根据「选择」,思考状态转移的逻辑

回想刚才的dp数组含义,可以根据「选择」对dp[i][j]dp[i][j]dp[i][j]得到以下状态转移:

  • 如果不把nums[i]nums[i]nums[i]算入子集,或者说你不把这第i个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i−1][j]dp[i-1][j]dp[i−1][j],继承之前的结果
  • 如果把nums[i]nums[i]nums[i]算入子集,或者说你把这第i个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i−1][j−nums[i−1]]dp[i - 1][j-nums[i-1]]dp[i−1][j−nums[i−1]]。
  • 首先,由于i是从 1 开始的,而数组索引是从 0 开始的,所以第i个物品的重量应该是nums[i−1]nums[i-1]nums[i−1],这一点不要搞混。
  • dp[i−1][j−nums[i−1]]dp[i - 1][j-nums[i-1]]dp[i−1][j−nums[i−1]]也很好理解:你如果装了第i个物品,就要看背包的剩余重量j−nums[i−1]j - nums[i-1]j−nums[i−1]限制下是否能够被恰好装满。
  • 换句话说,如果j−nums[i−1]j - nums[i-1]j−nums[i−1]的重量可以被恰好装满,那么只要把第iii个物品装进去,也可恰好装满jjj的重量;否则的话,重量jjj肯定是装不满的。

4. 最后一步处理一些边界情况

bool canPartition(vector<int>& nums) {int sum = 0;for (int num : nums) sum += num;// 和为奇数时,不可能划分成两个和相等的集合if (sum % 2 != 0) return false;int n = nums.size();sum = sum / 2;vector<vector<bool>> dp(n + 1, vector<bool>(sum + 1, false));// base casefor (int i = 0; i <= n; i++)dp[i][0] = true;for (int i = 1; i <= n; i++) {for (int j = 1; j <= sum; j++) {if (j - nums[i - 1] < 0) {// 背包容量不足,不能装入第 i 个物品dp[i][j] = dp[i - 1][j]; } else {// 装入或不装入背包dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]];}}}return dp[n][sum];
}

五、优化

再进一步,是否可以优化这个代码呢?注意到dp[i][j]dp[i][j]dp[i][j]都是通过上一行dp[i−1][..]dp[i-1][..]dp[i−1][..]转移过来的,之前的数据都不会再使用了。

所以,我们可以进行状态压缩,将二维dp数组压缩为一维,节约空间复杂度

bool canPartition(vector<int>& nums) {int sum = 0, n = nums.size();for (int num : nums) sum += num;if (sum % 2 != 0) return false;sum = sum / 2;vector<bool> dp(sum + 1, false);// base casedp[0] = true;for (int i = 0; i < n; i++) for (int j = sum; j >= 0; j--) if (j - nums[i] >= 0) dp[j] = dp[j] || dp[j - nums[i]];return dp[sum];
}

这就是状态压缩,其实这段代码和之前的解法思路完全相同,只在一行dpdpdp数组上操作,iii每进行一轮迭代,dp[j]dp[j]dp[j]其实就相当于dp[i−1][j]dp[i-1][j]dp[i−1][j],所以只需要一维数组就够用了

唯一需要注意的是j应该从后往前反向遍历,因为每个物品(或者说数字)只能用一次,以免之前的结果影响其他的结果。

至此,子集切割的问题就完全解决了,时间复杂度 O(n∗sum)O(n*sum)O(n∗sum),空间复杂度 O(sum)O(sum)O(sum)。

经典动态规划:0-1 背包问题相关推荐

  1. o-1背包问题迭代_经典动态规划:01背包问题的变体

    点击上方蓝字设为星标 东哥带你手把手撕力扣~ 作者:labuladong   公众号:labuladong 若已授权白名单也必须保留以上来源信息 上篇文章 经典动态规划:0-1 背包问题 详解了通用的 ...

  2. 动态规划——0/1背包问题(全网最细+图文解析)

    ✨动态规划--0/1背包问题(全网最细+图文解析) 作者介绍:

  3. 动态规划0—1背包问题

    动态规划0-1背包问题 Ø    问题描写叙述:    给定n种物品和一背包.物品i的重量是wi,其价值为vi,背包的容量为C.问应怎样选择装入背包的物品,使得装 入背包中物品的总价值最大? Ø   ...

  4. 回溯法经典例题--0/1背包问题--C语言

    问题描述:         设n个物品的编号为0~n-1,重量和价值分别用数组w[]与v[]存放,背包限制重量用W表示,X[]存放最优解,x[i]的值为0.1分别表示物品i不在.在背包内. 求解:   ...

  5. 背包问题动态规划matlab,01背包问题动态规划详解

    计算机算法分析考试:动态规划0-1背包问题,怎么算她说她没醉,却一直摇摇晃晃掉眼泪:你说你爱她,却从未想过给她一个家. 要考试了,老师给划重点有一题:动态规划0-1背包问题,怎么算. 怎么理问题描述: ...

  6. 0/1背包问题——动态规划、回溯、分支限界法对比

    0/1背包问题--动态规划.回溯.分支限界法对比 2017.12.19 20:42:02 字数 3713 阅读 2820 目录 1.问题描述 1.1 问题描述 1.2 问题的数学表示(规划类问题,此种 ...

  7. 0/1背包问题——动态规划方法

    1.定义 动态规划:把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解. 2.求解步骤 (1)找到状态转化条件 (2)归纳状态转移方程 (3)定义初始条件值 3.实例解析--0/1背包 ...

  8. 【动态规划】0/1背包问题

    问题 H: [动态规划]0/1背包问题 时间限制: 1 Sec  内存限制: 64 MB 提交: 152  解决: 95 [提交] [状态] [讨论版] [命题人:admin] 题目描述 张琪曼和李旭 ...

  9. 动态规划之0/1背包问题(动态规划入门)

    动态规划很早以前就接触过但是因为太晦涩难懂一下子到现在才开始真正的学习到其中的道理,0/1背包问题是动态规划的入门类问题 比较好理解 首先我们要知道动态规划是用于解决最优解的问题 它是一种思想而不是一 ...

最新文章

  1. python迭代计算_如何在Python中迭代坐标列表并计算它们之间的距离
  2. 5G UPF + MEC 的部署位置、场景与模式
  3. Problem M. Mediocre String Problem(Z 函数 + PAM)
  4. 使用mongoose 在 Node中操作MongoDB数据库
  5. Eclipse启动时指定jdk版本
  6. (hdu 1568) Fibonacci
  7. SQL2008 行锁使用RowLock
  8. Oracle EBS R12 电子技术参考手册 - eTRM (电子文档)
  9. JavaScript学习手册三:JS运算符
  10. 网站漏洞修复之苹果cms电影系统
  11. gitbook 入门教程之还在搞公众号互推涨粉?gitbook 集成导流工具,轻轻松松躺增粉丝!...
  12. 别在直接背3500个英语单词了,支你一招,看过来
  13. python用谷歌内核制作浏览器_用cef Python打造自己的浏览器
  14. 多穿立体库系统四向车PLC流程控制
  15. 【C++】C++静态库和动态库的区别
  16. python异常处理_Python异常处理
  17. 为什么不能结账_为什么其他结帐行总是移动得更快
  18. CTF解题记录-Misc-“短信”
  19. web端加载百度地图和天地图
  20. annotations are not allowed here

热门文章

  1. 09 Softmax 回归 + 损失函数 + 图片分类数据集【动手学深度学习v2】
  2. 201124阶段二sqlite3 API
  3. C++17中那些值得关注的特性
  4. 【翻译】Nginx的反向代理
  5. CCF201409-5 拼图(30分)
  6. 合法练习黑客技术?这15个网站也许可以帮到你
  7. ubuntu编译qemu报错:‘ERROR: DTC (libfdt) version = 1.4.0 not present.’
  8. 使用反射把用户控件(ASCX)传至网页(ASPX)
  9. java集合概念初步介绍
  10. 《解剖PetShop》系列之二