数据结构与算法之动态规划--旷工问题

动态规划:

        针对最常见的最优化问题,动态规划如何设计求解呢?下面我们研究一个典型的最优化问题:矿工挖矿问题。矿工挖矿问题是为了解决在给定矿产和矿工数量的前提下,能够获得最多钻石的挖矿策略。

问题描述:

        假设某地区有5座钻石矿,每座钻石矿的钻石储量不同,根据挖矿难度需要参与挖掘的工人数量也不同。假设能够参与挖矿工人的总数是10人,且每座钻石矿要么全挖,要么不挖不能只派出一部分人挖取一部分矿产。要求用程序求解出,要想得到尽可能多的钻石,应该选择挖取哪几座矿产?

各矿产的钻石储量与所需挖掘工人数量如表所示:

 问题分析:

    1.分析原问题最优解的结构特征
        
首先寻找最优子结构。我们的解题目标是确定10个工人挖5座矿产时能够获得的最多的钻石数量,该结果可以从10个工人挖4个矿产的子问题中递归求解。证明不再赘述。在解决了10个工人挖4个矿产后,存在两种选择:一种选择是放弃其中一座矿,比如第五座矿产,将10个工人全部投放到前4座矿产的挖掘中如图所示

        另一种选择是对第5座矿产进行挖掘,因此需要从10人中分配3个人加入到第5座矿产的挖掘工作中,如图所示

         因此,最终的最优解应该是这两 种选择中获得钻石数量较多的那个,即为上述两图所描述的场景中的最大值。为了方便描述,我们假设矿产的数量为n,工人的数量为m,当前获得的钻石数量为G[n],当前所用矿工的数量为L[n],则根据上述分析,要获得10个矿工挖掘第5座矿产的最优解F(5,10),需要在凡4,10)和F(4,10-L[4])+G[4]中获取较大的值,即

                                          F(5,10)=max{F(4,10),F(4,10-L[4]+G[4]}

         因此,针对该问题而言,以上便是F(5,10)情况下的最优子结构。

2.建立递归关系,写出状态转移函数
        我们首先来考虑该问题的边界和初始条件。对于一个矿产的情况,若当前的矿工数量不能满足该矿产的挖掘需要,则获得的钻石数量为0,若能满足矿工数量要求,则获得的钻石数量为G[0]。因此,该问题的初始边界条件可表述为:

        当n=1,m≥L[0]时,F(n,m)=G[0];当n=1,m<L[0]时,F(n,m)=0。

综上,可以得到该问题的状态转移函数为:

F(n,m)=0(n≤1,m<L[0])
F(n,m)=G[0](n==1,m≥L[0])
F(n,m)=F(n-1,m)(n>1,m<L[n-1])
F(n,m)=max(F(n-1,m),F(n-1,m-L[n-1])+G[n-1])(n>1,m≥L[n-1])

        至此,我们定义了用动态规划解决该问题的几个要素。下面,我们要做的是利用边界和初始条件、最优子结构和状态转移函数对该问题进行求解。

3.计算最优解的值

        初始化阶段,我们利用表格分析求解思路。如表所示,表格的第一列代表挖掘矿产数,即n的取值情况;表格的第一行代表占用工人数即m的取值情况;中间各空白区域是我们需要通过计算填入的对应的钻石数量,即F(n,m)的取值。

        在挖掘第一个矿产时,由于其所需的工人数量为5,所以当m 的取值小于5时,根据公式F(n,m)=0(n≤1,m<[0]),获得的钻石数量均为0。当m的取值大于或等于5时,根据公式(n,m)=G[0](n==1,m≥L[0]),钻石数量的取值为400,如下表所示。此时确定了该问题的边界。

        在挖掘第2个矿产时,由于其需要5个人进行挖掘,因此当m 取值小于5时,根据公式F(n,m)=F(n-1,m)(n>1,m<L[n-1]),F(2,m)=F(1,m)=0;当m 取值大于或等于5时,根据公式F(n,m)=max{F(n-1,m),F(n-1,m-L[n-1])+G[n-1]}(n>1,m≥L[n-1]),在5~9人的区间里,获得的钻石数量为500,即所有人都去参加第2个矿产的挖掘时获得的钻石量。这是因为当m ∈{5,9}时,F(1,m)<F(1,m-5)+500,但人数只够挖掘1个矿产,故选择储量较大的矿产。而在参与人数上升为10人时,上式仍成立,但此时两个矿产可以同时挖掘,因此获得的钻石数量为900,如下表所示

         同理,在挖掘第3个矿产时,钻石产出量为200,需要的工人数量为3,根据上述计算方式,可得钻石产出量如下表所示

        第4个矿产的钻石产出量为300,需要的工人数量为4,根据上述计算方式,可得钻石产出量如下表所示

 针对第5个矿产的钻石产出量计算与上述过程一致,具体产出量如下表所示 

        通过以上的计算过程,我们不难发现,除第一个矿产的相关数据,表格中的其他数据都可以由前一行的一个或两个格子推导而来。例如,3个矿产8个人挖掘的钻石量F(3,8)就来自2个矿产8个人挖掘的钻石量F(2,8)和2个矿产5个人挖掘的钻石量F(2,5),即F(3,8)=max{F(2,8),F(2,5)+200}max(500,500+200)=700。再比如,4个矿产10个人挖掘的钻石量F(4,10),来自3个矿产10个人挖掘的钻石量F(3,10)和3个矿产7个人挖掘的钻石量F(3,7),即F(4,10)=max{F(3,10),F(3,7)+300}=max(900500+300)=900。

        根据以上思路,在用程序实现该算法的过程中,采用自底向上的方式进行计算,像填表过程一样从左至右、从上到下逐渐获得计算结果。这样,可以不需要存储整个表格的内容,仅需要存储前一行的结果,就可以推出下一行的内容,避免了重复计算。

 4.构造最优解
        填表结束后,我们通过回溯的方式可以找出该矿工挖矿问题的最优解组合为所有矿工(10人)挖掘矿产1和矿产2,最多可以挖得价值900的钻石。

代码实现:

def  goldMine(n,m,g,L):  #n:第n个矿产 m:工人的数量 g:当前获得的钻石数量G[n]  L:当前所有旷工的数量L[n] 
    # results = [0 for _ in range(m+1)]     #  保存返回结果的数组
    t_results = [0 for _ in range(m+1)]   #  保存上一行结果的数组
    for i in range(1,m+1):     # 填充边界格子的值,从左到右填充表格第一行的内容   i指的是人数 从1遍历到m
        t_results[i]=0 if i<L[0] else g[0]   # 若当前人数少于挖掘第一个金矿所需人数,黄金量为0;若当前人数不少于第一个金矿所需人数,黄金量为g[0]
    for i in range(1,n): # 外层循环为金矿数量
        results = [0 for _ in range(m+1)]
        for j in range(1,m+1): # 内层循环为旷工数量
            results[j] = t_results[j] if j < L[i] else max(t_results[j],t_results[j-L[i]]+g[i])  #转换函数
        t_results = results #更新上一行结果的数组
    return results[-1]
g=[400,500,200,300,350]
L=[5,5,3,4,3]
print(goldMine(5,10,g,L)) # 输出900

        我们定义函数goldMine(n,m,g,L)来计算挖掘第n个矿产,有m个工人参与时能获得的钻石量,其中q和L分别为数组,分别存放对应各个矿产的钻石量和所需工人数。在正式迭代之前,首先界定边界的情况。之后进入循环的迭代过程,在迭代中,根据之前的分析,我们仅需要关注前一行t_ results的取值,即可通过状态转移函数获得F(n,m)的值,因此,整个迭代过程仅需要引入t_results数组保存前一行的值即可。之后,通过函数goldMine(n,m,g,L)迭代计算各个矿产数量与工人数量组合所能产生的钻石量,最终获得问题的解。

 .

  • 70
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值