灯神动态规划(Dynamic Programing)学习笔记 打劫问题 凑整问题 背包问题 例题+原理+源码超详细讲解

动态规划Dynamic Programing学习笔记 打劫问题 凑整问题 背包问题

学习资源

灯神的视频是我目前个人觉得讲解最清晰的动态规划教学,这篇文章是对他视频内容的汇总,在此基础上我还个人补充了背包问题的解决方案。
灯神的b站小课堂

Example1. 打家劫舍问题 looting problem

题目模型简化如下: 给定数组arr=[4,1,1,9,1],从中选择数字来使得总和最大注意不能选择相邻的数字。例如,选择4后,不能选择1;选择9后,不能选择9左右两侧的数字1。

  1. 模型建立。我们拿出一组新的数组arr=[1,2,4,1,7,8,3],将其按顺序分别编号为0-6。如图在这里插入图片描述

  2. 假定opt(6)表示:从前6个数字中选出总和最大的最佳方案(也就是我们期望得到的结果),同理opt(5)表示从前五个数字中选出来总和最大的方案……依次类推。

  3. 我们在这个基础上进行推理:对于第六个数字,我们有选择和不选择这两种操作,当我们选择第六个数时,根据规则就无法选择第五个数字,因此opt(6)=opt(4)+arr[6],即在这种情况下,opt6的值等于前四个数字中选出的最大值加上第六个数的数值。如果我们不选择第六个数,那么opt(6)=opt(5),即前五个数所能选出总和最大的值。

  4. 根据这个逻辑,为了算出opt6,我们需要经历以下过程。加号表示选择当前数,减号表示不选择当前数。整个过程的树状图如下在这里插入图片描述根据上图所示的逻辑,我们可以从中推出普遍的递推规律。因为我们是求最大值,所以前i个数字所能构成的最大总和为opt(i)=max(opt(i-1),opt(i-2)+arr[i]),其中,opt(i)是我们放弃选择第i个数时所得到的前i-1个数能构成的最大值;opt(i-2)+arr[i] 是我们选择第i个数时能得到的最大值。

  5. 寻找出口条件#当i=0时 opt(1)=max(arr[1],arr[0]) #当i=1时 opt(0)=arr(0)

  6. 用代码来实现当前逻辑。分两种,首先是recursive method

import numpy as np

arr=[1,2,4,1,7,8,3]

def rec_method(arr,i):  #recursive method
    if i==0: 			#在if和elif中填写我们前面推出来的出口条件
        return arr[0]
    elif i==1:
        return max(arr[0],arr[1])
    else:				#在else中填写我们发现的基本逻辑
        a=rec_method(arr,i-1)
        b=rec_method(arr,i-2)+arr[i]
        return max(a,b)
    
rec_method(arr,6)

运行这段代码后可以发现最终结果为15。这个地方我们运行的基本原理就是在函数中不停地调用自身,最终由opt6出发推演至opt0,直到算出答案(如上面我手绘的树状图)。这种办法虽然变成起来思路比较直观,但在该推演过程中有很多重复的运算。回到上面的树状图,该办法在计算opt6时,重复计算了opt4和opt3,我分别用黄色、绿色荧光笔将重复部分标记了起来。

当arr内数据和i的数量变大时,使用这种办法的运算成本将大幅升高。接下来介绍非recursive的计算方法

#续上面的代码
def nonrec_method(arr):
    opt = np.zeros(len(arr)) #建立数组来存储opt数据
    opt[0]=arr[0]
    opt[1]=max(arr[1],arr[0])
    for i in range(2,len(arr)):
        A = opt[i-2]+arr[i]
        B = opt[i-1]
        opt[i] = max(A,B)
    return opt[len(arr)-1]
nonrec_method(arr)

输出结果为15.0。该种方法可以理解为逆着树状图,由opt0出发一步一步推演至最终结果opt6,好处在于每一步的结果我们都储存在了数组中,节约了不少计算资源。

Example2. 凑整问题

题目要求如下,给定数组arr=[3,34,4,12,5,2],判断能否实用数组内的数构成S(s为一个数)。

  1. 模型建立:假定S=9,将arr中的数据从0-5进行编号。如图在这里插入图片描述

  2. 我们设subset(i,S),其中i表示使用前i个数字,S为我们期望拼凑成的数。例如,subset(5,9)表示使用前五个数字来拼凑出9的总和。subset(2,3)表示使用前两个数字拼凑出3。我们从这个符号出发,开始推理过程,首先考虑subset(5,9),我们此时要用前五个数来拼出9,首先判断第五个数。在这个地方我们有两种选择,第一种选择是使用第五个数来拼凑出9,那么选择后的情况就变成了subset(4,7),表示用前4个数去凑成7。第二种选择是不使用第五个数,那么情况就变成了subset(4,9)。绘制成树状图如下在这里插入图片描述

  3. 由此我们可以推出普遍的递推规律,若使用前i个数凑成S,递推规律表达式为在这里插入图片描述其中arr表示存放数据的数组。

  4. 接下来,我们开始寻找出口条件,第一种情况是当S下降为0时,说明前面的数字已经完成了拼凑任务,此时应该返回True,表示能够拼凑出S;第二种情况是当i下降为0时,S仍然不为0。i=0说明已经引导到了数组的首位数,如果此时满足arr[0]==S,那么说明能够凑出S,返回True。如果不相等则说明arr中的数字无法拼成S,返回False。这两个出口条件总结如下:

