贪心算法和动态规划的思路及其Python实现

贪心算法

百度的定义: 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

通俗一点讲,当要解决某一个问题时,先判断第一步的最优解,然后把剩下的步骤看作下一个递归的具体问题。

如0-1背包问题:给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

假设具体问题数值:A物品,重量为6kg,价值为8元,

                             B物品,重量为8kg,价值为13元,

                             C物品,重量为10kg,价值为15元

背包可以装为50kg的物品。

有经验的小朋友肯定首先判断拿取哪一个物品既轻又有价值。A物品:单位重量的价值为8/6(元)

                                                                                                 B物品:单位重量的价值为13/8(元)

                                                                                                 C物品:单位重量的价值为15/10(元)

计算看出,只要能装,首先要拿B物品,因为单位价值最高。

即剩余50kg

第一步:我有 50 kg背包,我可以选择的物品有 A,B,C

第二步:选择当前这一步最优解,即 B。而问题的背包变成50-8=42kg,而问题只有背包重量改变,重新回到第一步判断。

所以就是:(选择优先B,然后C,然后A)

1:我有  50kg  背包,我可以选择A,B,C,总价值为0    ,我选择B

2:我有  42kg  背包,我可以选择A,B,C,总价值为13  ,我选择B

3:我有  34kg  背包,我可以选择A,B,C,总价值为26  ,我选择B

4:我有  26kg  背包,我可以选择A,B,C,总价值为39  ,我选择B

5:我有  18kg  背包,我可以选择A,B,C,总价值为52  ,我选择B

6:我有  10kg  背包,我可以选择A,B,C,总价值为65  ,我选择B

7:我有  2kg    背包,我可以选择               ,总价值为78  ,没有空间选了


这时候有眼尖的小朋友就问了:“那这道题这样做的话,只能取6个B物品,重量为48kg,总价值为6*13=78元。那如果我取5个B物品,1个C物品,不是刚好50kg吗?这样总价值有5*13+15=80元呢”。其实这就看出,贪心算法得到的并不是最优解

如何用代码实现呢?

# coding=utf-8
if __name__ == '__main__':
    beg = 50                       #背包50kg
    value = 0                      #已经获得的价值
    choice = []
    while beg > 0:                 #如果背包还有空位,则递归
        if beg >= 8:               #选择当前这一步的最优解,既选择B商品
            beg = beg - 8
            value = value + 13
            choice.append("B")
        elif beg >= 10:            #要是B商品选择不了,则选择第二单位价值的物品,即A物品
            beg = beg - 10
            value = value + 15
            choice.append("A")
        elif beg >= 6:
            beg = beg - 6
            value = value + 8
            choice.append("C")
        else:                      #当所有的物品都选择不了,则退出
            break
    print "剩余的背包重量:",beg
    print "获得的总价值:",value
    print "选择的物品的类型及顺序:",choice

贪心算法只能保证 次优解


动态规划

动态规划可以看一下http://www.cnblogs.com/sdjl/articles/1274312.html提及到的金矿例题,我觉得算是我见过讲得最简练的博客了(膜拜..)
里面主要提到动态规划的6个思考点:(以下是借鉴上述博客作者的思路,再加以延伸)

1:最优子结构

好像贪心一样,动态规划也是把一个复杂问题分成一个一个步骤,而与贪心不一样的是,贪心是保证当前这一步是当前问题的最优解,而不保证这一步属于整个复杂问题的最优解。而最优子结构就是要规定当前这一步是整个复杂问题的最优解。(区别可参考一开始那个背包问题,最后一步究竟取B,还是取A。)

2:子问题重叠

子问题重叠在贪心也需要体现,走每一步面对的问题都是同一个类型的,例如背包问题的子问题就是:当前我的背包有Xkg空间,我能选择XXX。例如在金矿问题的子问题就是:当前我有X人,XXX剩余金矿。

3:边界

边界体现在背包没有空间,或者金矿没有足够人数取挖。

