字节面试算法题-0,1背包问题

时间:2024-03-01 21:20:29

   我们在上一篇文章初识动态规划已经对动态规划的算法思想有了一定的了解,今天我们再来通过一个经典问题:0,1背包问题,从更深层次的角度来认识一下动态规划算法。建议先看上一篇文章,再来看这篇。

   首先,我们来看一下什么是0,1背包问题。   

   问题描述:给定 n 件物品,物品的重量分别为w1、w2、w3....,现需要挑选物品放入背包中,假定背包能承受的最大重量为V,问应该如何选择装入背包中的物品,使得装入背包中物品的的重量最大?

  首先,我们最直观的想法就是,穷举所有可能的装法,然后从中选出满足条件的最大值。我们可以使用回溯算法来实现。如下所示:

class BagQ:
     #假设物品的重量都大于0
     #背包的最大承重也大于0
     maxW=0
     weight=[2,2,8,3,5,3]  #物品重量
     n=6   #物品个数
     w=10  #背包的最大承重
     def getMax(self,i,cw):
          if cw==self.w or i==self.n: #背包装满或者物品被考察完了
               if cw>self.maxW:
                    self.maxW=cw
               return
          self.getMax(i+1,cw) #第i个物品不放入背包
​
          #考察放入第i个物品后,会不会超过背包的容量
          if cw+self.weight[i]<=self.w:
               self.getMax(i+1,cw+self.weight[i]) #选择装第i个物品
​
bag=BagQ()
bag.getMax(0,0)
print(bag.maxW)

  我们通过代码可以看到回溯算法的时间复杂度较高,是指数级别的。那有什么方法可以降低时间复杂度吗?我们最好的方式就是把递归调用树画出来,来找找规律。递归调用树如下所示:

 

      递归树的每个节点表示一种状态,用(i,w)来表示。比如f(1,2)表示第一个物品放入背包,此时背包的重量为2,下一步f(2,2)表示第二个物品不放入背包,此时背包的重量不变。而f(2,4)表示第二个物品放入背包,此时背包的重量为4。

      从上图我们可以发现,会有重复的子问题出现,比如f(2,2)被计算了两次,那我们该如何避免重复计算呢?

      我们可以这么来看,我们把整个求解阶段分为n个阶段,每个阶段去决策一个物品是否放入背包。每个物品决策完之后,对应的背包中物品的重量会有多种可能,也就是多种状态。

      我们来一步一步分析。

  1. 第一个物品的重量为2,我们先来决策第一个物品是否放入背包,它有两种可能,要么放入,要么不放,与之相对应的背包的重量也有两种可能,要么是0,要么是2。

  2. 第二个物品的重量为2,我们再来决策第二个物品是否放入背包,它也有两种可能,要么放入,要么不放,与之相对应的背包的重量就不是两种可能了,它有4种可能(我们的排列组合知识可以派上用场了)。它需要依赖于上个物品是否放入背包,所以它是需要依赖于上一个状态的。

       .....

      从上面的分析来看,第n个阶段背包的状态是需要依赖于第n-1个阶段的,所以我们需要把上一个阶段的状态保存下来,才能快速的求出这个阶段的状态,因此状态转移矩阵就出来了。我们这里需要定义一个二维的数组,来记录不同阶段的状态。如下图所示:

     下面我们来看代码是如何实现的:

def bag(weight,n,w):
     status=[[0 for _ in range(w+1)] for _ in range(n)]
     status[0][0]=1
     if(weight[0]<=w):
          status[0][weight[0]]=1
     for i in range(n): #动态规划状态转移
          #不把第i个物品放入背包
          for j in range(w+1):
               if status[i-1][j] == 1:
                    status[i][j] = 1
          #把第i个物品放入背包
          for j in range(w+1-weight[i]):
               if status[i-1][j] == 1:
                    status[i][j+weight[i]] = 1
     #输出结果
     print(status)
     for i in range(w,-1,-1):
          if status[n-1][i]==1:
               return i
     return 0
​
weight=[2,2,8,3,5,3]
n=6
w=10
print(bag(weight,n,w))

     我们通过把问题分解为多个阶段,每个阶段对应一个决策。然后记录下每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,依次前进,从而把问题解决。

       接下来,我们再来把0,1背包问题升级一下,引入物品价值这一说。也就是针对一组不同价值、不同重量的物品,我们将物品放入背包中,在满足背包最大重量的限制条件下,背包中可装入物品的总价值最大是多少呢?这个思路和上一个思路类似,我这里就不在赘述。建议大家先用回溯算法实现,然后画出递归树,最后写出状态转移矩阵,再实现代码。我这里直接给出代码。如果有问题,欢迎大家留言。

def bag(weight,value,n,w):
     status=[[-1 for _ in range(w+1)] for _ in range(n)]
     status[0][0]=0
     if(weight[0]<=w):
          status[0][weight[0]]=value[0]
     for i in range(n): #动态规划状态转移
          #不把第i个物品放入背包
          for j in range(w+1):
               if status[i-1][j] >= 0:
                    status[i][j] = status[i-1][j]
          #把第i个物品放入背包
          for j in range(w+1-weight[i]):
               if status[i-1][j] >= 0:
                    v=status[i-1][j]+value[i]
                    if(v>status[i][j+weight[i]]):
                         status[i][j+weight[i]]=v
     #输出结果
     print(status)
     maxV=0
     for i in range(w+1):
          if status[n-1][w]>maxV:
               maxV=status[n-1][w]
     return maxV
​
weight=[2,2,8,3,5,3]
value=[3,4,12,6,3,2]
n=6
w=10
print(bag(weight,value,n,w)) 

      经过这篇文章和上一篇文章,我们应该对动态规划有了一个清晰的认识,我会在下一篇把问题抽象一下,看哪类问题适合动态规划来解决,以及解决动态规划问题的思考过程是怎么样的?为了不错过,请关注公众号。

    极客时间10+的王争【数据结构与算法之美】pdf 资料下载,请关注公众号【程序员学长】,回复【数据结构与算法】即可得到。