【动态规划】背包问题

注意:for j in range(1,m+1):是枚举所有的情况,不用一一判断放入物品后背包容量减少后j的变化。为什么从1开始,因为0已经写出来了,即dp[i-1][j]=dp[i-1][j-0*a[i-1]]+0*v[i-1]

01背包无价值

dp定义:前i项物品中的所有排序对应的当前重量(由给定数组决定)就为j的值,将此dp[i][j]=True表示就给的数组的前i项可以组成重量为j的组合。自然dp[i][0]都是True,不用放入物品自然是0。

for循环从m开始是因为题目要求最多装多少,故先从最大的开始。

        dp=[[False]*(m+1) for i in range(n+1)]
        dp[0][0]=True
        for i in range(1,n+1):
            dp[i][0]=True
            for j in range(1,m+1):
                if j-a[i-1]>=0:
                    dp[i][j]=dp[i-1][j] or dp[i-1][j-a[i-1]]
                else:
                    dp[i][j]=dp[i-1][j]
        
        for i in range(m,-1,-1):
            if dp[n][i]==True:
                return i
                break

01背包有价值

        dp=[[0]*(m+1) for i in range(n+1)]
        #dp[0][j]=0,初始化前0项的价值为0
        for i in range(1,n+1):
            for j in range(1,m+1):
                if j-a[i-1]>=0:
                    dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])
                else:
                    dp[i][j]=dp[i-1][j]
        return dp[n][m]

完全背包问题

max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])实际上可以写成max(dp[i-1][j-0*a[i-1]]+0*v[i-1],dp[i-1][j-1*a[i-1]]+1*v[i-1])

依次类推多重背包就可以写成:

for count in range(j//a[i-1]+1):

        max(dp[i-1][j-0*a[i-1]]+0*v[i-1],dp[i-1][j-1*a[i-1]]+1*v[i-1],dp[i-1][j-2*a[i-1]]+2*v[i-1]......)

        n=len(a)
        dp=[[0]*(m+1) for i in range(n+1)]
        for i in range(1,n+1):
            for j in range(m+1):
                for count in range(j//a[i-1]+1):
                        dp[i][j]=max(dp[i][j],dp[i-1][j-count*a[i-1]]+count*v[i-1])
        return dp[n][m]

但是会超时

优化

        放第i件物品时。这里的处理和01背包有所不同,因为01背包的每个物品只能选择一个,因此选择放第i件物品就意味着必须转移到dp[i-1][v-w[i]]这个状态;

        但是完全背包不同,完全背包如果选择放第i件物品之后并不是转移到dp[i-1][v-w[i]],而是转移到dp[i][v],这是因为每种物品可以放任意件(注意有容量的限制,因此还是有限的),放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保持大于等于0为止。

        注意(for j in range(a[i-1],m+1):整个过程表示当前i-1个商品选完时该选则第i个商品,a[i-1]一直放,每一次放的次数不同所对应的j不同,直到到装不下时(即j<a[i-1]) ,而且每一次都是根据第i-1个商品所得到的关于j的一维列表得到的,也是一个i个商品对应的关于j一维列表。

        关于状态方程为什么是dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1]),因为dp[i][j]是先从dp[i-1][j]转移过来,然后再进行a[i][j]的数量选择,即dp[i][j-a[i-1]]。

n=len(a)
dp=[[0]*(m+1) for i in range(n+1)]
for i in range(1,n+1):
    for j in range(a[i-1],m+1):
        dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1])

return dp[n][m]

多重背包

我们考虑一下,如果所有ni都满足ni ≥ W / wi,那不就变成完全背包的问题了么。可见,完全背包的基本实现思路也可以应用到多重背包的基本实现。对于多重背包的基本实现,与完全背包是基本一样的,不同就在于物品的个数上界不再是v/c[i]而是n[i]与v/c[i]中较小的那个。所以我们要在完全背包的基本实现之上,再考虑这个上界问题。

这次我们直接空间优化,不再讲解二维做法:

多重背包是可以不选,也可以选 1 个,可以选多个,而 0-1 背包只能选 0 个或者 1 个。

那就直接把种物品分开,即可比如:

每个盘子 3 块钱,我有 2 个。每双筷子 1 块钱,我有 10 双,每对刀叉 3 块钱,我有 3 个。

那么我就可以拆成,有 2 个三块的盘子,每个可以选也可以不选,就变成了 0-1 背包。