4:子问题独立

子问题即上面走的每一步之间没有干扰(互不影响)

----------------------------------------------------------------------------------------------------

看完这里还是迷迷糊糊?不急,还没说完,上面那四个点只是基础,还需要剔除重复的步骤。(相当于深搜的剪枝吧..)

这里我再引入一道题:斐波那契数列之青蛙跳台阶。

一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法。

这道题其实用动态规划可以解决。一般的想法:

# coding=utf-8
def fib(n):          #当前有N个台阶,可以选择跳一个台阶,也可以选择跳两个
    if n <=1 :       #边界问题,要是当前只剩下一个台阶,则只剩下一个方法跳。
        return 1
    else:            #跳一个台阶和跳两个台阶都是一个选项。
        return fib(n-1)+fib(n-2)
print fib(5)
这种最简单的方法是可以实现少台阶的情况,一旦多情况(100个台阶)就运行不下去。http://blog.csdn.net/baidu_28312631/article/details/47418773博客作者阐述了如果动态规划不优化的话,答案可能性有多少。在这里我用图列出一共有多少种可能性。

每到叶结点的0代表剩下0步,确定唯一的跳台阶的唯一确定方案。上面一共有8个叶结点,即有8种方案。不过这里我们不是讨论这道题的解,而是讨论动态规划的优化。

当我剩下1层台阶时,我只有两种可能,就是这里(或者从2到0)

咦,那当我只剩下2层台阶时,我也只有一种可能性,就是

当我剩下3层台阶时,也只有

剩下4层台阶时,有



这样的话,其实我有5层台阶时,其中包括1个(4层台阶子树),2个(3层台阶子树),3个(2层台阶子树),5个(从1到0),4个(从2直接到0)。

这样我用一个“备忘录”(动态规划的一个专用名词)记录我计算过的树结构的话,5层台阶的备忘录就只有5项,而不优化的话,我就要计算答案那么多的可能性(8种),一旦台阶数变多,答案的可能性个数会飞速上升。
经过优化的跳台阶的动态规划:(网上有个版本是用装饰器在未优化的基础上添加功能,我这里简单用普通方法写一遍..)
代码如下:
class Dp1(object):                                     #动态规划类
    def __init__(self,n):                              #初始化
        self.mark = [0 for _ in xrange(n+1)]           #定义一个一维数组,初始化全部为0,长度为台阶数。用来当作“备忘录”。
        print self.dp(n)                               #开始递归
    def dp(self,n):                                    #递归的方法
        self.m = 0                                     #m的含义是当前n个台阶有m种跳法
        if self.mark[n] != 0:                          #先从备忘录寻找n,若存在mark[n]不等于0,则代表曾经计算过,n个台阶有mark[n]种跳法
            self.m = self.mark[n]                      #若备忘录有,则直接得到n层台阶的答案
        elif n <= 0:                                   #从这里开始的四行是用来判断“边界问题”
            if n == 0:                                 #若刚好跳完台阶,则这样算一种方法
                self.m = 1                             #m变成1,代表是一种可行方法
            else:                                      #有可能跳的台阶超过实际台阶数
                self.m = 0                             #m为0,代表不可行
        elif n>0:                                      #这里两行是用于规划转移方程式(其实这里很简单),青蛙只有两种可能,跳一层或者跳两层。
            self.m = self.dp(n-2)+self.dp(n-1)         #当前n层台阶的解个数 等于 n-1层台阶的解 + n-2层台阶的解
        self.mark[n] = self.m                          #把m放入备忘录,下次若是再次是n层台阶,则不用计算直接取备忘录的数。(优化)
        return self.m                                  #返回

if __name__ == '__main__':
    dp1 = Dp1(100)


