背包问题

本文详细介绍了01背包和完全背包问题,包括题意、思路和优化空间复杂度的方法,并给出了模板伪代码。同时,讨论了背包问题问法的变化,如求方案总数的动态规划解法,探讨了不同版本的解决方案。
摘要由CSDN通过智能技术生成

01背包问题

题目

有N件物品和一个容量为V的背包,第i件物品的费用是c[i],价值是w[i],求解将哪些物品装入背包可以使得价值总和最大。

思路

01背包问题是最基础的背包问题,特点是每种物品仅有一件,可以选择放或者不放。
用子问题定义状态,即f[i][v]表示前i件物品恰好放入一个容量为v的背包可以获得的最大价值,状态转移方程是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]},逗号前面是选择不放入第i件的情况。逗号后面是选择放入第i件的情况。

优化空间复杂度

空间复杂度可以由O(N*V)优化到O(V),上面的思路是通过一个主循环i=1…N,每次算出二维数组f[i][0…V]的所有值,优化空间复杂度的思路是:只用一个数组f[0…V],如果在每次主循环中,以v=V…0从大到小的顺序推出f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值,则第i次循环结束后f[v]中表示的就是状态f[i][v]。

模板伪代码如下
for i=1..N
	for v=V..0
		f(v)=max{f[v],f[v-c[i]]}+w[i];

下面的模板代码实现:处理一件01背包中的物品,cost和weight表示物品的费用和价值。

procedure ZeroOnePack(cost,weight)
	for v=V..cost
		f[v]=max{f[v],f[v-cost]}+weight

模板代码中费用为cost的物品不会影响状态f[0…cost-1]

有了上面的模板代码,01背包问题可以写成下面:

for i=1..N
	ZeroOnrPack(c[i],w[i]);
初始化
  1. 若题目要求恰好装满背包,在初始化时,除了f[0]设置为0,其他f[1…V]均设置为-∞,这样最终得到的f[N]就是恰好装满背包的最优解。
  2. 若题目不要求恰好装满背包,只是希望价格尽量大,初始化时应该将f[0…V]全部设置为0。
  3. 原因:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态,如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing恰好装满,其他容量的背包均没有合法的解,属于未定义的状态,他们的值就都应该是-∞,如果背包并非必须装满,那么任何容量的背包都有一个合法解即“什么都不装”,这个解的weight价值是0,所以初始化状态的值也全部是0了。

完全背包问题

题目

有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i种物品的费用是c[i],价值是w[i],求解将哪些物品装入背包可以使得这些物品的费用总和不超过背包容量,且价值总和最大。

思路

类似于01背包问题,所不同的是每种物品有无限条件,即从每种物品角度出发,与它相关的策略已经不是取或者不取,而是取0件,取1件等。
按照01背包问题的思路写出状态转移方程:
f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k*c[i]<=v}
转化为01背包问题求解:O(VN)的算法,使用一维数组。

for i=1..N
	for v=0..N
		f[v]=max{f[v],f[v-cost]}+weight

完全背包的伪代码与01背包的伪代码的唯一区别是:v变量的循环顺序。在01背包中,v是按照从大到小的顺序,因为要保证第i次的循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推来的,即是为了保证每件物品只能选一次,保证在考虑“选择第i件物品”时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]],而完全背包的特点是每种物品可选择无限件,所以在考虑选择第i种物品时,正好需要一个可能已经选入第i种物品的子结果f[i][v-c[i]],所以必须采用v=0…V从小到大的顺序。

背包问题问法的变化

求方案总数

例题:
494.目标和
第一种解法:递归(枚举)
第二种解法:动态规划(01背包)

动态规划的几个版本
版本一 官方答案

思路:dp[i][j]表示数组前i个元素组成和为j的方案数。考虑到第i个数nums[i]可以添加“+”或者“-”,所以有状态转移方程:
dp[i][j]+=dp[i-1][j-nums[i]](注意不是dp[i][j]=dp[i-1][j-nums[i]])(对当前数组元素nums[i]取负号的情况,所以右边式子的第二维是j-nums[i],j和i都是当前的)

补充:解释上面式子:
dp[i][j]=dp[i][j]+dp[i-1][j-nums[i]],式子中左边的dp[i][j]中的i和j都是当前的i和j,而右边式子中的dp[i][j]是上一轮求出来的值dp[i][j],所以其中的i和j是上一轮的i和j。而右边式子中的dp[i-1][j-nums[i]]中的i和j是当前的i和j,与左边式子的i和j一致,只有右边的dp[i][j]是上一轮的结果。

dp[i][j]+=dp[i-1][j+nums[i]](对当前数组元素取正号的情况,所以右边式子是j+nums[i],j个i都是当前的。)

把j=j-nums[i]以及j=j+nums[i]代入上面的两个式子有:同样的j=j-nums[i],左边的j是当前考虑的累加和,右边的j是上一轮的累加和,nums[i]的i是当前的i。这是为了把状态方程的右边式子化为一致。
dp[i][j+nums[i]]+=dp[i-1][j](代入j=j+nums[i])
dp[i][j-nums[i]]+=dp[i-1][j](代入j=j-nums[i])

由于数组中所有数的和不超过1000,所以j的最小值可以达到-1000,由于不允许数组下标为负,所以需要平移,将区间[-sum,sum]平移到[0,2*sum],即给dp[i][j]的第二维预先增加1000。
dp[i][j+nums[i]+1000]+=dp[i-1][j+1000]
dp[i][j-nums[i]+1000]+=dp[i-1][j+1000]

class Solution {
   
    public int findTargetSumWays(int[] nums, int S) {
   
        //官方版的动态规划
        int[][] dp=new int[nums.length][2001];//2001=1000*2+1
        dp[0][nums[0]+1000]+=1;//可以写为dp[0][nums[0]+1000]=1;<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值