也就是说,对于每种是可以选多个,那就直接拆分成独立的个体就可以了。

代码与完全背包的区别除了多了个表示物品个数的数组s[ ]之外,只在内循环的控制条件那里。

        n=len(a)
        dp=[[0]*(m+1) for i in range(n+1)]
        for i in range(1,n+1):
            for j in range(1,m+1):
                for k in range(min(j//a[i-1]+1,s[i])+1)):
                        dp[i][j]=max(dp[i][j],dp[i-1][j-k*a[i-1]]+k*v[i-1])
        return dp[n][m]

滚动数组(空间优化(二维变一维)

01背包

因为状态转移每次只与上一层有关,所以用一个一维数组就可以。

为什么从大到小遍历?

看 dp[j]=dp[j-c[i]]+w[i]这一状态转移,

为什么从c[i]开始,是因为j不能小于c[i]

为什么是逆序呢,因为在第一次i=0的for循环时,所有满足range(c[i],C+1)的dp[j]都是w[I],所有的dp[j - c[i]]都是0,因为第一次之前的价值均为0,

可能有人要问了,第一次的j难道不都是C吗,是都是,但是其他不是C的但是大于c[i]的可以不用管。

如果从小到大则当循环到大的j时,dp[j - c[i]]不一定等于0。

是根据小的改大的,如果先把小的改了,那小的还会被用到,数据就不对了,所以从小到大。

        为什么是逆序遍历?,这个是根据原状态方程判断的由dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])可知,dp[i][j]是由上方i-1层和左边一直到j为止的一维数据转换而来,变到一维dp[j] = max(dp[j - a[i]] + v[i], dp[j])则是由之前i-1次的一维列表的转换而来,因为是靠旧的且是左侧的数据,所以就要从大到小开始遍历(如果是靠右边的旧数据,则是从小到大)

        注意到状态转移方程中计算dp[i][v]时总是只需要dp[i-1][v]左侧部分的数据(即只需要图中正上方与左上方的数据),且当计算dp[i+1][ ]的部分时, dp[i-1]的数据又完全用不到了(只需要用到dp[i][ ]),因此不妨可以直接开一个一维数组dp[v](即把第一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),

    for i in range(n):
        for j in reversed(range(a[i],m+1)):
            dp[j] = max(dp[j - a[i]] + v[i], dp[j])

    print(dp[m])

完全背包

空间优化:

未省略取物品次数k的等价转换代码:

for i in range(1, N + 1):
    for j in reversed(range(a[i],m+1)):
        for k in range(0, j // a[i] + 1):
            dp[j] = max(dp[j], dp[j - k * a[i]] + k * v[i])

print(dp[m])

因为状态转移每次只与上一层有关,所以用一个一维数组就可以。

为什么这次是从小到大遍历了呢?

是根据小的改大的,而此时的含义为选了 x 件后的容量与质量,跟第一个类似,但含义不同,所以子处理方式上也有本质区别,处理完一件后在处理下件。

完全背包的一维数组实现和01背包也是几乎完全相同,唯一差别是完全背包的内循环是正向遍历,而01背包的内循环是逆向遍历。

省略取物品次数k的等价转换代码:

        为什么是又变成了正序,这次还看原状态方程dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1]),可知dp[i][j]是由右上一层即i-j层正上方(在一维中直接是dp[j] =dp[j])和右测以j开头的数据得到因为dp[i][j]=dp[i][j-a[i-1]]中左侧的j小于右测的j。故一维状态方程为正序。

        怎么理解必须正向枚举呢?求解dp[i][v]需要它左边的dp[i][v-w[i]]和它上方的dp[i-1][v],显然如果让v从小到大枚举,dp[i][v-w[i]]就总是已经计算出的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,可以直接覆盖。

        为什么可以覆盖,因为物品可以多次取出,每多取一次都是在覆盖。

    for i in range(n):
        for j in range(a[i],m+1):
            dp[j] = max(dp[j - a[i]] + v[i], dp[j]);

    print(dp[m])

多重背包

还是看原状态方程dp[i][j]=max(dp[i][j],dp[i-1][j-k*a[i-1]]+k*v[i-1])

    for i in range(n):

        for j in reversed(range(a[i],m+1)):

            for k in range(s[i]+1):
                if j<k*a[i]:
                    break
                dp[j] = max(dp[j], dp[j - k * a[i]] + v[i] * k)

    print(dp[m])

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值