所以其实一开始没有写错,是6个思考点,是经过优化的6个思考点,第五步是做备忘录,第六步是时间分析。
总结步骤的话,根据博客作者的话来说,就是:

       1、构造问题所对应的过程。

       2、思考过程的最后一个步骤,看看有哪些选择情况。

       3、找到最后一步的子问题,确保符合“子问题重叠”,把子问题中不相同的地方设置为参数。

       4、使得子问题符合“最优子结构”。

       5、找到边界,考虑边界的各种处理方式。

       6、确保满足“子问题独立”,一般而言,如果我们是在多个子问题中选择一个作为实施方案,而不会同时实施多个方案,那么子问题就是独立的。

       7、考虑如何做备忘录。

       8、分析所需时间是否满足要求。

       9、写出转移方程式。


一开始的金矿例题是用C++实现的,我这里换成python。
这是根据开头提到的博客作者代码思路改编的。
class Dp(object):
    def __init__(self, n, m, peopleneed, gold):        #n是总人数,m是金矿数
        self.peopleneed = peopleneed                   #每一个金矿开挖需要的人数
        self.gold = gold                               #每一个金矿的金矿数
        self.maxgold =[[-1 for i in xrange(n)] for i in xrange(m)]   #初始化备忘录,创建一个m行n列的二维数组。
        print self.getmaxgold(n,m)                 #n,m减一是因为数组是从0开始

    def getmaxgold(self,n,m):
        #retmaxgold = 0
        if self.maxgold[m-1][n-1] != -1:
            retmaxgold = self.maxgold[m-1][n-1]
        elif m == 0:
            if (n >= self.peopleneed[m-1]):
                retmaxgold = self.gold[m-1]
            else:
                retmaxgold = 0
        elif n >= self.peopleneed[m-1]:
            retmaxgold = max(self.getmaxgold(n - self.peopleneed[m-1],m -1)+self.gold[m-1],self.getmaxgold(n,m-1))
        else:
            retmaxgold = self.getmaxgold(n,m-1)
        self.maxgold[m-1][n-1] = retmaxgold
        return retmaxgold

if __name__ == '__main__':
    peopleneed = []
    gold = []
    str = raw_input("")
    try:
        str = str.split(" ")
        n = int(str[0])
        m = int(str[1])
        for i in range(m):
            goldmount = raw_input("")
            goldmount = goldmount.split(" ")
            peopleneed.append(int(goldmount[0]))
            gold.append(int(goldmount[1]))
    except:
        print "输入格式错误"
    dp = Dp(n, m, peopleneed, gold)



输入格式为:

100 5

77 92

22 22

29 87

50 46

99 90

答案: 133




下面找了一些题目练习(动态规划只有练习才能提高...只能不断练习)

例题来源:http://www.cnblogs.com/wuyuegb2312/p/3281264.html#q1

1.硬币找零

难度评级:★

  假设有几种硬币,如1、3、5,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。

代码实现:

# coding=utf-8
class Dp2(object):
    def __init__(self,money):
        self.mark = [0 for _ in xrange(money+1)]                   #备忘录
        print self.dp(money)                                       #开始递归
    def dp(self,money):  
        self.coin = 0                                              #需要的硬币数为0
        if self.mark[money] != 0:                                  #在备忘录中寻找该金额下的最少硬币找零数,若存在,则取出
            self.coin = self.mark[money]
        elif money <= 0:                                           #边界问题
            if money == 0:                                         #如果金额为零,则代表刚好算是一种找零方法
                self.coin = 0                                      #这里的0不是代表硬币数为0,而是代表这种方法可行,因为在下面已经有加1,若是这里coin为1,结果就会比答案多1
            else:
                self.coin = float("inf")                           #若是金额为负数,即“拿多了”,这种方法不可行,则硬币消耗数为 无穷大
        elif money > 0:
            self.coin = min(self.dp(money-1),self.dp(money-3),self.dp(money-5))+1     #递归,找出最少的可以凑齐金额数money的方法
        self.mark[money] = self.coin                               #做备忘录
        return self.coin

if __name__ == '__main__':
    dp2 = Dp2(65)               #找零钱

之后慢慢再更新题目



  • 17
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值