if i==0: return arr[0]==S #如果arr[0]==S,说明能凑成,返回True;反之则返回False
elif S==0: return True
  1. 通过Recursive的方法编写的python源码如下:
import numpy as np

arr=[3,34,4,12,5,2]

def opt(arr,i,S):
    if S==0 :
        return True
    elif i==0:
        return S==arr[i]
    elif arr[i]>S:
        return opt(arr,i-1,S)
    else:
        A = opt(arr,i-1,S-arr[i])
        B = opt(arr,i-1,S)
        return A or B
print(opt(arr,len(arr)-1,1))

运行后结果为True,说明arr内的数字能够凑出9。

  1. 上面的recursive法虽然解决了这个问题,但由于本质是重复的迭代计算,随着数组复杂度的增加,运算占用资源会大大上升,速度也会明显变慢。在这样一情况下,我们使用另一种方法来解决这个问题。
def nonrec_opt(arr,S):
    subset = np.zeros([len(arr),S+1],dtype=bool)
    subset[0,:]=False
    subset[:,0]=True
    if arr[0]<S:
        subset[0,arr[0]]=True
    for i in range(1,len(arr)):
        for j in range(1,S+1):
            if arr[i]>j:
                subset[i,j] = subset[i-1,j]
            else:
                A = subset[i-1,j-arr[i]]
                B = subset[i-1,j]
                subset[i,j] = A or B
    l,w = subset.shape
    print(subset)
    return subset[l-1,w-1]

print(nonrec_opt(arr,9))

运行后输出结果同样为True,但该方法中,我们使用numpy创建了一个数组,依次计算并存储了各个阶段中subset的bool值,避免了前面recursive method中出现的重复计算的问题。本质上,这种办法相当于创建了一个如下的bool表格:在这里插入图片描述红圈就是我们最终想要得到的结果,即`subset(5,9)

Example3. 背包问题

  1. 题目要求,有以下编号为1-4的四件物品,他们各自的体积及其所占据的空间如下表格所示,现有一个容积为8的背包,如果我们想让该包中所存放的物品价值最大,应该怎么放?在这里插入图片描述
  2. 题目分析:在Example2中,我们分析了凑整问题的解决方案,当前这个背包问题与凑整问题有相似之处但更为复杂:一点相似之处在于把物品放入背包,相当于是一个条件弱化的凑整问题,不需要恰好相等,只需要保证放入物品的总体积小于背包容积即可。不同之处在于,前一个凑整问题的返回值可为布尔量(即0和1),0表示当前方案不可行,1表示可以凑出该数值;本题中不再单纯的返回一个布尔量,而是返回背包内物品的总价值,我们取最终价值最高的方案来当作最优解。
  3. 和Example2一样的方法,我们首先设计表格:在这里插入图片描述
  4. 表格中横轴从0开始直至背包体积、纵轴从0开始直至物体总数量。相应地,我们在纵轴的旁表上相应物品编号的属性。其中,我用S,size来表示物体体积、V,value表示物品价值。 首先,我们可以在第一行和第一列全部填上0,因为在背包容积为0和包内物品数量为0的这两种情况下,包内物体的总价值都是恒为0的。接下来,我们看编号为1的这一行,在到横轴的背包体积达到2之前,一号物品始终是放不进书包的,故opt(1,1)为0(这个地方opt表示:当物品数量为1,背包容量为1时,背包内物品的最大总价值 ,后面还会常用到)。当背包容积为2时,此时能够放进第一个物品,我们有两种选择,放入一号物品或者不放入。此时的opt(1,2)也就等于这两种选择中让包内物品价值更大的那个选择,即opt(1,2)=max( 放入,不放入 )。如果放入,背包体积变为2-2=0,物品数量1-1=0,即opt(0,0)再加上第一件物品的价值,opt(0,0)+3。如果不放,说明放弃一号物品,物品数量-1,背包容积不变,即opt(0,2)。如下图:在这里插入图片描述
  5. 按照同样的逻辑,我们可以依次将该表格填下去,而每一次决策(选或不选)均取决于哪一种选择能带来更高的物品总价值,且总能参考前面已经得到的opt值。完成的表格如下,(有时间了再更新)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

onetwothree_go

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值