动态规划算法-07背包问题进阶
简介
我们在本专栏之前的文章介绍了基础的01背包问题及其解题思路,本文我们将讲述其拓展题型,也就是完全背包问题和多重背包问题。
01背包问题
首先,我们先来简单回顾一下经典的01背包问题,关于01背包的详细讲解可以参考我的博客。
给定NNN件物品和一个容量为WWW的背包,第iii件物品的价值为viv_ivi,重量为wiw_iwi,求解哪些物品装入背包使得这些物品的重量不超过背包容量且价值总和最大。
这是最基础的背包问题,它的特点是每种物品仅有一件,你只能选或者不选,因此叫做01背包问题。我们定义问题的状态f[i,j]f[i, j]f[i,j]表示前iii件物品放入容量为jjj的背包可以获得的最大价值,显然其状态转移方程如下。
f[i,j]=max{f[i−1,j],f[i−1,j−wi]+vi}f[i, j] = max\{ f[i-1, j], f[i-1, j-w_i] + v_i \} f[i,j]=max{f[i−1,j],f[i−1,j−wi]+vi}
这个方程非常重要,几乎所有的背包问题都可以在这个方程上找到影子。因此我们不妨来分析一下:
将前iii件物品放入容量为jjj的背包中,这个子问题,若只考虑第iii件物品的策略(即选或不选),那么就可以转化为一个只牵扯前i−1i-1i−1件物品的问题。若不选择第iii件物品,那么问题转化为“前i−1i-1i−1件物品放入容量为jjj的背白中”;若选择第iii件物品,那么问题转化为“前i−1i-1i−1件物品放入剩余容量为j−wij-w_ij−wi的背包中”,此时获得的最大价值为f[i−1][j−wi]f[i-1][j-w_i]f[i−1][j−wi]加上放入第iii件物品的价值viv_ivi。当然,这两种情况还需要考虑第iii件物品能否放得进背包才可。
完全背包问题
问题描述
完全背包问题描述如下。
有NNN种物品以及一个容量为WWW的背包,每种物品都有无限件,第iii种物品的重量为wiw_iwi且价值为viv_ivi。求解将哪些物品放入背包可以使得这些物品的重量不超过背包容量且价值总和最大。
基本思路
这个问题很类似于01背包问题,不同的是每种物品不是一件而是无数件,也就是说,从每种物品的角度看,对其的策略并非取还是不取两种,而是取0件、取1件…直至取⌊W/wi⌋\lfloor W / w_i \rfloor⌊W/wi⌋件。如果按照01背包的思路,记f[i,j]f[i, j]f[i,j]为前iii种物品恰放入容量为jjj的背包中的最大价值,仍旧可以按照每种物品不同的策略写出状态转移方程,如下。
f[i,j]=max{f[i−1,j−kwi]+kvi∣0⩽kwi⩽j}f[i, j] = max\{ f[i-1, j-kw_i] + k v_i | 0 \leqslant kw_i \leqslant j \} f[i,j]=max{f[i−1,j−kwi]+kvi∣0⩽kwi⩽j}
这和01背包一样有O(WN)O(WN)O(WN)个状态需要求解,但是求解每个状态的时间不再是常数级别的了,求解状态f[i,j]f[i, j]f[i,j]的时间为O(jwi)O(\frac{j}{w_i})O(wij),总的复杂度可以认为是O(NW∑Wwi)O(NW\sum\frac{W}{w_i})O(NW∑wiW),显然这个复杂度是比较大的。
这个思路源于01背包问题的基本思路,这说明01背包问题确实是很重要的,可以推广到其他类型的背包问题上,但是这个复杂度是需要改进的。
简单优化
完全背包有一个简单有效的优化,具体而言,若两件物品iii、jjj满足wi⩽wjw_i \leqslant w_jwi⩽wj且vi⩾vjv_i \geqslant v_jvi⩾vj,那么物品jjj可以直接去掉,无需考虑。 这个策略的正确性是显而易见的,任何情况下都可以将价值小重量大的jjj换为物美价廉的iii,得到的方案至少不会更差。对于随机生成的数据,这个方法会大大减少物品的件数,加快速度。但是,这种策略并不能改善最坏情况的复杂度,因为精心设计的数据可能一件物品也去不掉。
这个优化的思路可以简单的O(N2)O(N^2)O(N2)实现。而且背包问题有种不错的方法:首先去除费用大于背包容量WWW的物品,然后使用类似计数排序的做法,计算出重量相同的物品中价值最高的是哪个,在O(W+N)O(W+N)O(W+N)的时间内可以完成这个优化。
问题转化求解
01背包是最基本的背包问题,可以考虑将完全背包问题转化为01背包问题来求解。
最直观的做法是,考虑到第iii种物品最多选⌊W/wi⌋\lfloor W / w_i \rfloor⌊W/wi⌋,于是可以将第iii种物品转化为⌊W/wi⌋\lfloor W / w_i \rfloor⌊W/wi⌋件费用及价值均不变的物品,然后求解这个01背包问题。虽然这个简单粗暴的做法并没有改进时间复杂度,但是指明了完全背包转化为01背包问题的思路:将一种物品拆分为多个只能选0件或者1件的01背包中的物品。
更加高效的转化方法为:把第iii种物品拆分为重量为wi2kw_i2^kwi2k、价值为vi2kv_i2^kvi2k的若干件物品,其中kkk取遍满足wi2k⩽Ww_i2^k \leqslant Wwi2k⩽W的非负整数。这是二进制的思想,背后的原因是,不管最优策略选择了几件第iii种物品,其件数写成二进制之后,总可以表示为若干个2k2^k2k件物品的和。这样一来就把每种物品拆分为O(log⌊W/wi⌋)O(log\lfloor W / w_i \rfloor)O(log⌊W/wi⌋)件物品,是一个较大的改进。
O(WN)O(WN)O(WN)的算法
直接看这个算法的伪代码。
f[0...W] <--- 0
for i <--- 1 to Nfor j <--- w_i to Wf[j] <--- max(f[j], f[j-w_i] + v_i)
我们发现一个有趣的地方,那就是这个伪代码与01背包问题的伪代码只有jjj的循环次序不一样。为什么这个算法是可行的呢?这里我们不妨回忆一下为什么01背包中对于jjj的遍历必须要按照递减的顺序进行,这其实是为了保证第iii次循环的状态f[i,j]f[i, j]f[i,j]是由状态f[i−1,j−wi]f[i-1,j-w_i]f[i−1,j−wi]递推而来。换句话说,这正是为了保证每件物品只选了一次啊,保证在考虑“选入第iii件物品”这件策略时,依据的时一个绝无已经选入第iii件物品的子结果f[i−1,j−wi]f[i-1, j-w_i]f[i−1,j−wi]。而现在,完全背包的特点恰好是每种物品可以选择无数件,所以在考虑“加选意见第iii种物品”这种策略时,却正需要一个可能已选入第iii种物品的子结果f[i,j−wi]f[i, j-w_i]f[i,j−wi],所以就可以并且必须采用jjj的递增顺序循环。
当然,正常的这个算法可以由另外的思路得出,如将基本思路种求解f[i,j−wi]f[i, j-w_i]f[i,j−wi]的状态转移方程显式写出来,代入原方程,会发现该方程可以等价变为下面的形式,用一维数组实现这个递推就得到了上述伪代码。
f[i,j]=max(f[i−1,j],f[i,j−wi]+vi)f[i, j] = max(f[i-1, j], f[i, j-w_i] + v_i) f[i,j]=max(f[i−1,j],f[i,j−wi]+vi)
实例分析
我们以Leetcode 322. 零钱兑换为例,其题目如下。
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
这题其实就是个完全背包问题,转化一下就是,这里的硬币就是物品,这里的总金额就是背包容量,目标为最小的硬币个数。不过由于这里不是要求不超过背包容量,而是刚好装满背包,因此初始化dp数组需要使用无穷大来进行。
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:dp = [float('inf')] * (amount + 1)dp[0] = 0for i in range(len(coins)):for j in range(coins[i], amount+1):dp[j] = min(dp[j], dp[j-coins[i]] + 1)return dp[-1] if dp[-1] != float('inf') else -1
多重背包问题
问题描述
有NNN种物品和一个容量为WWW的背包,第iii种物品最多有mim_imi件可用,每件重量为wiw_iwi且价值为viv_ivi。求解将哪些物品装入背包可以使得这些物品不超过背包容量且价值总和最大。
基本思路
这道题和完全背包问题是很类似的,递推方程也只需要略微修改即可。因为对于第iii种物品有mi+1m_i + 1mi+1种策略:取0件、取1件…取mim_imi件,令f[i,j]f[i, j]f[i,j]表示前iii种物品恰放入一个容量为jjj的背包的最大价值,则状态转移方程如下:
f[i,j]=max{f[i−1,j−k∗wi]+k∗vi∣0⩽k⩽mi}f[i, j] = max \{ f[i-1, j-k*w_i] + k * v_i | 0 \leqslant k \leqslant m_i \} f[i,j]=max{f[i−1,j−k∗wi]+k∗vi∣0⩽k⩽mi}
复杂度为O(W∑mi)O(W\sum m_i)O(W∑mi)。
问题转化求解
转化为01背包问题是一个常规的做法:把第iii种物品换位mim_imi件01背包中的物品,则得到了物品数为∑mi\sum m_i∑mi的01背包问题。直接求解这个问题,复杂度依然是O(W∑mi)O(W\sum m_i)O(W∑mi)。我们想要优化这个复杂度,借助二进制的思路,我们得到下面的策略:将第iii种物品分成若干件01背包中的物品,其中每件物品有一个系数。这件物品的重量和价值均是原来的重量和价值乘以这个系数。令这些系数分别为111,222,222^222 … 2k−12^{k-1}2k−1, mi−2k+1m_i-2^k + 1mi−2k+1且kkk是满足mi−2k+1>0m_i-2^k+1 > 0mi−2k+1>0的最大整数。例如,如果mim_imi为13,那么k=3k=3k=3,这种最多取13件的物品就被分成系数分别为1,2,4,6的四件物品。分成的这几件物品的系数和为mim_imi,表明不可能取多于mim_imi件的第iii种物品。另外,这种方法也能保证对于000…mim_imi间的每一个整数,均可以用若干个系数的和表示。
这样,第iii种物品分成了O(logmi)O(logm_i)O(logmi)种物品,将原问题转化为了复杂度O(W∑logmi)O(W\sum logm_i)O(W∑logmi)的01背包问题,这是很大的改进,处理一件多重背包中物品的伪代码如下。
def multiple_pacck(f, w_i, v_i, m_i)if w_i * m_i >= Wcomplete_pack(f, w_i, v_i)return k <-- 1while k < m_izeroone_pack(k*w_i, k*v_i)m_i <-- m_i - kk <--- 2kzeroone_pack(w_i*m_i, v_i * m_i)
O(WN)O(WN)O(WN)的算法
当问题是“每种有若干件的物品能否填满给定容量的背包”,只须考虑填满背包的可行性,不需要考虑每件物品的价值时,多重背包问题同样有O(WN)O(WN)O(WN)复杂度的算法。
下面介绍一种较为简单的O(WN)O(WN)O(WN)解多重背包的算法,它的基本思想是这样的:设f[i,j]f[i, j]f[i,j]表示“用了前iii种物品填满容量为jjj的背包后,最多还剩下几个第iii种物品可用”,如果f[i,j]=−1f[i, j]=-1f[i,j]=−1则说明这种状态不可行,若可行应满足0⩽f[i,j]⩽mi0 \leqslant f[i, j] \leqslant m_i0⩽f[i,j]⩽mi。
递推求f[i,j]f[i, j]f[i,j]得伪代码如下:
f[0,1... W] < --- -1
f[0, 0] <--- 0
for i <--- 1 to Nfor j <--- 0 to Wif f[i-1, j] >= 0f[i, j] = m_ielsef[i, j] = -1for j <--- 0 to W - w_iif f[i, j] > 0f[i, j+w_i] <--- max{f[i, j+w_i], f[i, j] - 1}
最终得f[N][0...W]f[N][0...W]f[N][0...W]就是多重背包可行性问题的答案。
实例分析
力扣目前我还没有发现明显的多重背包类的题目,后续我会补充。
总结
本文介绍了01背包的两种变种,即完全背包和多重背包,这是非常值得思考的两种题型,本文的内容参考了背包九讲,欢迎支持原作者。
动态规划算法-07背包问题进阶相关推荐
- 动态规划算法-03背包问题
背包问题 简介 背包问题一个很著名的动态规划问题,也称为0-1背包问题,很多题都是以此为模板进行魔改的. 问题描述 有N个重量为w1,w2,w3,,,wn且价值为v1,v2,v3,,,vn的物品和一个 ...
- 动态规划算法解决背包问题
一.动态规划算法概述 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从子问题解得到原问题解. 但是经分解得到的子问题往往不是互相独立的.不同子问题的数目常常 ...
- 算法导论——动态规划:0-1背包问题(完全解)
2019独角兽企业重金招聘Python工程师标准>>> package org.loda.dynamic;import org.junit.Test;/*** * @ClassNam ...
- 详解:动态规划算法【Java实现】——背包问题
动态规划 动态规划算法介绍 动态规划算法最佳实践-背包问题 思路分析: 图解分析: Java代码实现: 动态规划算法介绍 1)动态规划(Dynamic Programming)算法的核心思想是:将大 ...
- 完全背包问题贪心算法c语言,数据结构与算法学习之路:背包问题的贪心算法和动态规划算法...
一.背包问题描述: 有N种物品和一个重量为M的背包,第i种物品的重量是w[i],价值是p[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大. 二.解决方法: 1.贪心算 ...
- 总结——01背包问题 (动态规划算法)
0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi . 问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大? 分析一波,面对每个物品,我 ...
- 01背包问题-动态规划算法(最简洁)
动态规划算法: 思路:从第一个物品开始填表(m[i][j]),从左到右,从上到下 如果物品比背包容量大,即放不下,则m[i][j]=m[i-1][j] 如果物品比背包容量下,即放得下,则比较 放与不放 ...
- 探讨与研究——动态规划算法、回溯法、分支限界法解0-1背包问题
一个人终归是要成长的,是要不断历练的,没有人可以安安稳稳一辈子.就算是最有地位最有钱的人也要不断追求.不断历练.不断提升自己. 人的学问少时在不断学习,青年时期不断实践.随着时间推移,到了老年终有所成 ...
- Java使用动态规划算法思想解决01背包问题
Java使用动态规划算法思想解决背包问题 背包问题是一种组合优化的NP完全问题.问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高 动 ...
最新文章
- SecureCRT中sqlplus,使用Backspace删除时 ^H^H
- Python进阶【第五篇】函数式编程及某些特殊函数
- [杂谈]杂谈章2 eclipse没有(添加)“Dynamic Web Project”
- hdu1824 基础2sat
- Python的安装 || python介绍
- u-boot分析之小结(六)
- 使用Github Pages建独立博客
- 详解如何充分发挥先验信息优势,用MRC框架解决各类NLP任务
- 自己写的 ORACLE 函数的解读
- 华为云GaussDB(for MySQL)2.0全新升级,三大技术大揭秘
- 是否有“他们的”版本的“git merge -s ours”?
- SQL Agent服务无法启动如何破
- 如何使用fiddler工具抓包?
- SpringBoot自定义数据源DruidDataSource
- centos 6.9部署svn服务器和客户端(客户端含windows、linux版本)
- windows10下部署环境并运行Siammask中的demo全过程记录
- Ping IPv6在线测试检测 testipv6 加速镜像
- 指针分配和释放空间(转)
- 拼多多开店有哪些优势?
- 【监控】Prometheus(普罗米修斯)监控概述